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]