diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index f35911bb117d..2f41131b69eb 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 02223472c26a..d5c10342a5fa 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 4b5b2f826589..2cb835eddff6 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 89fe98f79dd8..208c86e57e91 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 02223472c26a..d5c10342a5fa 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index c0d1a34b0337..6a26bfc7ddc6 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index beb9030d8892..b4d13b136b47 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 5bcc23d0584a..3ad52a6bc845 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "78d735b50d94254ff236fc34a6fbaa5ed27692a0", + "revision": "415520315b048f40979e9bac344bec99e18df901", "repo_path": "integration/gaia-central" } diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 088a6145af75..9867c1f46ee6 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 94cd77036b72..0afeb9b55134 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index e886dce9689c..89177860d172 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 196812c112f4..7b20ab3841ac 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/browser/base/content/browser-loop.js b/browser/base/content/browser-loop.js index 26012b1f94ed..ebc964fc95e5 100644 --- a/browser/base/content/browser-loop.js +++ b/browser/base/content/browser-loop.js @@ -20,22 +20,52 @@ XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/Panel /** * Opens the panel for Loop and sizes it appropriately. * - * @param {event} event The event opening the panel, used to anchor - * the panel to the button which triggers it. + * @param {event} event The event opening the panel, used to anchor + * the panel to the button which triggers it. + * @param {String} [tabId] Identifier of the tab to select when the panel is + * opened. Example: 'rooms', 'contacts', etc. */ - openCallPanel: function(event) { + openCallPanel: function(event, tabId = null) { let callback = iframe => { + // Helper function to show a specific tab view in the panel. + function showTab() { + if (!tabId) { + return; + } + + let win = iframe.contentWindow; + let ev = new win.CustomEvent("UIAction", Cu.cloneInto({ + detail: { + action: "selectTab", + tab: tabId + } + }, win)); + win.dispatchEvent(ev); + } + + // If the panel has been opened and initialized before, we can skip waiting + // for the content to load - because it's already there. + if (("contentWindow" in iframe) && iframe.contentWindow.document.readyState == "complete") { + showTab(); + return; + } + iframe.addEventListener("DOMContentLoaded", function documentDOMLoaded() { iframe.removeEventListener("DOMContentLoaded", documentDOMLoaded, true); injectLoopAPI(iframe.contentWindow); + iframe.contentWindow.addEventListener("loopPanelInitialized", function loopPanelInitialized() { + iframe.contentWindow.removeEventListener("loopPanelInitialized", + loopPanelInitialized); + showTab(); + }); }, true); }; // Used to clear the temporary "login" state from the button. Services.obs.notifyObservers(null, "loop-status-changed", null); - PanelFrame.showPopup(window, event.target, "loop", null, - "about:looppanel", null, callback); + PanelFrame.showPopup(window, event ? event.target : this.toolbarButton.node, + "loop", null, "about:looppanel", null, callback); }, /** @@ -89,6 +119,67 @@ XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/Panel this.toolbarButton.node.setAttribute("state", state); }, + /** + * Show a desktop notification when 'do not disturb' isn't enabled. + * + * @param {Object} options Set of options that may tweak the appearance and + * behavior of the notification. + * Option params: + * - {String} title Notification title message + * - {String} [message] Notification body text + * - {String} [icon] Notification icon + * - {String} [sound] Sound to play + * - {String} [selectTab] Tab to select when the panel + * opens + * - {Function} [onclick] Callback to invoke when + * the notification is clicked. + * Opens the panel by default. + */ + showNotification: function(options) { + if (MozLoopService.doNotDisturb) { + return; + } + + if (!options.title) { + throw new Error("Missing title, can not display notification"); + } + + let notificationOptions = { + body: options.message || "" + }; + if (options.icon) { + notificationOptions.icon = options.icon; + } + if (options.sound) { + // This will not do anything, until bug bug 1105222 is resolved. + notificationOptions.mozbehavior = { + soundFile: `chrome://browser/content/loop/shared/sounds/${options.sound}.ogg` + }; + } + + let notification = new window.Notification(options.title, notificationOptions); + notification.addEventListener("click", e => { + if (window.closed) { + return; + } + + try { + window.focus(); + } catch (ex) {} + + // We need a setTimeout here, otherwise the panel won't show after the + // window received focus. + window.setTimeout(() => { + if (typeof options.onclick == "function") { + options.onclick(); + } else { + // Open the Loop panel as a default action. + this.openCallPanel(null, options.selectTab || null); + } + }, 0); + }); + }, + /** * Play a sound in this window IF there's no sound playing yet. * diff --git a/browser/base/content/test/general/browser_lastAccessedTab.js b/browser/base/content/test/general/browser_lastAccessedTab.js index 3f03c01e4cbd..0fd03b689634 100644 --- a/browser/base/content/test/general/browser_lastAccessedTab.js +++ b/browser/base/content/test/general/browser_lastAccessedTab.js @@ -5,7 +5,7 @@ let originalTab; let newTab; function isCurrent(tab, msg) { - const tolerance = 1; + const tolerance = 5; const difference = Math.abs(Date.now() - tab.lastAccessed); ok(difference <= tolerance, msg + " (difference: " + difference + ")"); } @@ -14,20 +14,20 @@ function test() { waitForExplicitFinish(); originalTab = gBrowser.selectedTab; - setTimeout(step2, 100); + setTimeout(step2, 10); } function step2() { isCurrent(originalTab, "selected tab has the current timestamp"); newTab = gBrowser.addTab("about:blank", {skipAnimation: true}); - setTimeout(step3, 100); + setTimeout(step3, 10); } function step3() { ok(newTab.lastAccessed < Date.now(), "new tab hasn't been selected so far"); gBrowser.selectedTab = newTab; isCurrent(newTab, "new tab has the current timestamp after being selected"); - setTimeout(step4, 100); + setTimeout(step4, 10); } function step4() { diff --git a/browser/components/loop/LoopRooms.jsm b/browser/components/loop/LoopRooms.jsm index 2db795f4ba4a..705baaa2be0c 100644 --- a/browser/components/loop/LoopRooms.jsm +++ b/browser/components/loop/LoopRooms.jsm @@ -93,7 +93,7 @@ const checkForParticipantsUpdate = function(room, updatedRoom) { // Check for participants that joined. for (participant of updatedRoom.participants) { if (!containsParticipant(room, participant)) { - eventEmitter.emit("joined", room.roomToken, participant); + eventEmitter.emit("joined", room, participant); eventEmitter.emit("joined:" + room.roomToken, participant); } } @@ -101,7 +101,7 @@ const checkForParticipantsUpdate = function(room, updatedRoom) { // Check for participants that left. for (participant of room.participants) { if (!containsParticipant(updatedRoom, participant)) { - eventEmitter.emit("left", room.roomToken, participant); + eventEmitter.emit("left", room, participant); eventEmitter.emit("left:" + room.roomToken, participant); } } diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index 512d3d644648..b350e1c8ed72 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -51,7 +51,7 @@ const cloneErrorObject = function(error, targetWindow) { if (typeof value != "string" && typeof value != "number") { value = String(value); } - + Object.defineProperty(Cu.waiveXrays(obj), prop, { configurable: false, enumerable: true, @@ -115,6 +115,8 @@ const injectObjectAPI = function(api, targetWindow) { injectedAPI[func] = function(...params) { let lastParam = params.pop(); let callbackIsFunction = (typeof lastParam == "function"); + // Clone params coming from content to the current scope. + params = [cloneValueInto(p, api) for (p of params)]; // If the last parameter is a function, assume its a callback // and wrap it differently. @@ -134,6 +136,7 @@ const injectObjectAPI = function(api, targetWindow) { }); } else { try { + lastParam = cloneValueInto(lastParam, api); return cloneValueInto(api[func](...params, lastParam), targetWindow); } catch (ex) { return cloneValueInto(ex, targetWindow); diff --git a/browser/components/loop/MozLoopService.jsm b/browser/components/loop/MozLoopService.jsm index 1fda4f541160..7d5c48ef61d2 100644 --- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -1030,7 +1030,7 @@ this.MozLoopService = { }; LoopRooms.on("add", onRoomsChange); LoopRooms.on("update", onRoomsChange); - LoopRooms.on("joined", (e, roomToken, participant) => { + LoopRooms.on("joined", (e, room, participant) => { // Don't alert if we're in the doNotDisturb mode, or the participant // is the owner - the content code deals with the rest of the sounds. if (MozLoopServiceInternal.doNotDisturb || participant.owner) { @@ -1039,7 +1039,12 @@ this.MozLoopService = { let window = gWM.getMostRecentWindow("navigator:browser"); if (window) { - window.LoopUI.playSound("room-joined"); + window.LoopUI.showNotification({ + sound: "room-joined", + title: room.roomName, + message: MozLoopServiceInternal.localizedStrings.get("rooms_room_joined_label"), + selectTab: "rooms" + }); } }); diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index 845602e6a2a0..48392b4f825e 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -759,6 +759,17 @@ loop.panel = (function(_, mozL10n) { }); }, + _UIActionHandler: function(e) { + switch (e.detail.action) { + case "selectTab": + this.selectTab(e.detail.tab); + break; + default: + console.error("Invalid action", e.detail.action); + break; + } + }, + /** * The rooms feature is hidden by default for now. Once it gets mainstream, * this method can be simplified. @@ -803,11 +814,13 @@ loop.panel = (function(_, mozL10n) { componentDidMount: function() { window.addEventListener("LoopStatusChanged", this._onStatusChanged); window.addEventListener("GettingStartedSeen", this._gettingStartedSeen); + window.addEventListener("UIAction", this._UIActionHandler); }, componentWillUnmount: function() { window.removeEventListener("LoopStatusChanged", this._onStatusChanged); window.removeEventListener("GettingStartedSeen", this._gettingStartedSeen); + window.removeEventListener("UIAction", this._UIActionHandler); }, _getUserDisplayName: function() { diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx index 5054eb74aa32..a63892fe92fc 100644 --- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -759,6 +759,17 @@ loop.panel = (function(_, mozL10n) { }); }, + _UIActionHandler: function(e) { + switch (e.detail.action) { + case "selectTab": + this.selectTab(e.detail.tab); + break; + default: + console.error("Invalid action", e.detail.action); + break; + } + }, + /** * The rooms feature is hidden by default for now. Once it gets mainstream, * this method can be simplified. @@ -803,11 +814,13 @@ loop.panel = (function(_, mozL10n) { componentDidMount: function() { window.addEventListener("LoopStatusChanged", this._onStatusChanged); window.addEventListener("GettingStartedSeen", this._gettingStartedSeen); + window.addEventListener("UIAction", this._UIActionHandler); }, componentWillUnmount: function() { window.removeEventListener("LoopStatusChanged", this._onStatusChanged); window.removeEventListener("GettingStartedSeen", this._gettingStartedSeen); + window.removeEventListener("UIAction", this._UIActionHandler); }, _getUserDisplayName: function() { diff --git a/browser/components/loop/content/shared/js/roomStore.js b/browser/components/loop/content/shared/js/roomStore.js index e324784ae273..4b1da8a36c96 100644 --- a/browser/components/loop/content/shared/js/roomStore.js +++ b/browser/components/loop/content/shared/js/roomStore.js @@ -239,11 +239,16 @@ loop.store = loop.store || {}; expiresIn: this.defaultExpiresIn }; - this._mozLoop.rooms.create(roomCreationData, function(err) { + this._mozLoop.rooms.create(roomCreationData, function(err, createdRoom) { this.setStoreState({pendingCreation: false}); if (err) { this.dispatchAction(new sharedActions.CreateRoomError({error: err})); + return; } + // Opens the newly created room + this.dispatchAction(new sharedActions.OpenRoom({ + roomToken: createdRoom.roomToken + })); }.bind(this)); }, diff --git a/browser/components/loop/test/shared/roomStore_test.js b/browser/components/loop/test/shared/roomStore_test.js index 7022d32ba1c5..5fe8158d764f 100644 --- a/browser/components/loop/test/shared/roomStore_test.js +++ b/browser/components/loop/test/shared/roomStore_test.js @@ -80,6 +80,7 @@ describe("loop.store.RoomStore", function () { rooms: { create: function() {}, getAll: function() {}, + open: function() {}, on: sandbox.stub() } }; @@ -230,13 +231,28 @@ describe("loop.store.RoomStore", function () { it("should switch the pendingCreation state flag to false once the " + "operation is done", function() { sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) { - cb(); + cb(null, {roomToken: "fakeToken"}); }); store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData)); expect(store.getStoreState().pendingCreation).eql(false); }); + + it("should dispatch an OpenRoom action once the operation is done", + function() { + var dispatch = sandbox.stub(dispatcher, "dispatch"); + sandbox.stub(fakeMozLoop.rooms, "create", function(data, cb) { + cb(null, {roomToken: "fakeToken"}); + }); + + store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData)); + + sinon.assert.calledOnce(dispatch); + sinon.assert.calledWithExactly(dispatch, new sharedActions.OpenRoom({ + roomToken: "fakeToken" + })); + }); }); describe("#copyRoomUrl", function() { diff --git a/browser/components/loop/test/xpcshell/test_looprooms.js b/browser/components/loop/test/xpcshell/test_looprooms.js index 1db9e03d3663..627fc58017c1 100644 --- a/browser/components/loop/test/xpcshell/test_looprooms.js +++ b/browser/components/loop/test/xpcshell/test_looprooms.js @@ -146,25 +146,25 @@ const onRoomDeleted = function(e, room) { gExpectedDeletes.splice(idx, 1); } -const onRoomJoined = function(e, roomToken, participant) { - let participants = gExpectedJoins[roomToken]; +const onRoomJoined = function(e, room, participant) { + let participants = gExpectedJoins[room.roomToken]; Assert.ok(participants, "Participant should be expected to join"); let idx = participants.indexOf(participant.roomConnectionId); Assert.ok(idx > -1, "Participant should be expected to join"); participants.splice(idx, 1); if (!participants.length) { - delete gExpectedJoins[roomToken]; + delete gExpectedJoins[room.roomToken]; } }; -const onRoomLeft = function(e, roomToken, participant) { - let participants = gExpectedLeaves[roomToken]; +const onRoomLeft = function(e, room, participant) { + let participants = gExpectedLeaves[room.roomToken]; Assert.ok(participants, "Participant should be expected to leave"); let idx = participants.indexOf(participant.roomConnectionId); Assert.ok(idx > -1, "Participant should be expected to leave"); participants.splice(idx, 1); if (!participants.length) { - delete gExpectedLeaves[roomToken]; + delete gExpectedLeaves[room.roomToken]; } }; diff --git a/dom/nfc/gonk/Nfc.js b/dom/nfc/gonk/Nfc.js index 55059fb4fa8b..080f852cd896 100644 --- a/dom/nfc/gonk/Nfc.js +++ b/dom/nfc/gonk/Nfc.js @@ -609,17 +609,9 @@ Nfc.prototype = { * Process a message from the gMessageManager. */ receiveMessage: function receiveMessage(message) { - let isRFAPI = message.name == "NFC:ChangeRFState"; - let isSendFile = message.name == "NFC:SendFile"; - let isInfoAPI = message.name == "NFC:QueryInfo"; - - if (!isRFAPI && !isInfoAPI && (this.rfState != NFC.NFC_RF_STATE_DISCOVERY)) { - debug("NFC is not enabled. current rfState:" + this.rfState); - this.sendNfcErrorResponse(message, NFC.NFC_GECKO_ERROR_NOT_ENABLED); - return null; - } - - if (!isRFAPI && !isSendFile && !isInfoAPI) { + if (["NFC:ChangeRFState", + "NFC:SendFile", + "NFC:QueryInfo"].indexOf(message.name) == -1) { // Update the current sessionId before sending to the NFC service. message.data.sessionId = SessionHelper.getId(message.data.sessionToken); } diff --git a/dom/nfc/gonk/nfc_consts.js b/dom/nfc/gonk/nfc_consts.js index 41f116e836a7..72f88c050df6 100644 --- a/dom/nfc/gonk/nfc_consts.js +++ b/dom/nfc/gonk/nfc_consts.js @@ -26,14 +26,12 @@ this.DEBUG_NFC = DEBUG_ALL || false; this.NFC_GECKO_SUCCESS = 0; this.NFC_GECKO_ERROR_GENERIC_FAILURE = 1; this.NFC_GECKO_ERROR_P2P_REG_INVALID = 2; -this.NFC_GECKO_ERROR_NOT_ENABLED = 3; -this.NFC_GECKO_ERROR_SEND_FILE_FAILED = 4; -this.NFC_GECKO_ERROR_BAD_SESSION_TOKEN = 5; +this.NFC_GECKO_ERROR_SEND_FILE_FAILED = 3; +this.NFC_GECKO_ERROR_BAD_SESSION_TOKEN = 4; this.NFC_ERROR_MSG = {}; this.NFC_ERROR_MSG[this.NFC_GECKO_ERROR_GENERIC_FAILURE] = "NfcGenericFailureError"; this.NFC_ERROR_MSG[this.NFC_GECKO_ERROR_P2P_REG_INVALID] = "NfcP2PRegistrationInvalid"; -this.NFC_ERROR_MSG[this.NFC_GECKO_ERROR_NOT_ENABLED] = "NfcNotEnabledError"; this.NFC_ERROR_MSG[this.NFC_GECKO_ERROR_SEND_FILE_FAILED] = "NfcSendFileFailed"; this.NFC_ERROR_MSG[this.NFC_GECKO_ERROR_BAD_SESSION_TOKEN] = "NfcBadSessionToken"; diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 33a2bce50752..59c14b1e24c4 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -1514,14 +1514,17 @@ public abstract class GeckoApp // External URLs should always be loaded regardless of whether Gecko is // already running. if (isExternalURL) { + // Restore tabs before opening an external URL so that the new tab + // is animated properly. + Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED); loadStartupTab(passedUri); - } else if (!mIsRestoringActivity) { - loadStartupTab(null); - } + } else { + if (!mIsRestoringActivity) { + loadStartupTab(null); + } - // We now have tab stubs from the last session. Any future tabs should - // be animated. - Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED); + Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED); + } // If we're not restoring, move the session file so it can be read for // the last tabs section. diff --git a/mobile/android/base/Tabs.java b/mobile/android/base/Tabs.java index 80462512a748..311650259c76 100644 --- a/mobile/android/base/Tabs.java +++ b/mobile/android/base/Tabs.java @@ -87,7 +87,6 @@ public class Tabs implements GeckoEventListener { private Tabs() { EventDispatcher.getInstance().registerGeckoThreadListener(this, - "Session:RestoreEnd", "Tab:Added", "Tab:Close", "Tab:Select", @@ -410,11 +409,6 @@ public class Tabs implements GeckoEventListener { public void handleMessage(String event, JSONObject message) { Log.d(LOGTAG, "handleMessage: " + event); try { - if (event.equals("Session:RestoreEnd")) { - notifyListeners(null, TabEvents.RESTORED); - return; - } - // All other events handled below should contain a tabID property int id = message.getInt("tabID"); Tab tab = getTab(id); diff --git a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_default_favicon.png b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_default_favicon.png index 9a0ab144207f..fb483f40b1c2 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_default_favicon.png and b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_default_favicon.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_back.png b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_back.png index db4ee5b8ef24..1903e73bcd4c 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_back.png and b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_back.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_forward.png b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_forward.png index f8bcc2573963..6294796e2a9d 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_forward.png and b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_forward.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_reload.png b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_reload.png index 5bd9a95a1613..8c724067b388 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_reload.png and b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_ic_menu_reload.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_menu.png b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_menu.png index 4f06493f51ea..eae6d51d5356 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_menu.png and b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_menu.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_tab_close_active.png b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_tab_close_active.png index ca09ae09adbd..260378bc9603 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_tab_close_active.png and b/mobile/android/base/newtablet/res/drawable-large-hdpi-v11/new_tablet_tab_close_active.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_default_favicon.png b/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_default_favicon.png index 1d5e8b2eaef5..ed47cbf7d0ce 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_default_favicon.png and b/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_default_favicon.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_back.png b/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_back.png index 9c8462ed15f4..72c1583efd55 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_back.png and b/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_back.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_forward.png b/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_forward.png index fd0c94ac7e07..c798967b70a2 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_forward.png and b/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_forward.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_reload.png b/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_reload.png index 20678853b846..4d99edf357bc 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_reload.png and b/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_ic_menu_reload.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_tab_close_active.png b/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_tab_close_active.png index e24adb499599..82a81adbb0f5 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_tab_close_active.png and b/mobile/android/base/newtablet/res/drawable-large-mdpi-v11/new_tablet_tab_close_active.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_default_favicon.png b/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_default_favicon.png index 32723effcb89..cfdf745588a5 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_default_favicon.png and b/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_default_favicon.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_ic_menu_back.png b/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_ic_menu_back.png index a21969df84f1..6e94d969c638 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_ic_menu_back.png and b/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_ic_menu_back.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_ic_menu_forward.png b/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_ic_menu_forward.png index e8b901553c45..28d390d1c3d3 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_ic_menu_forward.png and b/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_ic_menu_forward.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_menu.png b/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_menu.png index 832ea534548c..a788766c8b1e 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_menu.png and b/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_menu.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_tab_close_active.png b/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_tab_close_active.png index cb997070af27..b9d65312f6c7 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_tab_close_active.png and b/mobile/android/base/newtablet/res/drawable-large-xhdpi-v11/new_tablet_tab_close_active.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_default_favicon.png b/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_default_favicon.png index 567b5b62a40e..70a1443b7b9e 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_default_favicon.png and b/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_default_favicon.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_ic_menu_back.png b/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_ic_menu_back.png index d27bc18f0fe7..7941c7dbc420 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_ic_menu_back.png and b/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_ic_menu_back.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_ic_menu_forward.png b/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_ic_menu_forward.png index bfd523ae7bfb..54beae24db81 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_ic_menu_forward.png and b/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_ic_menu_forward.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_menu.png b/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_menu.png index c50c9cfeca8d..cf1dba453893 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_menu.png and b/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_menu.png differ diff --git a/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_tab_close_active.png b/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_tab_close_active.png index 8e8f299f092c..cc35204c3bc8 100644 Binary files a/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_tab_close_active.png and b/mobile/android/base/newtablet/res/drawable-large-xxhdpi-v11/new_tablet_tab_close_active.png differ diff --git a/mobile/android/base/tabs/TabStrip.java b/mobile/android/base/tabs/TabStrip.java index 2db821758a9b..3e99f2f52571 100644 --- a/mobile/android/base/tabs/TabStrip.java +++ b/mobile/android/base/tabs/TabStrip.java @@ -103,6 +103,9 @@ public class TabStrip extends ThemedLinearLayout { public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) { switch (msg) { case RESTORED: + tabStripView.restoreTabs(); + break; + case ADDED: tabStripView.addTab(tab); break; diff --git a/mobile/android/base/tabs/TabStripView.java b/mobile/android/base/tabs/TabStripView.java index c6a1837632f4..7a7a0d16364d 100644 --- a/mobile/android/base/tabs/TabStripView.java +++ b/mobile/android/base/tabs/TabStripView.java @@ -44,6 +44,8 @@ public class TabStripView extends TwoWayView { private final TabAnimatorListener animatorListener; + private boolean isRestoringTabs; + // Filled by calls to ShapeDrawable.getPadding(); // saved to prevent allocation in draw(). private final Rect dividerPadding = new Rect(); @@ -205,7 +207,46 @@ public class TabStripView extends TwoWayView { }); } + private void animateRestoredTabs() { + getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { + @Override + public boolean onPreDraw() { + getViewTreeObserver().removeOnPreDrawListener(this); + + final List childAnimators = new ArrayList(); + + final int tabHeight = getHeight() - getPaddingTop() - getPaddingBottom(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + childAnimators.add( + ObjectAnimator.ofFloat(child, "translationY", tabHeight, 0)); + } + + final AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(childAnimators); + animatorSet.setDuration(ANIM_TIME_MS); + animatorSet.setInterpolator(ANIM_INTERPOLATOR); + animatorSet.addListener(animatorListener); + + TransitionsTracker.track(animatorSet); + + animatorSet.start(); + + return true; + } + }); + } + private void ensurePositionIsVisible(final int position) { + // We just want to move the strip to the right position + // when restoring tabs on startup. + if (isRestoringTabs) { + setSelection(position); + return; + } + getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() { @Override public boolean onPreDraw() { @@ -244,6 +285,13 @@ public class TabStripView extends TwoWayView { adapter.clear(); } + void restoreTabs() { + isRestoringTabs = true; + refreshTabs(); + animateRestoredTabs(); + isRestoringTabs = false; + } + void addTab(Tab tab) { // Refresh the list to make sure the new tab is // added in the right position. diff --git a/mobile/android/base/tabs/TabsGridLayout.java b/mobile/android/base/tabs/TabsGridLayout.java index 505d00637dca..544577944c7f 100644 --- a/mobile/android/base/tabs/TabsGridLayout.java +++ b/mobile/android/base/tabs/TabsGridLayout.java @@ -6,6 +6,7 @@ package org.mozilla.gecko.tabs; import java.util.ArrayList; +import java.util.List; import org.mozilla.gecko.animation.ViewHelper; import org.mozilla.gecko.GeckoAppShell; @@ -18,13 +19,22 @@ import org.mozilla.gecko.Tabs; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.PointF; import android.util.AttributeSet; -import android.util.TypedValue; +import android.util.SparseArray; import android.view.Gravity; import android.view.View; -import android.widget.GridView; import android.view.ViewGroup; +import android.view.ViewTreeObserver; +import android.view.animation.DecelerateInterpolator; import android.widget.Button; +import android.widget.GridView; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorSet; +import com.nineoldandroids.animation.ObjectAnimator; +import com.nineoldandroids.animation.PropertyValuesHolder; +import com.nineoldandroids.animation.ValueAnimator; + /** * A tabs layout implementation for the tablet redesign (bug 1014156). @@ -36,12 +46,18 @@ class TabsGridLayout extends GridView Tabs.OnTabsChangedListener { private static final String LOGTAG = "Gecko" + TabsGridLayout.class.getSimpleName(); + private static final int ANIM_TIME_MS = 200; + public static final int ANIM_DELAY_MULTIPLE_MS = 20; + private static final DecelerateInterpolator ANIM_INTERPOLATOR = new DecelerateInterpolator(); + private final Context mContext; private TabsPanel mTabsPanel; + private final SparseArray mTabLocations = new SparseArray(); final private boolean mIsPrivate; private final TabsLayoutAdapter mTabsAdapter; + private final int mColumnWidth; public TabsGridLayout(Context context, AttributeSet attrs) { super(context, attrs, R.attr.tabGridLayoutViewStyle); @@ -67,9 +83,13 @@ class TabsGridLayout extends GridView setGravity(Gravity.CENTER); setNumColumns(GridView.AUTO_FIT); + // The clipToPadding setting in the styles.xml doesn't seem to be working (bug 1101784) + // so lets set it manually in code for the moment as it's needed for the padding animation + setClipToPadding(false); + final Resources resources = getResources(); - final int columnWidth = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_column_width); - setColumnWidth(columnWidth); + mColumnWidth = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_column_width); + setColumnWidth(mColumnWidth); final int padding = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding); final int paddingTop = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding_top); @@ -87,9 +107,7 @@ class TabsGridLayout extends GridView mCloseClickListener = new Button.OnClickListener() { @Override public void onClick(View v) { - TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag(); - Tab tab = Tabs.getInstance().getTab(itemView.getTabId()); - Tabs.getInstance().closeTab(tab); + closeTab(v); } }; @@ -121,6 +139,47 @@ class TabsGridLayout extends GridView } } + private void populateTabLocations(final Tab removedTab) { + mTabLocations.clear(); + + final int firstPosition = getFirstVisiblePosition(); + final int lastPosition = getLastVisiblePosition(); + final int numberOfColumns = getNumColumns(); + final int childCount = getChildCount(); + final int removedPosition = mTabsAdapter.getPositionForTab(removedTab); + + for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) { + final View child = getChildAt(i); + if (child != null) { + mTabLocations.append(x, new PointF(child.getX(), child.getY())); + } + } + + final boolean firstChildOffScreen = ((firstPosition > 0) || getChildAt(0).getY() < 0); + final boolean lastChildVisible = (lastPosition - childCount == firstPosition - 1); + final boolean oneItemOnLastRow = (lastPosition % numberOfColumns == 0); + if (firstChildOffScreen && lastChildVisible && oneItemOnLastRow) { + // We need to set the view's bottom padding to prevent a sudden jump as the + // last item in the row is being removed. We then need to remove the padding + // via a sweet animation + + final int removedHeight = getChildAt(0).getMeasuredHeight(); + final int verticalSpacing = getVerticalSpacing(); + + ValueAnimator paddingAnimator = ValueAnimator.ofInt(getPaddingBottom() + removedHeight + verticalSpacing, getPaddingBottom()); + paddingAnimator.setDuration(ANIM_TIME_MS * 2); + + paddingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), (Integer) animation.getAnimatedValue()); + } + }); + paddingAnimator.start(); + } + } + @Override public void setTabsPanel(TabsPanel panel) { mTabsPanel = panel; @@ -160,6 +219,9 @@ class TabsGridLayout extends GridView break; case CLOSED: + if(mTabsAdapter.getCount() > 0) { + animateRemoveTab(tab); + } if (tab.isPrivate() == mIsPrivate && mTabsAdapter.getCount() > 0) { if (mTabsAdapter.removeTab(tab)) { int selected = mTabsAdapter.getPositionForTab(Tabs.getInstance().getSelectedTab()); @@ -244,4 +306,90 @@ class TabsGridLayout extends GridView } } } + + private View getViewForTab(Tab tab) { + final int position = mTabsAdapter.getPositionForTab(tab); + return getChildAt(position - getFirstVisiblePosition()); + } + + void closeTab(View v) { + TabsLayoutItemView itemView = (TabsLayoutItemView) v.getTag(); + Tab tab = Tabs.getInstance().getTab(itemView.getTabId()); + + Tabs.getInstance().closeTab(tab); + updateSelectedPosition(); + } + + private void animateRemoveTab(final Tab removedTab) { + final int removedPosition = mTabsAdapter.getPositionForTab(removedTab); + + final View removedView = getViewForTab(removedTab); + + // The removed position might not have a matching child view + // when it's not within the visible range of positions in the strip. + if (removedView == null) { + return; + } + final int removedHeight = removedView.getMeasuredHeight(); + + populateTabLocations(removedTab); + + getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + getViewTreeObserver().removeOnPreDrawListener(this); + // We don't animate the removed child view (it just disappears) + // but we still need its size to animate all affected children + // within the visible viewport. + final int childCount = getChildCount(); + final int firstPosition = getFirstVisiblePosition(); + final int numberOfColumns = getNumColumns(); + + final List childAnimators = new ArrayList<>(); + + PropertyValuesHolder translateX, translateY; + for (int x = 0, i = removedPosition - firstPosition ; i < childCount; i++, x++) { + final View child = getChildAt(i); + ObjectAnimator animator; + + if (i % numberOfColumns == numberOfColumns - 1) { + // Animate X & Y + translateX = PropertyValuesHolder.ofFloat("translationX", -(mColumnWidth * numberOfColumns), 0); + translateY = PropertyValuesHolder.ofFloat("translationY", removedHeight, 0); + animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX, translateY); + } else { + // Just animate X + translateX = PropertyValuesHolder.ofFloat("translationX", mColumnWidth, 0); + animator = ObjectAnimator.ofPropertyValuesHolder(child, translateX); + } + animator.setStartDelay(x * ANIM_DELAY_MULTIPLE_MS); + childAnimators.add(animator); + } + + final AnimatorSet animatorSet = new AnimatorSet(); + animatorSet.playTogether(childAnimators); + animatorSet.setDuration(ANIM_TIME_MS); + animatorSet.setInterpolator(ANIM_INTERPOLATOR); + animatorSet.start(); + + // Set the starting position of the child views - because we are delaying the start + // of the animation, we need to prevent the items being drawn in their final position + // prior to the animation starting + for (int x = 1, i = (removedPosition - firstPosition) + 1; i < childCount; i++, x++) { + final View child = getChildAt(i); + + final PointF targetLocation = mTabLocations.get(x+1); + if (targetLocation == null) { + continue; + } + + child.setX(targetLocation.x); + child.setY(targetLocation.y); + } + + return true; + } + }); + } + } diff --git a/mobile/android/components/SessionStore.js b/mobile/android/components/SessionStore.js index b3a4767008fe..30a016976d5a 100644 --- a/mobile/android/components/SessionStore.js +++ b/mobile/android/components/SessionStore.js @@ -142,11 +142,6 @@ SessionStore.prototype = { selected: true }); } - - // Let Java know we're done restoring tabs so tabs added after this can be animated - Messaging.sendRequest({ - type: "Session:RestoreEnd" - }); }.bind(this) }; Services.obs.addObserver(restoreCleanup, "sessionstore-windows-restored", false); diff --git a/netwerk/protocol/http/nsHttpHandler.cpp b/netwerk/protocol/http/nsHttpHandler.cpp index a3a6d312a447..1ef14cdf1bc7 100644 --- a/netwerk/protocol/http/nsHttpHandler.cpp +++ b/netwerk/protocol/http/nsHttpHandler.cpp @@ -1519,6 +1519,25 @@ nsHttpHandler::TimerCallback(nsITimer * aTimer, void * aClosure) thisObject->mCapabilities &= ~NS_HTTP_ALLOW_PIPELINING; } +static void +NormalizeLanguageTag(char *code) +{ + bool is_region = false; + while (*code != '\0') + { + if (*code == '-') { + is_region = true; + } else { + if (is_region) { + *code = nsCRT::ToUpper(*code); + } else { + *code = nsCRT::ToLower(*code); + } + } + code++; + } +} + /** * Allocates a C string into that contains a ISO 639 language list * notated with HTTP "q" values for output with a HTTP Accept-Language @@ -1574,6 +1593,8 @@ PrepareAcceptLanguages(const char *i_AcceptLanguages, nsACString &o_AcceptLangua *trim = '\0'; if (*token != '\0') { + NormalizeLanguageTag(token); + comma = count_n++ != 0 ? "," : ""; // delimiter if not first item uint32_t u = QVAL_TO_UINT(q); diff --git a/netwerk/test/unit/test_header_Accept-Language_case.js b/netwerk/test/unit/test_header_Accept-Language_case.js new file mode 100644 index 000000000000..c13494dbb7af --- /dev/null +++ b/netwerk/test/unit/test_header_Accept-Language_case.js @@ -0,0 +1,32 @@ +var testpath = "/bug1054739"; + +function run_test() { + let intlPrefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).getBranch("intl."); + + let oldAcceptLangPref = intlPrefs.getCharPref("accept_languages"); + + let testData = [ + ["de, en-US, en", "de,en-US;q=0.7,en;q=0.3"], + ["de,en-us,en", "de,en-US;q=0.7,en;q=0.3"], + ["en-US, en", "en-US,en;q=0.5"], + ["EN-US;q=0.2, EN", "en-US,en;q=0.5"], + ]; + + for (let i = 0; i < testData.length; i++) { + let acceptLangPref = testData[i][0]; + let expectedHeader = testData[i][1]; + + intlPrefs.setCharPref("accept_languages", acceptLangPref); + let acceptLangHeader = setupChannel(testpath).getRequestHeader("Accept-Language"); + equal(acceptLangHeader, expectedHeader); + } + + intlPrefs.setCharPref("accept_languages", oldAcceptLangPref); +} + +function setupChannel(path) { + let ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); + let chan = ios.newChannel("http://localhost:4444" + path, "", null); + chan.QueryInterface(Ci.nsIHttpChannel); + return chan; +} diff --git a/netwerk/test/unit/xpcshell.ini b/netwerk/test/unit/xpcshell.ini index 1510577028b4..54df63810940 100644 --- a/netwerk/test/unit/xpcshell.ini +++ b/netwerk/test/unit/xpcshell.ini @@ -198,6 +198,7 @@ skip-if = bits != 32 [test_gzipped_206.js] [test_head.js] [test_header_Accept-Language.js] +[test_header_Accept-Language_case.js] [test_headers.js] [test_http_headers.js] [test_httpauth.js] diff --git a/services/mobileid/MobileIdentityVerificationFlow.jsm b/services/mobileid/MobileIdentityVerificationFlow.jsm index ad7583aaf35e..bd0f66ccbede 100644 --- a/services/mobileid/MobileIdentityVerificationFlow.jsm +++ b/services/mobileid/MobileIdentityVerificationFlow.jsm @@ -37,68 +37,67 @@ MobileIdentityVerificationFlow.prototype = { return Promise.reject(ERROR_INTERNAL_UNEXPECTED); } this.sessionToken = registerResult.msisdnSessionToken; - return this._doVerification(); + // We save the timestamp of the start of the verification timeout to be + // able to provide to the UI the remaining time on each retry. + if (!this.timer) { + log.debug("Creating verification code timer"); + this.timerCreation = Date.now(); + this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + this.timer.initWithCallback(this.onVerificationCodeTimeout.bind(this), + VERIFICATIONCODE_TIMEOUT, + this.timer.TYPE_ONE_SHOT); + } + + if (!this.verifyStrategy) { + return Promise.reject(ERROR_INTERNAL_INVALID_VERIFICATION_FLOW); + } + + return this.verifyStrategy() + .then(() => { + return this._doVerification(); + }, (reason) => { + this.verificationCodeDeferred.reject(reason); + }); } ) }, _doVerification: function() { log.debug("_doVerification"); - // We save the timestamp of the start of the verification timeout to be - // able to provide to the UI the remaining time on each retry. - if (!this.timer) { - log.debug("Creating verification code timer"); - this.timerCreation = Date.now(); - this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); - this.timer.initWithCallback(this.onVerificationCodeTimeout.bind(this), - VERIFICATIONCODE_TIMEOUT, - this.timer.TYPE_ONE_SHOT); - } - - if (!this.verifyStrategy) { - return Promise.reject(ERROR_INTERNAL_INVALID_VERIFICATION_FLOW); - } this.verificationCodeDeferred = Promise.defer(); - this.verifyStrategy() - .then( - () => { - // If the verification flow can be for an external phone number, - // we need to ask the user for the verification code. - // In that case we don't do a notification about the verification - // process being done until the user enters the verification code - // in the UI. - if (this.verificationOptions.external) { - let timeLeft = 0; - if (this.timer) { - timeLeft = this.timerCreation + VERIFICATIONCODE_TIMEOUT - - Date.now(); - } - this.ui.verificationCodePrompt(this.retries, - VERIFICATIONCODE_TIMEOUT / 1000, - timeLeft / 1000) - .then( - (verificationCode) => { - if (!verificationCode) { - return this.verificationCodeDeferred.reject( - ERROR_INTERNAL_INVALID_PROMPT_RESULT); - } - // If the user got the verification code that means that the - // introduced phone number didn't belong to any of the inserted - // SIMs. - this.ui.verify(); - this.verificationCodeDeferred.resolve(verificationCode); - } - ); - } else { - this.ui.verify(); - } - }, - (reason) => { - this.verificationCodeDeferred.reject(reason); + // If the verification flow can be for an external phone number, + // we need to ask the user for the verification code. + // In that case we don't do a notification about the verification + // process being done until the user enters the verification code + // in the UI. + if (this.verificationOptions.external) { + let timeLeft = 0; + if (this.timer) { + timeLeft = this.timerCreation + VERIFICATIONCODE_TIMEOUT - + Date.now(); } - ); + this.ui.verificationCodePrompt(this.retries, + VERIFICATIONCODE_TIMEOUT / 1000, + timeLeft / 1000) + .then( + (verificationCode) => { + if (!verificationCode) { + return this.verificationCodeDeferred.reject( + ERROR_INTERNAL_INVALID_PROMPT_RESULT); + } + // If the user got the verification code that means that the + // introduced phone number didn't belong to any of the inserted + // SIMs. + this.ui.verify(); + this.verificationCodeDeferred.resolve(verificationCode); + } + ); + } else { + this.ui.verify(); + } + return this.verificationCodeDeferred.promise.then( this.onVerificationCode.bind(this) ); @@ -145,8 +144,11 @@ MobileIdentityVerificationFlow.prototype = { log.error("Retries left " + this.retries); if (!this.retries) { this.ui.error(ERROR_NO_RETRIES_LEFT); + this.timer.cancel(); + this.timer = null; return Promise.reject(ERROR_NO_RETRIES_LEFT); } + this.ui.error(ERROR_INVALID_VERIFICATION_CODE); this.verifying = false; if (this.queuedTimeout) { this.onVerificationCodeTimeout(); diff --git a/services/mobileid/tests/xpcshell/head.js b/services/mobileid/tests/xpcshell/head.js index 751ae81c77eb..28048e2dab6f 100644 --- a/services/mobileid/tests/xpcshell/head.js +++ b/services/mobileid/tests/xpcshell/head.js @@ -5,6 +5,8 @@ const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components; "use strict"; +const Cm = Components.manager; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); @@ -17,3 +19,444 @@ Cu.import("resource://gre/modules/Services.jsm"); Services.prefs.setCharPref("services.mobileid.server.uri", "https://dummyurl.com"); }).call(this); + +const DEBUG = false; + +const GET_ASSERTION_IPC_MSG = "MobileId:GetAssertion"; +const GET_ASSERTION_RETURN_OK = "MobileId:GetAssertion:Return:OK"; +const GET_ASSERTION_RETURN_KO = "MobileId:GetAssertion:Return:KO"; + +// === Globals === + +const ORIGIN = "app://afakeorigin"; +const APP_ID = 1; +const PRINCIPAL = { + origin: ORIGIN, + appId: APP_ID +}; +const PHONE_NUMBER = "+34666555444"; +const ANOTHER_PHONE_NUMBER = "+44123123123"; +const VERIFICATION_CODE = "123456"; +const SESSION_TOKEN = "aSessionToken"; +const ICC_ID = "aIccId"; +const ANOTHER_ICC_ID = "anotherIccId"; +const MNC = "aMnc"; +const ANOTHER_MNC = "anotherMnc"; +const MCC = "aMcc"; +const ANOTHER_MCC = "anotherMcc"; +const OPERATOR = "aOperator"; +const ANOTHER_OPERATOR = "anotherOperator"; +const RADIO_INTERFACE = { + rilContext: { + iccInfo: { + iccid: ICC_ID, + mcc: MCC, + mnc: MNC, + msisdn: PHONE_NUMBER, + operator: OPERATOR + } + }, + voice: { + network: { + shortName: OPERATOR + }, + roaming: false + }, + data: { + network: { + shortName: OPERATOR + } + } +}; +const ANOTHER_RADIO_INTERFACE = { + rilContext: { + iccInfo: { + iccid: ANOTHER_ICC_ID, + mcc: ANOTHER_MCC, + mnc: ANOTHER_MNC, + msisdn: ANOTHER_PHONE_NUMBER, + operator: ANOTHER_OPERATOR + } + }, + voice: { + network: { + shortName: ANOTHER_OPERATOR + }, + roaming: false + }, + data: { + network: { + shortName: ANOTHER_OPERATOR + } + } +}; + +const INVALID_RADIO_INTERFACE = { + rilContext: { + iccInfo: { + iccid: null, + mcc: "", + mnc: "", + msisdn: "", + operator: "" + } + }, + voice: { + network: { + shortName: "" + }, + roaming: undefined + }, + data: { + network: { + shortName: "" + } + } +}; + +const CERTIFICATE = "eyJhbGciOiJEUzI1NiJ9.eyJsYXN0QXV0aEF0IjoxNDA0NDY5NzkyODc3LCJ2ZXJpZmllZE1TSVNETiI6IiszMTYxNzgxNTc1OCIsInB1YmxpYy1rZXkiOnsiYWxnb3JpdGhtIjoiRFMiLCJ5IjoiNGE5YzkzNDY3MWZhNzQ3YmM2ZjMyNjE0YTg1MzUyZjY5NDcwMDdhNTRkMDAxMDY4OWU5ZjJjZjc0ZGUwYTEwZTRlYjlmNDk1ZGFmZTA0NGVjZmVlNDlkN2YwOGU4ODQyMDJiOTE5OGRhNWZhZWE5MGUzZjRmNzE1YzZjNGY4Yjc3MGYxZTU4YWZhNDM0NzVhYmFiN2VlZGE1MmUyNjk2YzFmNTljNzMzYjFlYzBhNGNkOTM1YWIxYzkyNzAxYjNiYTA5ZDRhM2E2MzNjNTJmZjE2NGYxMWY3OTg1YzlmZjY3ZThmZDFlYzA2NDU3MTdkMjBiNDE4YmM5M2YzYzVkNCIsInAiOiJmZjYwMDQ4M2RiNmFiZmM1YjQ1ZWFiNzg1OTRiMzUzM2Q1NTBkOWYxYmYyYTk5MmE3YThkYWE2ZGMzNGY4MDQ1YWQ0ZTZlMGM0MjlkMzM0ZWVlYWFlZmQ3ZTIzZDQ4MTBiZTAwZTRjYzE0OTJjYmEzMjViYTgxZmYyZDVhNWIzMDVhOGQxN2ViM2JmNGEwNmEzNDlkMzkyZTAwZDMyOTc0NGE1MTc5MzgwMzQ0ZTgyYTE4YzQ3OTMzNDM4Zjg5MWUyMmFlZWY4MTJkNjljOGY3NWUzMjZjYjcwZWEwMDBjM2Y3NzZkZmRiZDYwNDYzOGMyZWY3MTdmYzI2ZDAyZTE3IiwicSI6ImUyMWUwNGY5MTFkMWVkNzk5MTAwOGVjYWFiM2JmNzc1OTg0MzA5YzMiLCJnIjoiYzUyYTRhMGZmM2I3ZTYxZmRmMTg2N2NlODQxMzgzNjlhNjE1NGY0YWZhOTI5NjZlM2M4MjdlMjVjZmE2Y2Y1MDhiOTBlNWRlNDE5ZTEzMzdlMDdhMmU5ZTJhM2NkNWRlYTcwNGQxNzVmOGViZjZhZjM5N2Q2OWUxMTBiOTZhZmIxN2M3YTAzMjU5MzI5ZTQ4MjliMGQwM2JiYzc4OTZiMTViNGFkZTUzZTEzMDg1OGNjMzRkOTYyNjlhYTg5MDQxZjQwOTEzNmM3MjQyYTM4ODk1YzlkNWJjY2FkNGYzODlhZjFkN2E0YmQxMzk4YmQwNzJkZmZhODk2MjMzMzk3YSJ9LCJwcmluY2lwYWwiOiIwMzgxOTgyYS0xZTgzLTI1NjYtNjgzZS05MDRmNDA0NGM1MGRAbXNpc2RuLWRldi5zdGFnZS5tb3phd3MubmV0IiwiaWF0IjoxNDA0NDY5NzgyODc3LCJleHAiOjE0MDQ0OTEzOTI4NzcsImlzcyI6Im1zaXNkbi1kZXYuc3RhZ2UubW96YXdzLm5ldCJ9." + +// === Helpers === + +function addPermission(aAction) { + let uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(ORIGIN, null, null); + let _principal = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager) + .getAppCodebasePrincipal(uri, APP_ID, false); + let pm = Cc["@mozilla.org/permissionmanager;1"] + .getService(Ci.nsIPermissionManager); + pm.addFromPrincipal(_principal, MOBILEID_PERM, aAction); +} + +function removePermission() { + let uri = Cc["@mozilla.org/network/io-service;1"] + .getService(Ci.nsIIOService) + .newURI(ORIGIN, null, null); + let _principal = Cc["@mozilla.org/scriptsecuritymanager;1"] + .getService(Ci.nsIScriptSecurityManager) + .getAppCodebasePrincipal(uri, APP_ID, false); + let pm = Cc["@mozilla.org/permissionmanager;1"] + .getService(Ci.nsIPermissionManager); + pm.removeFromPrincipal(_principal, MOBILEID_PERM); +} + +// === Mocks === + +let Mock = function(aOptions) { + if (!aOptions) { + aOptions = {}; + } + this._options = aOptions; + this._spied = {}; +}; + +Mock.prototype = { + _: function(aMethod) { + DEBUG && do_print("_ " + aMethod + JSON.stringify(this._spied)); + let self = this; + return { + callsLength: function(aNumberOfCalls) { + if (aNumberOfCalls == 0) { + do_check_eq(self._spied[aMethod], undefined); + return; + } + do_check_eq(self._spied[aMethod].length, aNumberOfCalls); + }, + call: function(aCallNumber) { + return { + arg: function(aArgNumber, aValue) { + let _arg = self._spied[aMethod][aCallNumber - 1][aArgNumber - 1]; + if (Array.isArray(aValue)) { + do_check_eq(_arg.length, aValue.length) + for (let i = 0; i < _arg.length; i++) { + do_check_eq(_arg[i], aValue[i]); + } + return; + } + + if (typeof aValue === 'object') { + do_check_eq(JSON.stringify(_arg), JSON.stringify(aValue)); + return; + } + + do_check_eq(_arg, aValue); + } + } + } + } + }, + + _spy: function(aMethod, aArgs) { + DEBUG && do_print(aMethod + " - " + JSON.stringify(aArgs)); + if (!this._spied[aMethod]) { + this._spied[aMethod] = []; + } + this._spied[aMethod].push(aArgs); + }, + + getSpiedCalls: function(aMethod) { + return this._spied[aMethod]; + } +}; + +// UI Glue mock up. +let MockUi = function(aOptions) { + Mock.call(this, aOptions); +}; + +MockUi.prototype = { + __proto__: Mock.prototype, + + _startFlowResult: { + phoneNumber: PHONE_NUMBER, + mcc: MNC + }, + + _verifyCodePromptResult: { + verificationCode: VERIFICATION_CODE + }, + + startFlow: function() { + this._spy("startFlow", arguments); + return Promise.resolve(this._options.startFlowResult || + this._startFlowResult); + }, + + verificationCodePrompt: function() { + this._spy("verifyCodePrompt", arguments); + return Promise.resolve(this._options.verificationCodePromptResult || + this._verifyCodePromptResult); + }, + + verify: function() { + this._spy("verify", arguments); + }, + + error: function() { + this._spy("error", arguments); + }, + + verified: function() { + this._spy("verified", arguments); + }, + + set oncancel(aCallback) { + }, + + set onresendcode(aCallback) { + } +}; + +// Credentials store mock up. +let MockCredStore = function(aOptions) { + Mock.call(this, aOptions); +}; + +MockCredStore.prototype = { + __proto__: Mock.prototype, + + _getByOriginResult: null, + + _getByMsisdnResult: null, + + _getByIccIdResult: null, + + getByOrigin: function() { + this._spy("getByOrigin", arguments); + let result = this._getByOriginResult; + if (this._options.getByOriginResult) { + if (Array.isArray(this._options.getByOriginResult)) { + result = this._options.getByOriginResult.length ? + this._options.getByOriginResult.shift() : null; + } else { + result = this._options.getByOriginResult; + } + } + return Promise.resolve(result); + }, + + getByMsisdn: function() { + this._spy("getByMsisdn", arguments); + return Promise.resolve(this._options.getByMsisdnResult || + this._getByMsisdnResult); + }, + + getByIccId: function() { + this._spy("getByIccId", arguments); + return Promise.resolve(this._options.getByIccIdResult || + this._getByIccIdResult); + }, + + add: function() { + this._spy("add", arguments); + return Promise.resolve(); + }, + + setDeviceIccIds: function() { + this._spy("setDeviceIccIds", arguments); + return Promise.resolve(); + }, + + removeOrigin: function() { + this._spy("removeOrigin", arguments); + return Promise.resolve(); + }, + + delete: function() { + this._spy("delete", arguments); + return Promise.resolve(); + } +}; + +// Client mock up. +let MockClient = function(aOptions) { + Mock.call(this, aOptions); +}; + +MockClient.prototype = { + + __proto__: Mock.prototype, + + _discoverResult: { + verificationMethods: ["sms/mt"], + verificationDetails: { + "sms/mt": { + mtSender: "123", + url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify" + } + } + }, + + _registerResult: { + msisdnSessionToken: SESSION_TOKEN + }, + + _smsMtVerifyResult: {}, + + _verifyCodeResult: { + msisdn: PHONE_NUMBER + }, + + _signResult: { + cert: CERTIFICATE + }, + + hawk: { + now: function() { + return Date.now(); + } + }, + + discover: function() { + this._spy("discover", arguments); + return Promise.resolve(this._options.discoverResult || + this._discoverResult); + }, + + register: function() { + this._spy("register", arguments); + return Promise.resolve(this._options.registerResult || + this._registerResult); + }, + + smsMtVerify: function() { + this._spy("smsMtVerify", arguments); + return Promise.resolve(this._options.smsMtVerifyResult || + this._smsMtVerifyResult); + }, + + verifyCode: function() { + this._spy("verifyCode", arguments); + if (this._options.verifyCodeError) { + let error = Array.isArray(this._options.verifyCodeError) ? + this._options.verifyCodeError.shift() : + this._options.verifyCodeError; + if (!this._options.verifyCodeError.length) { + this._options.verifyCodeError = null; + } + return Promise.reject(error); + } + return Promise.resolve(this._options.verifyCodeResult || + this._verifyCodeResult); + }, + + sign: function() { + this._spy("sign", arguments); + if (this._options.signError) { + let error = Array.isArray(this._options.signError) ? + this._options.signError.shift() : + this._options.signError; + return Promise.reject(error); + } + return Promise.resolve(this._options.signResult || this._signResult); + } +}; + +// Override MobileIdentityUIGlue. +const kMobileIdentityUIGlueUUID = "{05df0566-ca8a-4ec7-bc76-78626ebfbe9a}"; +const kMobileIdentityUIGlueContractID = + "@mozilla.org/services/mobileid-ui-glue;1"; + +// Save original factory. +/*const kMobileIdentityUIGlueFactory = + Cm.getClassObject(Cc[kMobileIdentityUIGlueContractID], Ci.nsIFactory);*/ + +let fakeMobileIdentityUIGlueFactory = { + createInstance: function(aOuter, aIid) { + return MobileIdentityUIGlue.QueryInterface(aIid); + } +}; + +// MobileIdentityUIGlue fake component. +let MobileIdentityUIGlue = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIMobileIdentityUIGlue]), + +}; + +(function registerFakeMobileIdentityUIGlue() { + Cm.QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory(Components.ID(kMobileIdentityUIGlueUUID), + "MobileIdentityUIGlue", + kMobileIdentityUIGlueContractID, + fakeMobileIdentityUIGlueFactory); +})(); + +// The tests rely on having an app registered. Otherwise, it will throw. +// Override XULAppInfo. +const XUL_APP_INFO_UUID = Components.ID("{84fdc459-d96d-421c-9bff-a8193233ae75}"); +const XUL_APP_INFO_CONTRACT_ID = "@mozilla.org/xre/app-info;1"; + +let (XULAppInfo = { + vendor: "Mozilla", + name: "MobileIdTest", + ID: "{230de50e-4cd1-11dc-8314-0800200b9a66}", + version: "1", + appBuildID: "2007010101", + platformVersion: "", + platformBuildID: "2007010101", + inSafeMode: false, + logConsoleErrors: true, + OS: "XPCShell", + XPCOMABI: "noarch-spidermonkey", + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIXULAppInfo, + Ci.nsIXULRuntime, + ]) +}) { + let XULAppInfoFactory = { + createInstance: function (outer, iid) { + if (outer != null) { + throw Cr.NS_ERROR_NO_AGGREGATION; + } + return XULAppInfo.QueryInterface(iid); + } + }; + Cm.QueryInterface(Ci.nsIComponentRegistrar) + .registerFactory(XUL_APP_INFO_UUID, + "XULAppInfo", + XUL_APP_INFO_CONTRACT_ID, + XULAppInfoFactory); +} diff --git a/services/mobileid/tests/xpcshell/test_mobileid_manager.js b/services/mobileid/tests/xpcshell/test_mobileid_manager.js index e778e93ab563..cbd8e0a9bb50 100644 --- a/services/mobileid/tests/xpcshell/test_mobileid_manager.js +++ b/services/mobileid/tests/xpcshell/test_mobileid_manager.js @@ -3,424 +3,16 @@ "use strict"; -const Cm = Components.manager; - Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/MobileIdentityManager.jsm"); Cu.import("resource://gre/modules/MobileIdentityCommon.jsm"); -const DEBUG = false; - -const GET_ASSERTION_IPC_MSG = "MobileId:GetAssertion"; -const GET_ASSERTION_RETURN_OK = "MobileId:GetAssertion:Return:OK"; -const GET_ASSERTION_RETURN_KO = "MobileId:GetAssertion:Return:KO"; - -// === Globals === - -const ORIGIN = "app://afakeorigin"; -const APP_ID = 1; -const PRINCIPAL = { - origin: ORIGIN, - appId: APP_ID -}; -const PHONE_NUMBER = "+34666555444"; -const ANOTHER_PHONE_NUMBER = "+44123123123"; -const VERIFICATION_CODE = "123456"; -const SESSION_TOKEN = "aSessionToken"; -const ICC_ID = "aIccId"; -const ANOTHER_ICC_ID = "anotherIccId"; -const MNC = "aMnc"; -const ANOTHER_MNC = "anotherMnc"; -const MCC = "aMcc"; -const ANOTHER_MCC = "anotherMcc"; -const OPERATOR = "aOperator"; -const ANOTHER_OPERATOR = "anotherOperator"; -const RADIO_INTERFACE = { - rilContext: { - iccInfo: { - iccid: ICC_ID, - mcc: MCC, - mnc: MNC, - msisdn: PHONE_NUMBER, - operator: OPERATOR - } - }, - voice: { - network: { - shortName: OPERATOR - }, - roaming: false - }, - data: { - network: { - shortName: OPERATOR - } - } -}; -const ANOTHER_RADIO_INTERFACE = { - rilContext: { - iccInfo: { - iccid: ANOTHER_ICC_ID, - mcc: ANOTHER_MCC, - mnc: ANOTHER_MNC, - msisdn: ANOTHER_PHONE_NUMBER, - operator: ANOTHER_OPERATOR - } - }, - voice: { - network: { - shortName: ANOTHER_OPERATOR - }, - roaming: false - }, - data: { - network: { - shortName: ANOTHER_OPERATOR - } - } -}; - -const INVALID_RADIO_INTERFACE = { - rilContext: { - iccInfo: { - iccid: null, - mcc: "", - mnc: "", - msisdn: "", - operator: "" - } - }, - voice: { - network: { - shortName: "" - }, - roaming: undefined - }, - data: { - network: { - shortName: "" - } - } -}; - -const CERTIFICATE = "eyJhbGciOiJEUzI1NiJ9.eyJsYXN0QXV0aEF0IjoxNDA0NDY5NzkyODc3LCJ2ZXJpZmllZE1TSVNETiI6IiszMTYxNzgxNTc1OCIsInB1YmxpYy1rZXkiOnsiYWxnb3JpdGhtIjoiRFMiLCJ5IjoiNGE5YzkzNDY3MWZhNzQ3YmM2ZjMyNjE0YTg1MzUyZjY5NDcwMDdhNTRkMDAxMDY4OWU5ZjJjZjc0ZGUwYTEwZTRlYjlmNDk1ZGFmZTA0NGVjZmVlNDlkN2YwOGU4ODQyMDJiOTE5OGRhNWZhZWE5MGUzZjRmNzE1YzZjNGY4Yjc3MGYxZTU4YWZhNDM0NzVhYmFiN2VlZGE1MmUyNjk2YzFmNTljNzMzYjFlYzBhNGNkOTM1YWIxYzkyNzAxYjNiYTA5ZDRhM2E2MzNjNTJmZjE2NGYxMWY3OTg1YzlmZjY3ZThmZDFlYzA2NDU3MTdkMjBiNDE4YmM5M2YzYzVkNCIsInAiOiJmZjYwMDQ4M2RiNmFiZmM1YjQ1ZWFiNzg1OTRiMzUzM2Q1NTBkOWYxYmYyYTk5MmE3YThkYWE2ZGMzNGY4MDQ1YWQ0ZTZlMGM0MjlkMzM0ZWVlYWFlZmQ3ZTIzZDQ4MTBiZTAwZTRjYzE0OTJjYmEzMjViYTgxZmYyZDVhNWIzMDVhOGQxN2ViM2JmNGEwNmEzNDlkMzkyZTAwZDMyOTc0NGE1MTc5MzgwMzQ0ZTgyYTE4YzQ3OTMzNDM4Zjg5MWUyMmFlZWY4MTJkNjljOGY3NWUzMjZjYjcwZWEwMDBjM2Y3NzZkZmRiZDYwNDYzOGMyZWY3MTdmYzI2ZDAyZTE3IiwicSI6ImUyMWUwNGY5MTFkMWVkNzk5MTAwOGVjYWFiM2JmNzc1OTg0MzA5YzMiLCJnIjoiYzUyYTRhMGZmM2I3ZTYxZmRmMTg2N2NlODQxMzgzNjlhNjE1NGY0YWZhOTI5NjZlM2M4MjdlMjVjZmE2Y2Y1MDhiOTBlNWRlNDE5ZTEzMzdlMDdhMmU5ZTJhM2NkNWRlYTcwNGQxNzVmOGViZjZhZjM5N2Q2OWUxMTBiOTZhZmIxN2M3YTAzMjU5MzI5ZTQ4MjliMGQwM2JiYzc4OTZiMTViNGFkZTUzZTEzMDg1OGNjMzRkOTYyNjlhYTg5MDQxZjQwOTEzNmM3MjQyYTM4ODk1YzlkNWJjY2FkNGYzODlhZjFkN2E0YmQxMzk4YmQwNzJkZmZhODk2MjMzMzk3YSJ9LCJwcmluY2lwYWwiOiIwMzgxOTgyYS0xZTgzLTI1NjYtNjgzZS05MDRmNDA0NGM1MGRAbXNpc2RuLWRldi5zdGFnZS5tb3phd3MubmV0IiwiaWF0IjoxNDA0NDY5NzgyODc3LCJleHAiOjE0MDQ0OTEzOTI4NzcsImlzcyI6Im1zaXNkbi1kZXYuc3RhZ2UubW96YXdzLm5ldCJ9." - -// === Helpers === - -function addPermission(aAction) { - let uri = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService) - .newURI(ORIGIN, null, null); - let _principal = Cc["@mozilla.org/scriptsecuritymanager;1"] - .getService(Ci.nsIScriptSecurityManager) - .getAppCodebasePrincipal(uri, APP_ID, false); - let pm = Cc["@mozilla.org/permissionmanager;1"] - .getService(Ci.nsIPermissionManager); - pm.addFromPrincipal(_principal, MOBILEID_PERM, aAction); -} - -function removePermission() { - let uri = Cc["@mozilla.org/network/io-service;1"] - .getService(Ci.nsIIOService) - .newURI(ORIGIN, null, null); - let _principal = Cc["@mozilla.org/scriptsecuritymanager;1"] - .getService(Ci.nsIScriptSecurityManager) - .getAppCodebasePrincipal(uri, APP_ID, false); - let pm = Cc["@mozilla.org/permissionmanager;1"] - .getService(Ci.nsIPermissionManager); - pm.removeFromPrincipal(_principal, MOBILEID_PERM); -} - -// === Mocks === - -let Mock = function(aOptions) { - if (!aOptions) { - aOptions = {}; - } - this._options = aOptions; - this._spied = {}; -}; - -Mock.prototype = { - _: function(aMethod) { - DEBUG && do_print("_ " + aMethod + JSON.stringify(this._spied)); - let self = this; - return { - callsLength: function(aNumberOfCalls) { - if (aNumberOfCalls == 0) { - do_check_eq(self._spied[aMethod], undefined); - return; - } - do_check_eq(self._spied[aMethod].length, aNumberOfCalls); - }, - call: function(aCallNumber) { - return { - arg: function(aArgNumber, aValue) { - let _arg = self._spied[aMethod][aCallNumber - 1][aArgNumber - 1]; - if (Array.isArray(aValue)) { - do_check_eq(_arg.length, aValue.length) - for (let i = 0; i < _arg.length; i++) { - do_check_eq(_arg[i], aValue[i]); - } - return; - } - - if (typeof aValue === 'object') { - do_check_eq(JSON.stringify(_arg), JSON.stringify(aValue)); - return; - } - - do_check_eq(_arg, aValue); - } - } - } - } - }, - - _spy: function(aMethod, aArgs) { - DEBUG && do_print(aMethod + " - " + JSON.stringify(aArgs)); - if (!this._spied[aMethod]) { - this._spied[aMethod] = []; - } - this._spied[aMethod].push(aArgs); - }, - - getSpiedCalls: function(aMethod) { - return this._spied[aMethod]; - } -}; - -// UI Glue mock up. -let MockUi = function(aOptions) { - Mock.call(this, aOptions); -}; - -MockUi.prototype = { - __proto__: Mock.prototype, - - _startFlowResult: { - phoneNumber: PHONE_NUMBER, - mcc: MNC - }, - - _verifyCodePromptResult: { - verificationCode: VERIFICATION_CODE - }, - - startFlow: function() { - this._spy("startFlow", arguments); - return Promise.resolve(this._options.startFlowResult || - this._startFlowResult); - }, - - verificationCodePrompt: function() { - this._spy("verifyCodePrompt", arguments); - return Promise.resolve(this._options.verificationCodePromptResult || - this._verifyCodePromptResult); - }, - - verify: function() { - this._spy("verify", arguments); - }, - - error: function() { - this._spy("error", arguments); - }, - - verified: function() { - this._spy("verified", arguments); - }, - - set oncancel(aCallback) { - }, - - set onresendcode(aCallback) { - } -}; - // Save original credential store instance. const kMobileIdentityCredStore = MobileIdentityManager.credStore; - -// Credentials store mock up. -let MockCredStore = function(aOptions) { - Mock.call(this, aOptions); -}; - -MockCredStore.prototype = { - __proto__: Mock.prototype, - - _getByOriginResult: null, - - _getByMsisdnResult: null, - - _getByIccIdResult: null, - - getByOrigin: function() { - this._spy("getByOrigin", arguments); - let result = this._getByOriginResult; - if (this._options.getByOriginResult) { - if (Array.isArray(this._options.getByOriginResult)) { - result = this._options.getByOriginResult.length ? - this._options.getByOriginResult.shift() : null; - } else { - result = this._options.getByOriginResult; - } - } - return Promise.resolve(result); - }, - - getByMsisdn: function() { - this._spy("getByMsisdn", arguments); - return Promise.resolve(this._options.getByMsisdnResult || - this._getByMsisdnResult); - }, - - getByIccId: function() { - this._spy("getByIccId", arguments); - return Promise.resolve(this._options.getByIccIdResult || - this._getByIccIdResult); - }, - - add: function() { - this._spy("add", arguments); - return Promise.resolve(); - }, - - setDeviceIccIds: function() { - this._spy("setDeviceIccIds", arguments); - return Promise.resolve(); - }, - - removeOrigin: function() { - this._spy("removeOrigin", arguments); - return Promise.resolve(); - }, - - delete: function() { - this._spy("delete", arguments); - return Promise.resolve(); - } -}; - // Save original client instance. const kMobileIdentityClient = MobileIdentityManager.client; -// Client mock up. -let MockClient = function(aOptions) { - Mock.call(this, aOptions); -}; - -MockClient.prototype = { - - __proto__: Mock.prototype, - - _discoverResult: { - verificationMethods: ["sms/mt"], - verificationDetails: { - "sms/mt": { - mtSender: "123", - url: "https://msisdn.accounts.firefox.com/v1/msisdn/sms/mt/verify" - } - } - }, - - _registerResult: { - msisdnSessionToken: SESSION_TOKEN - }, - - _smsMtVerifyResult: {}, - - _verifyCodeResult: { - msisdn: PHONE_NUMBER - }, - - _signResult: { - cert: CERTIFICATE - }, - - hawk: { - now: function() { - return Date.now(); - } - }, - - discover: function() { - this._spy("discover", arguments); - return Promise.resolve(this._options.discoverResult || - this._discoverResult); - }, - - register: function() { - this._spy("register", arguments); - return Promise.resolve(this._options.registerResult || - this._registerResult); - }, - - smsMtVerify: function() { - this._spy("smsMtVerify", arguments); - return Promise.resolve(this._options.smsMtVerifyResult || - this._smsMtVerifyResult); - }, - - verifyCode: function() { - this._spy("verifyCode", arguments); - return Promise.resolve(this._options.verifyCodeResult || - this._verifyCodeResult); - }, - - sign: function() { - this._spy("sign", arguments); - if (this._options.signError) { - let error = Array.isArray(this._options.signError) ? - this._options.signError.shift() : - this._options.signError; - return Promise.reject(error); - } - return Promise.resolve(this._options.signResult || this._signResult); - } -}; - -// The test rely on having an app registered. Otherwise, it will throw. -// Override XULAppInfo. -const XUL_APP_INFO_UUID = Components.ID("{84fdc459-d96d-421c-9bff-a8193233ae75}"); -const XUL_APP_INFO_CONTRACT_ID = "@mozilla.org/xre/app-info;1"; - -let (XULAppInfo = { - vendor: "Mozilla", - name: "MobileIdTest", - ID: "{230de50e-4cd1-11dc-8314-0800200b9a66}", - version: "1", - appBuildID: "2007010101", - platformVersion: "", - platformBuildID: "2007010101", - inSafeMode: false, - logConsoleErrors: true, - OS: "XPCShell", - XPCOMABI: "noarch-spidermonkey", - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsIXULAppInfo, - Ci.nsIXULRuntime, - ]) -}) { - let XULAppInfoFactory = { - createInstance: function (outer, iid) { - if (outer != null) { - throw Cr.NS_ERROR_NO_AGGREGATION; - } - return XULAppInfo.QueryInterface(iid); - } - }; - Cm.QueryInterface(Ci.nsIComponentRegistrar) - .registerFactory(XUL_APP_INFO_UUID, - "XULAppInfo", - XUL_APP_INFO_CONTRACT_ID, - XULAppInfoFactory); -} - // === Global cleanup === - function cleanup() { MobileIdentityManager.credStore = kMobileIdentityCredStore; MobileIdentityManager.client = kMobileIdentityClient; @@ -431,7 +23,6 @@ function cleanup() { // Unregister mocks and restore original code. do_register_cleanup(cleanup); - // === Tests === function run_test() { run_next_test(); diff --git a/services/mobileid/tests/xpcshell/test_mobileid_verification_flow.js b/services/mobileid/tests/xpcshell/test_mobileid_verification_flow.js new file mode 100644 index 000000000000..7f6938b69a56 --- /dev/null +++ b/services/mobileid/tests/xpcshell/test_mobileid_verification_flow.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Cu.import("resource://gre/modules/MobileIdentityVerificationFlow.jsm"); + +function verifyStrategy() { + return Promise.resolve(); +} + +function cleanupStrategy() { +} + +function run_test() { + do_print("= Bug 1101444: Invalid verification code shouldn't restart " + + "verification flow ="); + + let client = new MockClient({ + // This will emulate two invalid attempts. The third time it will work. + verifyCodeError: ["INVALID", "INVALID"] + }); + let ui = new MockUi(); + + let verificationFlow = new MobileIdentityVerificationFlow({ + external: true, + sessionToken: SESSION_TOKEN, + msisdn: PHONE_NUMBER + }, ui, client, verifyStrategy, cleanupStrategy); + + verificationFlow.doVerification().then(() => { + // We should only do the registration process once. We only try registering + // again when the timeout fires, but not when we enter an invalid + // verification code. + client._("register").callsLength(1); + client._("verifyCode").callsLength(3); + // Because we do two invalid attempts, we should show the invalid code error twice. + ui._("error").callsLength(2); + }); + + do_test_finished(); +}; diff --git a/services/mobileid/tests/xpcshell/xpcshell.ini b/services/mobileid/tests/xpcshell/xpcshell.ini index 49ee9e8526d1..b221062ebb5f 100644 --- a/services/mobileid/tests/xpcshell/xpcshell.ini +++ b/services/mobileid/tests/xpcshell/xpcshell.ini @@ -1,7 +1,8 @@ [DEFAULT] head = head.js tail = -skip-if = toolkit == 'gonk' [test_mobileid_manager.js] +skip-if = 1 [test_mobileid_client.js] +[test_mobileid_verification_flow.js]