Merge m-c to m-i
authorPhil Ringnalda <philringnalda@gmail.com>
Thu, 27 Nov 2014 17:43:48 -0800
changeset 217921 66722c270acb01215da6fb05a780d1b36c136d3b
parent 217920 9fc2c2ca23952b8942c21645a85e7a6a9c098774 (current diff)
parent 217916 63abc656b8656a8407015a6f9541bd46719f9759 (diff)
child 217922 585dbcf446ea270c4185a36f45df33e45e1cd130
push id27897
push userphilringnalda@gmail.com
push dateFri, 28 Nov 2014 06:30:27 +0000
treeherderautoland@1162e4a4d7a2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to m-i
--- a/b2g/config/dolphin/sources.xml
+++ b/b2g/config/dolphin/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="3ab0d9c70f0b2e1ededc679112c392303f037361">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6499615ecece69e726657dc5caaeefa05fbb66bf"/>
--- a/b2g/config/emulator-ics/sources.xml
+++ b/b2g/config/emulator-ics/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d5d3f93914558b6f168447b805cd799c8233e300"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="8b13bfc1d7d25cee4de55f332654fdba25b8460b"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6499615ecece69e726657dc5caaeefa05fbb66bf"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/emulator-jb/sources.xml
+++ b/b2g/config/emulator-jb/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="0e94c080bee081a50aa2097527b0b40852f9143f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6499615ecece69e726657dc5caaeefa05fbb66bf"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/emulator-kk/sources.xml
+++ b/b2g/config/emulator-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="3ab0d9c70f0b2e1ededc679112c392303f037361">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6499615ecece69e726657dc5caaeefa05fbb66bf"/>
--- a/b2g/config/emulator/sources.xml
+++ b/b2g/config/emulator/sources.xml
@@ -14,17 +14,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="refs/tags/android-4.0.4_r2.1" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d5d3f93914558b6f168447b805cd799c8233e300"/>
   <project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="8b13bfc1d7d25cee4de55f332654fdba25b8460b"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6499615ecece69e726657dc5caaeefa05fbb66bf"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="dd924f92906085b831bf1cbbc7484d3c043d613c"/>
--- a/b2g/config/flame-kk/sources.xml
+++ b/b2g/config/flame-kk/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was git://codeaurora.org/-->
   <remote fetch="https://git.mozilla.org/external/caf" name="caf"/>
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="3ab0d9c70f0b2e1ededc679112c392303f037361">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6499615ecece69e726657dc5caaeefa05fbb66bf"/>
--- a/b2g/config/flame/sources.xml
+++ b/b2g/config/flame/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="0e94c080bee081a50aa2097527b0b40852f9143f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6499615ecece69e726657dc5caaeefa05fbb66bf"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="e95b4ce22c825da44d14299e1190ea39a5260bde"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="471afab478649078ad7c75ec6b252481a59e19b8"/>
--- a/b2g/config/gaia.json
+++ b/b2g/config/gaia.json
@@ -1,9 +1,9 @@
 {
     "git": {
         "git_revision": "", 
         "remote": "", 
         "branch": ""
     }, 
-    "revision": "78d735b50d94254ff236fc34a6fbaa5ed27692a0", 
+    "revision": "415520315b048f40979e9bac344bec99e18df901", 
     "repo_path": "integration/gaia-central"
 }
--- a/b2g/config/hamachi/sources.xml
+++ b/b2g/config/hamachi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6499615ecece69e726657dc5caaeefa05fbb66bf"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
   <project name="platform/bootable/recovery" path="bootable/recovery" revision="746bc48f34f5060f90801925dcdd964030c1ab6d"/>
--- a/b2g/config/helix/sources.xml
+++ b/b2g/config/helix/sources.xml
@@ -10,17 +10,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <default remote="caf" revision="b2g/ics_strawberry" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
   <project name="platform/bionic" path="bionic" revision="d2eb6c7b6e1bc7643c17df2d9d9bcb1704d0b9ab"/>
--- a/b2g/config/nexus-4/sources.xml
+++ b/b2g/config/nexus-4/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was https://git.mozilla.org/releases-->
   <remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
   <!-- B2G specific things. -->
   <project name="platform_build" path="build" remote="b2g" revision="0e94c080bee081a50aa2097527b0b40852f9143f">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
-  <project name="gaia" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6499615ecece69e726657dc5caaeefa05fbb66bf"/>
   <project name="valgrind" path="external/valgrind" remote="b2g" revision="daa61633c32b9606f58799a3186395fd2bbb8d8c"/>
   <project name="vex" path="external/VEX" remote="b2g" revision="47f031c320888fe9f3e656602588565b52d43010"/>
   <!-- Stock Android things -->
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.1" path="prebuilts/clang/linux-x86/3.1" revision="5c45f43419d5582949284eee9cef0c43d866e03b"/>
   <project groups="linux" name="platform/prebuilts/clang/linux-x86/3.2" path="prebuilts/clang/linux-x86/3.2" revision="3748b4168e7bd8d46457d4b6786003bc6a5223ce"/>
--- a/b2g/config/wasabi/sources.xml
+++ b/b2g/config/wasabi/sources.xml
@@ -12,17 +12,17 @@
   <!--original fetch url was git://github.com/apitrace/-->
   <remote fetch="https://git.mozilla.org/external/apitrace" name="apitrace"/>
   <default remote="caf" revision="ics_chocolate_rb4.2" sync-j="4"/>
   <!-- Gonk specific things and forks -->
   <project name="platform_build" path="build" remote="b2g" revision="df362ace56338da8173d30d3e09e08c42c1accfa">
     <copyfile dest="Makefile" src="core/root.mk"/>
   </project>
   <project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
-  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="80bc1445959db79e9d2e947cc56e1eb7b0d3d0f0"/>
+  <project name="gaia.git" path="gaia" remote="mozillaorg" revision="1fff49c664f905f11a86426a9835e6df6b58e825"/>
   <project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
   <project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
   <project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
   <project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
   <project name="apitrace" path="external/apitrace" remote="apitrace" revision="6499615ecece69e726657dc5caaeefa05fbb66bf"/>
   <project name="gonk-patches" path="patches" remote="b2g" revision="223a2421006e8f5da33f516f6891c87cae86b0f6"/>
   <!-- Stock Android things -->
   <project name="platform/abi/cpp" path="abi/cpp" revision="6426040f1be4a844082c9769171ce7f5341a5528"/>
--- a/browser/base/content/browser-loop.js
+++ b/browser/base/content/browser-loop.js
@@ -15,32 +15,62 @@ XPCOMUtils.defineLazyModuleGetter(this, 
     get toolbarButton() {
       delete this.toolbarButton;
       return this.toolbarButton = CustomizableUI.getWidget("loop-button").forWindow(window);
     },
 
     /**
      * 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);
     },
 
     /**
      * Triggers the initialization of the loop service.  Called by
      * delayedStartup.
      */
     init: function() {
       // Add observer notifications before the service is initialized
@@ -85,16 +115,77 @@ XPCOMUtils.defineLazyModuleGetter(this, 
         state = "disabled";
       } else if (MozLoopService.roomsParticipantsCount > 0) {
         state = "active";
       }
       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.
      *
      * @param {String} name Name of the sound, like 'ringtone' or 'room-joined'
      */
     playSound: function(name) {
       if (this.ActiveSound || MozLoopService.doNotDisturb) {
         return;
       }
--- a/browser/base/content/test/general/browser_lastAccessedTab.js
+++ b/browser/base/content/test/general/browser_lastAccessedTab.js
@@ -1,38 +1,38 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 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 + ")");
 }
 
 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() {
   ok(originalTab.lastAccessed < Date.now(),
      "original tab has old timestamp after being deselected");
   isCurrent(newTab, "new tab has the current timestamp since it's still selected");
 
   gBrowser.removeTab(newTab);
--- a/browser/components/loop/LoopRooms.jsm
+++ b/browser/components/loop/LoopRooms.jsm
@@ -88,25 +88,25 @@ const checkForParticipantsUpdate = funct
   if (!("participants" in room)) {
     return;
   }
 
   let participant;
   // 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);
     }
   }
 
   // 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);
     }
   }
 };
 
 /**
  * The Rooms class.
  *
--- a/browser/components/loop/MozLoopAPI.jsm
+++ b/browser/components/loop/MozLoopAPI.jsm
@@ -46,17 +46,17 @@ this.EXPORTED_SYMBOLS = ["injectLoopAPI"
  */
 const cloneErrorObject = function(error, targetWindow) {
   let obj = new targetWindow.Error();
   for (let prop of Object.getOwnPropertyNames(error)) {
     let value = error[prop];
     if (typeof value != "string" && typeof value != "number") {
       value = String(value);
     }
-    
+
     Object.defineProperty(Cu.waiveXrays(obj), prop, {
       configurable: false,
       enumerable: true,
       value: value,
       writable: false
     });
   }
   return obj;
@@ -110,16 +110,18 @@ const cloneValueInto = function(value, t
 const injectObjectAPI = function(api, targetWindow) {
   let injectedAPI = {};
   // Wrap all the methods in `api` to help results passed to callbacks get
   // through the priv => unpriv barrier with `Cu.cloneInto()`.
   Object.keys(api).forEach(func => {
     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.
       if (callbackIsFunction) {
         api[func](...params, function(...results) {
           // When the function was garbage collected due to async events, like
           // closing a window, we want to circumvent a JS error.
           if (callbackIsFunction && typeof lastParam != "function") {
@@ -129,16 +131,17 @@ const injectObjectAPI = function(api, ta
               MozLoopService.log.error(func + " error:", results[0]);
             }
             return;
           }
           lastParam(...[cloneValueInto(r, targetWindow) for (r of results)]);
         });
       } else {
         try {
+          lastParam = cloneValueInto(lastParam, api);
           return cloneValueInto(api[func](...params, lastParam), targetWindow);
         } catch (ex) {
           return cloneValueInto(ex, targetWindow);
         }
       }
     };
   });
 
--- a/browser/components/loop/MozLoopService.jsm
+++ b/browser/components/loop/MozLoopService.jsm
@@ -1025,26 +1025,31 @@ this.MozLoopService = {
 
     // The Loop toolbar button should change icon when the room participant count
     // changes from 0 to something.
     const onRoomsChange = () => {
       MozLoopServiceInternal.notifyStatusChanged();
     };
     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) {
         return;
       }
 
       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"
+        });
       }
     });
 
     // If expiresTime is not in the future and the user hasn't
     // previously authenticated then skip registration.
     if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
         !LoopRooms.getGuestCreatedRoom() &&
         !MozLoopServiceInternal.fxAOAuthTokenData) {
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -754,16 +754,27 @@ loop.panel = (function(_, mozL10n) {
     },
 
     _gettingStartedSeen: function() {
       this.setState({
         gettingStartedSeen: navigator.mozLoop.getLoopPref("gettingStarted.seen"),
       });
     },
 
+    _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.
      */
     _renderRoomsOrCallTab: function() {
       if (!this._roomsEnabled()) {
         return (
           Tab({name: "call"}, 
@@ -798,21 +809,23 @@ loop.panel = (function(_, mozL10n) {
 
     componentWillMount: function() {
       this.updateServiceErrors();
     },
 
     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() {
       return this.state.userProfile && this.state.userProfile.email ||
              mozL10n.get("display_name_guest");
     },
 
     render: function() {
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -754,16 +754,27 @@ loop.panel = (function(_, mozL10n) {
     },
 
     _gettingStartedSeen: function() {
       this.setState({
         gettingStartedSeen: navigator.mozLoop.getLoopPref("gettingStarted.seen"),
       });
     },
 
+    _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.
      */
     _renderRoomsOrCallTab: function() {
       if (!this._roomsEnabled()) {
         return (
           <Tab name="call">
@@ -798,21 +809,23 @@ loop.panel = (function(_, mozL10n) {
 
     componentWillMount: function() {
       this.updateServiceErrors();
     },
 
     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() {
       return this.state.userProfile && this.state.userProfile.email ||
              mozL10n.get("display_name_guest");
     },
 
     render: function() {
--- a/browser/components/loop/content/shared/js/roomStore.js
+++ b/browser/components/loop/content/shared/js/roomStore.js
@@ -234,21 +234,26 @@ loop.store = loop.store || {};
 
       var roomCreationData = {
         roomName:  this._generateNewRoomName(actionData.nameTemplate),
         roomOwner: actionData.roomOwner,
         maxSize:   this.maxRoomCreationSize,
         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));
     },
 
     /**
      * Executed when a room creation error occurs.
      *
      * @param {sharedActions.CreateRoomError} actionData The action data.
      */
--- a/browser/components/loop/test/shared/roomStore_test.js
+++ b/browser/components/loop/test/shared/roomStore_test.js
@@ -75,16 +75,17 @@ describe("loop.store.RoomStore", functio
     };
 
     beforeEach(function() {
       fakeMozLoop = {
         copyString: function() {},
         rooms: {
           create: function() {},
           getAll: function() {},
+          open: function() {},
           on: sandbox.stub()
         }
       };
       store = new loop.store.RoomStore(dispatcher, {mozLoop: fakeMozLoop});
       store.setStoreState(defaultStoreState);
     });
 
     describe("MozLoop rooms event listeners", function() {
@@ -225,23 +226,38 @@ describe("loop.store.RoomStore", functio
         store.createRoom(new sharedActions.CreateRoom(fakeRoomCreationData));
 
         expect(store.getStoreState().pendingCreation).eql(true);
       });
 
       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() {
       it("should copy the room URL", function() {
         var copyString = sandbox.stub(fakeMozLoop, "copyString");
 
         store.copyRoomUrl(new sharedActions.CopyRoomUrl({
           roomUrl: "http://invalid"
--- a/browser/components/loop/test/xpcshell/test_looprooms.js
+++ b/browser/components/loop/test/xpcshell/test_looprooms.js
@@ -141,35 +141,35 @@ const onRoomUpdated = function(e, room) 
 };
 
 const onRoomDeleted = function(e, room) {
   let idx = gExpectedDeletes.indexOf(room.roomToken);
   Assert.ok(idx > -1, "Deleted room should be expected");
   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];
   }
 };
 
 const parseQueryString = function(qs) {
   let map = {};
   let parts = qs.split("=");
   for (let i = 0, l = parts.length; i < l; ++i) {
     if (i % 2 === 1) {
--- a/dom/nfc/gonk/Nfc.js
+++ b/dom/nfc/gonk/Nfc.js
@@ -604,27 +604,19 @@ Nfc.prototype = {
      */
     gSystemMessenger.broadcastMessage("nfc-hci-event-transaction", message);
   },
 
   /**
    * 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);
     }
 
     switch (message.name) {
       case "NFC:ChangeRFState":
         this.sendToNfcService("changeRFState", message.data);
         break;
--- a/dom/nfc/gonk/nfc_consts.js
+++ b/dom/nfc/gonk/nfc_consts.js
@@ -21,24 +21,22 @@ this.DEBUG_ALL = false;
 // Set individually to debug specific layers
 this.DEBUG_CONTENT_HELPER = DEBUG_ALL || false;
 this.DEBUG_NFC = DEBUG_ALL || false;
 
 // Gecko specific error codes
 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";
 
 this.NFC_RF_STATE_IDLE = "idle";
 this.NFC_RF_STATE_LISTEN = "listen";
 this.NFC_RF_STATE_DISCOVERY = "discovery";
 
 this.TOPIC_MOZSETTINGS_CHANGED      = "mozsettings-changed";
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1509,25 +1509,28 @@ public abstract class GeckoApp
             }
 
             GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Restore", restoreMessage));
         }
 
         // 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);
+            }
+
+            Tabs.getInstance().notifyListeners(null, Tabs.TabEvents.RESTORED);
         }
 
-        // We now have tab stubs from the last session. Any future tabs should
-        // be animated.
-        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.
         if (!mShouldRestore) {
             getProfile().moveSessionFile();
         }
 
         Telemetry.HistogramAdd("FENNEC_STARTUP_GECKOAPP_ACTION", startupAction.ordinal());
 
--- a/mobile/android/base/Tabs.java
+++ b/mobile/android/base/Tabs.java
@@ -82,17 +82,16 @@ public class Tabs implements GeckoEventL
                     TabsAccessor.persistLocalTabs(getContentResolver(), getTabsInOrder());
                 }
             } catch (SecurityException se) {} // will fail without android.permission.GET_ACCOUNTS
         }
     };
 
     private Tabs() {
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
-            "Session:RestoreEnd",
             "Tab:Added",
             "Tab:Close",
             "Tab:Select",
             "Content:LocationChange",
             "Content:SecurityChange",
             "Content:ReaderEnabled",
             "Content:StateChange",
             "Content:LoadError",
@@ -405,21 +404,16 @@ public class Tabs implements GeckoEventL
        return Tabs.TabsInstanceHolder.INSTANCE;
     }
 
     // GeckoEventListener implementation
     @Override
     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);
 
             // "Tab:Added" is a special case because tab will be null if the tab was just added
             if (event.equals("Tab:Added")) {
                 String url = message.isNull("uri") ? null : message.getString("uri");
 
index 9a0ab144207fd6d2a44550adb4e7a862fcf9e374..fb483f40b1c217d68fc7dd6f5ab50eac296a7574
GIT binary patch
literal 159
zc%17D@N?(olHy`uVBq!ia0vp^5+KaN3?zjj6;1;w=>VS)S0Ei48d?ShrKP1nvb?+;
zLT>zfqYNm`Q4-`A%)r*r(YfT=zi<Km9H5xCr;B5VM0m2mxfcyhX3lKi6;w3>bkjUi
r8=4K;)L7h;D~bd5^a?4QaAalJcv$A*?xu;&Ky3`3u6{1-oD!M<Cp|DG
index db4ee5b8ef24600cf389e1d62679ae554155d9e4..1903e73bcd4cfdd11f3e6e820f11d427a8b31c96
GIT binary patch
literal 589
zc%17D@N?(olHy`uVBq!ia0vp^(jd&i3?z4Pv7`g3w*fvOt_&~`8XD^0;9zW=24W;6
zgeD}!#id0hC&$O9MJ1%iBqc>Bq{SsCM<=AlCZ<KlXT&F`$0Vf3C#3<&SdfC$*yObM
zr1ZGNw7A5K*rc?$q;wz|o0t)ooF12y77rpbKqwKYD+8!FE*U5V)&kO%0pccs6az(o
z3gVL?8Z*Ee(}7%&%H(vg0+7mhh*UC&1WRRrl|i(?*iZ(P6s)<R2K1_5NswPK10xeN
z3o9Fkgq*yxs=BVBskybCtD8qqaA;h7dUkGcS$S1WYkPN3-?SMsXU$!(Xz`NetJZGZ
zzVrC4hmW7XeD(I-hfklseEs(0=da&?{{GWn^dtjl@<C4*$B+uf(o4syni51>9x`*j
z-1}Y5smebjM5jZe;NAbCmdJ(wKjhv|FH?RRmgRh2?AEUJtMsRs-JWHhc6jbOvAx2#
z<Qwii^R?=6oR@kjlWCXW=I%8UzVuwsb^B<jC@%7DW|Dn#Q|{zB`mQfCmYrK#lMy<x
zC2HoK@Jaeb%U67T;8(s?OX+%-P2$}21Mb}qJ(+5@O3EEQaNe?wUGT&0#2vSp|LGar
z;cBSw<}sgjKt6G;>5ualifp&5Kh*vE{q+8M3~T@R{Qjx^XgVlKc)I$ztaD0e0sw-}
B^d<lR
index f8bcc2573963872c4b3bf10bb9e94e330e4d23be..6294796e2a9d8bc3f0cbaa97d6a4c58954a7e3fc
GIT binary patch
literal 404
zc%17D@N?(olHy`uVBq!ia0vp^(jd&i3?z4Pv7`g3$pJngt_)xh8XD^9>FJP=;Om<f
z8ygGc`XwbrM@I)FB!q@0M@E7L6A~hlk|GilV`Ed|lhb39)8mqWWLjKuT6|J^d{Ran
zPy~#C+&CZ?gwo@|%D{|_IEZ+129!*Oa6x*3%HqKqfy#{4+m{2~WKa_17tFxO#?HaT
z&BMzlAS5CtAtkF>($>*8W9IS|Yd37!v3uYC!^h5CxP0x#?RyWNz54L=yJUXXN}%2b
zPZ!6Kid(fOZ;Lesh_GHzbywUOd?|39YtsM!*DDQ-q%z<Bw7QtENXz7fSa-);KmV5W
zRZm_ldGcY!lG&P;78;prCjE8sO#grS>EfJ9uete&N*rIEJoB56YUOOaf9u<a^t$Dy
z+8MsZQ3rdkJ@God|AN%~OvT4fKQbl78SKuwyM|xNQIBi2p7LK%;Cj0HxvX<aXaWG(
CCZ5ay
index 5bd9a95a161377502d0e64865b5e3e74a414a55f..8c724067b38882439fb556631e626a8fcdefb0c3
GIT binary patch
literal 771
zc%17D@N?(olHy`uVBq!ia0vp^(m*WB!3-pGf}dA0FfdvN_=LCuxp7Gu(TVBt$?0*4
z8F4@|3B-;~0;06&gfx(X#B^ihG>9xv+#w+WB$%8Lo0t-xlmXNZRPW&602GN!N&{;L
zGSVU9AO?s8DT_@`1?fsk4^2n_GUDS?ViQ0LVq;@N6BB`ofhZy=38W@DEh;$~s3j&T
zDJmfyXiGfU90UnNKz2+_8pvUZ88I=*V2jd2!5}^!XkA)-a$0O+ny+tKXeh)BAeR7T
zA$p;PKphPBa&!vVmW;T#xEP4<z-EHTWUxyT)BTc?fLcB=>D>ngL`F%FUob<fR>|D0
zC$=vv)@7W)ntmsPgK4(Y-3N9oOH2R!z0mOD@P-2~V;7i6d^QgHCnkFSm(;aB`OWI8
zkFMU^bLIWMFFS8ZmY1pQILOASJ)<R$i$CYxBIcFfCr$maY+ixP%-_P%=brz3dUAOr
z?+yDIUBIa0_jGX#k&s+_Fw>eTkb(7qaw|`7z#_q}mSg)=R26^k^SxRA{QrNZ@@dhN
z)9tF_|LhmL@-ymh<F;>o3F<mt;)$>MR!+Gx&)~vs_d}QD7e<#Tn0W{-TWB#m^RS45
zN`|zgtoO1T3<-0V{cwqDoWK{rq-HF#D0srM^bF=dB1(}SzjS0zWT`x_nea?pL(wHs
z@Y1AHRyx5Ejmf3GyeB5cv^$^A-?6}96G#4R74Hw*T#eZCD<?%s&fBK>=D=mW)+RoG
zQLgIW85SG|3ni8ZTv2@KDW|zX_VN@f%hs1~{xm&#DynnJRM&j!?IW>A7pQVX?>MzT
z%h2OH+g&5a9X2+0fks)WCe?K-_wCzTUsG56|G-CX9-o@DBRkEv0HcG!)78&qol`;+
E0PL$bLjV8(
index 4f06493f51ea070c605bdcd1d3562145ece661f5..eae6d51d5356c27dc4740865265c9dafe1dd5cbe
GIT binary patch
literal 133
zc%17D@N?(olHy`uVBq!ia0vp^>_9Bd!3-qZXQVO$DgFST5LX}_mzW-(oF12y5ucoK
zgw4|)D8y0{<QL2kQu4sKeqRlcujuLG7*fHQoY2UyDASE2;Q;Fl#fFT}GZYx67aIKb
bC{<^;_lJou<<zt{K*bE6u6{1-oD!M<yon?;
index ca09ae09adbd7242fd2489cdc31205b9f2bdfe42..260378bc96034300fa8a6ea372b8c594555113c4
GIT binary patch
literal 231
zc%17D@N?(olHy`uVBq!ia0vp^d?3uh3?wzC-F*zC%mREuT!Hk|sZ(dpoH>2^^tp5A
z&H;n<V6b7s1|Yd<)27XvH*eXpW$V_h5OV+i{pD5W>Oh@>B|(0{46<?xp5DH%UcY|x
z`OEk3KYq9^QrZWU^!Ide45_%~d-gP6gMxtbMXN3wnZ}?0=bL7FE!n;K<<YNV%;it!
zT~NNWLOp2#i?3_5rSDJ9O3!Ie;;Sz1SMh$giA(MI-tddx@3~0jetosa{@^pVV+@lw
RtOFX&;OXk;vd$@?2>?I?XD<K%
index 1d5e8b2eaef5ee9e57fced98eea606b062b4801d..ed47cbf7d0ce70af0523319774f39d0955a5d256
GIT binary patch
literal 130
zc%17D@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPHV5AX?b1=8i^<)x*iWo2bRhT4pS
zyg(tAk|4ie22ZcY|9y|l0P^KLT^vI=WRnHT7@3{fl-Udd(h3?3B3>M9h~_G0W6;)R
UpZspB9LNR+Pgg&ebxsLQ0M@c0r2qf`
index 9c8462ed15f499d8bb346e044ff907a7dcde1730..72c1583efd554039d017cb779f41fdb5d843b0ca
GIT binary patch
literal 456
zc%17D@N?(olHy`uVBq!ia0vp^LLkh+3?vf;>QaH!t^l79R|Xge4Gnd0a4<Gb^Yrv|
zNJs#3V`F3el9HmMqeDZ}0umD9;^HDB(?SyyBO_BHl9J-$GNO``qY~1i<1^xt(&7`-
z;}X+jlT%|8(}3K#B%mOWn--sx9+#W}Bw>t<xa9QsWC)oKR0fg)F+d0?9-j;oNd{{H
ztB-?n!P=o(GJG8meFVDOwj{_en1PXrnT3^|gOi(=k6%DoL{wZ#T3*%KCNd^AucE1|
zuYdNO`Ae6tShZo(wmtg~9y@;K?D;EKZ`{1|@ad~>-?gf41pp0d@N{tuk&ui%=qhx`
zfx-1)|8DEX1qbhP?)e?x%{q0K=Ppr+AM2cwlou#_*KpZ<G79p35Hoe+U54FFJ%{8X
zQd6e*AJ9=;T*qw3t#9!DmDiDQccq%Cd<TxdWUtcI+|H^edh=5DgQ&^6FB!kL<$UD)
qkaJ2*ntA;qr#6S^#B;Z=-se9cFMV~HYu7hWY<Rl*xvX<aXaWGfHK~pO
index fd0c94ac7e07b7adad7a976854d59fc5b269434c..c798967b70a2b84feb3ef471a0b3145f15617d30
GIT binary patch
literal 299
zc%17D@N?(olHy`uVBq!ia0vp^LLkh+3?vf;>QaGJa)3{WE0B(iOp8iRj!4LeO-_zW
zOpi@Yi%-gkO8}DTu^^NIWW*(A#3rT1ff0}kMadZ;1eAr4KwU{tZX8$^LVC6=&H`Ga
zP!i-9%pfAKpc4`r78RGAnwC>gS~YXdo<nEOUcCO`&G+Ab{{G{3uJs414fk|$45_%4
zd%|6)L4k+iKnH8JqoDQA|JtQ*)ZP_7RJnDnRloJd<j=bMD;|pfjj{MV`Euc=s*)Y|
zJ6hTr!<geH-8*A;ta17z--BxDlQ(ke$Ohy%y!bUsbk9Cs+2Fl3l0YjMJYD@<);T3K
F0RV>_a$f)d
index 20678853b8465345d5c76ab85bd3bd38d4934b5c..4d99edf357bcc537d922c459bd2f2bcd0e15240a
GIT binary patch
literal 551
zc%17D@N?(olHy`uVBq!ia0vp^LO?9S!3-p~d(N!@QuhLULR^8ggM&kKbV@{GVrXb+
zTvD2+r)O+ZT6}UwTvB>`a=Jr80+0kDAU7!^CLsl=CN2>ylAIBpkP1?nm=PD3;p>|g
z2LwP_uwJ0aKs9k7Eg-!>Eip-{@j#VH86YlD3Zx8bOKdFASfFWOy@?>H*u=D`1h7<6
zS`0*4B3M~sT6}y;ELbEaF*P<RJt`>)WM+I?Y$C`IpkAQ)F$oz!hyAp!DhIm5vn0qb
zn1Mw`maU#W`_P{w95r_LAK6xg|N3kCW3CyOu;YS%jOX8Y%=8Z~T(my_c3|#T&5yd*
zVy5M(?kb4>S*aLroUqTzL}{<G(~Zwp9VQ;RG9PH@dQTU}5DCfBgW2Xxff8*G)q`fa
zcxf8;E}ZP_%3h?l>i7TuiuZD}*ZzF4>&yNt$I_zCP4iLPJ-0b#!dY+Y7w(DT`RjM^
zPx4B=#>cQSh?Ot<>*8Y{Io%S47X@1yx}_i3IY+GT)`f|epH8nTP%Q3TuDxTLXS&VT
z#05(f?`XUfYYO0GO;E|#NoTs6sZm^RKe@7_;lan1pZ+jyNr_Xww#eM_y?n*wo&Rc%
g#drTRiaS}uoOxHPJY9IsX9iG!xca%Qb4q9e08-uC$N&HU
index e24adb4995993aa353b02b52322471f1aec57c98..82a81adbb0f59f024cac494724c8fd29f186a8fc
GIT binary patch
literal 192
zc%17D@N?(olHy`uVBq!ia0vp^oFL4>3?#4ne^UZd>H$6>u0VSB?Afzs&04Tv!TkC2
zSFBjE91OOC!S?OjckI})fB*i!0vp7DN_k3x{DK*5?Mq6JpE&vB=kMP?dL)(s#ce!Y
z978H@$)1iBVld=j3RIab^uGR{<frc&qSmN1oaoJAb&6dO<y_18zK-G8`ph5CuI&g;
h+qJO1+%j!0f7n&V&pp$5+=1pWc)I$ztaD0e0s!g9O^N^j
index 32723effcb8917dc78c47edfd6003c2f97bf1310..cfdf745588a50fe71d40d3ce4bb25f420e2f5344
GIT binary patch
literal 170
zc%17D@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfvi2$DvS0G)61j@_HA>`6J!?{2?
z_L3mKU<R3G%eOsyEBRd89w=nx>EaloF)=wIMAx0kk%=eisfNPDgLft}a(0Lqws0$O
z8%#5rV!WW?Q()sk#RkDZiNH?5n?1``oMdFk?>BL*H&wm>G=;&_)z4*}Q$iB}YhW{{
index a21969df84f1f6e1719cdbbe568b560fdc21221d..6e94d969c638d3c8f816d80852dea08d0738012b
GIT binary patch
literal 754
zc%17D@N?(olHy`uVBq!ia0vp^DnP8t!3-p2n|AXvFfdvK_=LDJfI(<zsDpz8ko5HQ
zbVx|>^-YV7jg5|u4oFCdi;DxYV`9?c;?km$lOqx`Vq%ix;!<OhlH%imWO{63T6986
zTvB>WLPm6QdR$U!Oaf3aH6}SNJ}EUW5h#)on~)A-B&NqEr$WehpvI((xWtUur1bd2
zv^bDPph#Lghyme3+40F~v0z!SQR#8X8Sx+z#Eu7=0Fg>g2P@713Bni<6c1DnQv*_l
zU}Qi@h<YdkYB)qYh#j8{GZV^ylE2nnybBDt^pYUIU<O8J7B+ScE^a;nVKGTLc?A_U
zb!~lf3rh!Q7gu+0KmWj>kno6@*tpd6jLfXuf|AnmikjM%wyy4})21(8y=LvY4V!lC
z+_n4Q;bUjcU$}Df)}4os9zT8Y^3~gS?>~R}_Wj4tU%&tS{m1C@uN4?=Or9=|Arg|U
z2h+_DCrGe8u<i}*lG-};R&~GB+P&QYSuPr?-~az-pJVs)(Hxunb=-<NF0*tiqYY=|
zemcL8mpOZO=;{kMExT+&wzl)6EXcH2{9|VGvX7G<W%JBBV%loX;d@&5sJFSv+wUU%
zb1y9M67dt7B{zN1gob737hCi@8^<iOSk;`i$mwAZPpijv#u?u;pRG@5n;2tbEY;QX
zWbyyzKrV;((#%IM8yvUxWQbia@}Bv^(s>%EGGAE9Y1kQG*m^H8<k-UOeG7lgYg+fR
z>(Kl*y*kH-`VQgshYkppn6mS%aZG<Maiihft<(e|w#Y6UpKFm@c^13Qe=?7EbKvo_
c$3C3bf6%w>^O|Q9bU{hi)78&qol`;+0Nq14Bme*a
index e8b901553c45fed0c489eae3a037289dd7f78d2c..28d390d1c3d3bb3efc901630da4c24939b79fb17
GIT binary patch
literal 475
zc%17D@N?(olHy`uVBq!ia0vp^Dj>|k3?#4J%UA`Z)&=;4xB}^zn6#Lf<fw%7n8dXB
z_>{P~jOc`vxTN%$gp`<sjJTxK7@!moC8xzDW`M|~jF_a<xWx3Bq>R|4jJRYVJ1ssD
zD4re*B$LyD3^17%p9~ZMGBRR8y3*s5fu?~GNH!xL%m%87Pl9m48o>;(N+1a|CnFBQ
z&A7KSX%EoFmL)-c!3>frsz%0U7M8XSF0LNlJ^_Kj;n4|6DVe!>`9<XwHFXVrr_Wrz
z@$BWRH*ep6{PgA9_a8rh{r>a!-?qi6Ux0=z_H=O!sbK6qb5p31L89%UztZo0A!0XO
z#W??duTQozbQb>qUC#cq(%jwOWi=`%nf%Y!5sba*q@kP=^7_~cwXdt<B}4ZbtSU+i
z`FUi;rHO$@8v>s_^nQBADXjU0vu|>sSBOHEo=bjvgnRUwP+`*qooTDjEZUy=INkV=
z4~MZ@kF&&VgToQ#wTHgU+x60}<nqqfYVxnx%n}m>**<VE_x$nYT^VOBk<d{i`&j_!
OTn0~9KbLh*2~7amqrg}I
index 832ea534548c4c9ce26fc6d9066fb3b2b800a0f6..a788766c8b1efeca837c8156fdb1bea7f7288a97
GIT binary patch
literal 135
zc%17D@N?(olHy`uVBq!ia0vp^oItF?!3-pGY9E{eQv3lvA+A6=HYqJGDI-2PJw7?Z
ztxlpHD8y0{<QL3P(C{HKCS@*=uk7jK7*fHQoY2UqwNWCmp)ukB3v);jqcM-c1RjBP
d%gc5!FjS;7*NCv5C<iKM@O1TaS?83{1OOz8B7Fb=
index cb997070af278fbd6cfd1b207227bac8d46f59c8..b9d65312f6c7aaa58e081d9e7db0689d15cb625b
GIT binary patch
literal 240
zc%17D@N?(olHy`uVBq!ia0vp^LLkh+3?vf;>QaG}aez;VE07Ki4V^i2=F+80m%+e_
z6)Tp5!OE2@SFKvL5)5|i*s*ix&i(uM@AA>m0csN{3GxeOU}oLAeaEgN$4;I(cmBek
zKbPJ-Yy-;0c)B=-R4~S#v*bH$AaFFmE$&gp#D9PNXQw%Er2W+TWcj;G_ixr5&pxJo
zo_STLyo@4(!zRi0ZJH#X!8L1<<}9QCKNe-2%K7(WdBe6n^UbR|&31TgX)o)Sy?yNR
ZKc!7vdl;rmw*u{9@O1TaS?83{1ORZ4Xy5<<
index 567b5b62a40efa3e59d5f6630525715a48f5e004..70a1443b7b9eff4f12680823b75b70e04bef319d
GIT binary patch
literal 209
zc%17D@N?(olHy`uVBq!ia0vp^1|ZDB3?!H8JlO)I<N|y`Tp7TiprD|vtgIXZoO)yJ
z4wU9B3GxeOV3bTN+k4{k&%DP%#X!MGPZ!4!i_^&o0)F#58Bg&@O3YA7Pht!fm2gQ<
zVr}G^W;{p2puy{TO4}Jto`{C$CT*uYZ>X3oH$F3`!MCaS*@`1T$Wp6Pkmfh%rer!}
i!{!qQB2PTfn#K_Ie`fm0wap12mwCGSxvX<aXaWG5Tu8P6
index d27bc18f0fe7c3148c0a3bf4d8a753a2c0b0a8d1..7941c7dbc4204de4146d470813da9ecb4b665c3d
GIT binary patch
literal 1006
zc%17D@N?(olHy`uVBq!ia0vp^W<YGt!3-n|j(d7BFfjH9_=LDJz(8ndsDp!pv2hxZ
z;pyoKBz=9;Vq;^048NqL=;-Ls(6oSrgs7;fxVX5;$h64Fl+c8Ph@_;rxQyuNl!(N{
zsO03>*p&GAw3wLW=*0Bc<mBjtwD|ax*rfEB#MGFCl-R`7xTMs$#EjSkpln(^NDYvi
z7Mqk2mz)|8Vx)m&6M<4dG9wPiO-hSTPKPjJLF&O8lhWf7fr9CAU^WB+HG;_G44`#!
zAOnHgf#P5i%#H(@15pEJfJv}yI$RA@Fd3l{A`3DtIUQm?L;*}BBMznoY8{ebJXl#g
z!dR%K@yOOCXCT?0;ePwR6)-YdN`m}?85o(ESy<WFIk~ubc=`AR1cgP!C8VTfWaSl=
zRkU<;jZLg<-93GRLL+126OuBsbMgwyDynK~>zmv9`X@}9JZ0vbx$_q+T(orA@|CMr
zuid<5>$dGXcJ4oT=<t!FCr_O|bN<4`D_5`GxOw}|-FpunK6(1=`Rg}t-@X6v@$=Vj
z-+%o4_507?e}387jKHMQ<>}%WA|W~V;LmuTLK(IX>1n)^yY60_sjd1w(CKNMYDnKz
zRTowF_8gI|iK+AKW7=n(=Z~x0_qi_FSW~5-rebA*YQy5RbuYA=CuheP?~t|n&v*T)
z0{5;R3uiQGoH72U+^{UE;TUs<(fW*@Wr{a#-5&q+^H`;x@x<AKuXa(PeZ!Jx#=W!U
zR7yXaL@ihupQ5vi;p|DF<fXwE=KMYzu~BYTw_tMd)(cxMXvj1k+3lCSM@nda?=cUt
zNegBld%)*$Xt~L}mI)zti@)yuA%DN^9Mg&_-}K%SdzS9z-*KEp>5LJl$PAWQGY`o8
zG28W;VUKjlmI>O&H-$yb-Mwk{=?Cl8qAd9reHQH9ZRfIldPYfJ<BKEyQvFOFeY%$_
z_V6y+RVh`g=;FTr!VCTO7aw?f#it6$&RJwQr7h!rwbbEIfmHX*Jtv)})OkL>E7zg*
zE7RvqyT+NzKPng&Jy7g@xmRH6t;{D2Kjj&Q-Qv0#niuh5;qD`)+aE4ce6*_3$inh-
n_vtEUi&ZawT2)#eu>Z@L@JwL2ET7LuP@eU4^>bP0l+XkKtwF3d
index bfd523ae7bfbb5a6526376ea7b830998151f3436..54beae24db81daf5c4760d575b883092e485919e
GIT binary patch
literal 661
zc%17D@N?(olHy`uVBq!ia0vp^W+2SL3?z5Yp7oxAfsrY|C&ZNj3>+LBjE&PmL(}5o
z;vyr{Vq;??BU3^X65`@AqN7tH5)&g5GNO`_q7u?06Vl@2Q{&=N;*yi&<5Oaj(xVen
zqT@5-lG0)l(&Lg+V?ks_Y;szBVtQ<HDv*p%N{>xSi%U+APXsDUj|VY8YT}ZCC>=<~
zCxOM2z}%#Ckjf;m0uTvQp8@596~h={gv16J3pF7FY$ikmY(7{p9%>rI5RkDD^32m8
z2Y^8kS`y?J%)rRP$}cP;CN3c@BdegKs-dlCXl!C(?da<18yFfHo0yiJUszO9*}h=$
z(&a1HtX;QZ%Z{C=&s@5E<=Ty#ckVrS`1sk2SFhi^d;j6n=da(s|NQm)&)<LQ;wn`@
z3*LIVIEGX(&b@XttSvyq;o;}iE5Z*5Y!KbO+xdvY5kA}Z|Fs?+=+qIrxA*z|8t>S&
zwI{-N9W}DQy6MXH;JlR$w}V;N1y8l`pJ|<>SCf+WqjBfL>Sv3rpEb?dd9Y_^qx7?1
z>u0{+owV-zB+LIzmime7p3ku?J{5Dl@aWb@cbg9{6Z-r5+0|Y0(=`g*rffMCaqPxg
zkrv(4Yyr1(AHBJ;x=iym$Dw80R=twfRovQfMJ8yo$JrV|)83Fjo>#802Tu0z-6L%J
z&+W<$?ZCzQF1<FwqPBvn>K>IIoy}8D*abaWw!h2g{j#M(hOcklbn?#n#g^IP?)Y9s
R*#MNfJYD@<);T3K0RWXP9iadK
index c50c9cfeca8d4b1ef3f0001729e332db822905cb..cf1dba4538933326921ec8a7d526b1fc5facad09
GIT binary patch
literal 137
zc%17D@N?(olHy`uVBq!ia0vp^{6K8R!3-qzYmH?=0s%fDu0T32IXx~ZBR)AjJ~=~v
zO<gBYh@~XRFPLG)fftOIue$;Hs-7;6Ar*|t368AJedjy4Bwp~&2sk)_+deJifWZL<
fTf6qEUn~sssyOC}>gBQnH86O(`njxgN@xNAU56xP
index 8e8f299f092c23dba9bd21551f7a127fa6da9210..cc35204c3bc8a18becf400373ea7a655113a5973
GIT binary patch
literal 334
zc%17D@N?(olHy`uVBq!ia0vp^(jd&i3?z4Pv7`g3kN}?$S0LTp-92~i+>VY8AUS*X
z?0NI%&73(C$eurc{=$U|SHZyQ)vMQm!G;YRHX?x?V6b!N&fU9r@7c3w|Ni~AB7_%%
zEGr4}3ufTv<#%*)ar3Ngx_0B{trxFefB5+6^Y1@@{{7>!;$96@*5~Qs7*fF)dco7F
z!GOW}Vno|12Bqc;&;EVC!n^f<{(Y^9cFRgilHSgH_Q5@V+k1{zbvgNtlm9B6h?^2K
z;mMlAN1XOuxy$P5Kjo2qlbxzUr$s{t8>6FxfZ&m~B;|*9kG6Zb2u*Zhv3=}&q+O6h
zXyaPW8`<kQE3~e%oSBu)w2muU@lm;Qt7?4U8vD<;UWorR(agBAC2lv+WelFKelF{r
G5}E)$t(tuR
--- a/mobile/android/base/tabs/TabStrip.java
+++ b/mobile/android/base/tabs/TabStrip.java
@@ -98,16 +98,19 @@ public class TabStrip extends ThemedLine
         addTabButton.setImageLevel(isPrivate ? IMAGE_LEVEL_PRIVATE : IMAGE_LEVEL_NORMAL);
     }
 
     private class TabsListener implements Tabs.OnTabsChangedListener {
         @Override
         public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
             switch (msg) {
                 case RESTORED:
+                    tabStripView.restoreTabs();
+                    break;
+
                 case ADDED:
                     tabStripView.addTab(tab);
                     break;
 
                 case CLOSED:
                     tabStripView.removeTab(tab);
                     break;
 
--- a/mobile/android/base/tabs/TabStripView.java
+++ b/mobile/android/base/tabs/TabStripView.java
@@ -39,16 +39,18 @@ public class TabStripView extends TwoWay
     private static final DecelerateInterpolator ANIM_INTERPOLATOR =
             new DecelerateInterpolator();
 
     private final TabStripAdapter adapter;
     private final Drawable divider;
 
     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();
 
     private boolean isPrivate;
 
     private final Paint fadingEdgePaint;
     private final int fadingEdgeSize;
@@ -200,17 +202,56 @@ public class TabStripView extends TwoWay
 
                 animatorSet.start();
 
                 return true;
             }
         });
     }
 
+    private void animateRestoredTabs() {
+        getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
+            @Override
+            public boolean onPreDraw() {
+                getViewTreeObserver().removeOnPreDrawListener(this);
+
+                final List<Animator> childAnimators = new ArrayList<Animator>();
+
+                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() {
                 getViewTreeObserver().removeOnPreDrawListener(this);
                 smoothScrollToPosition(position);
                 return true;
             }
         });
@@ -239,16 +280,23 @@ public class TabStripView extends TwoWay
         adapter.refresh(tabs);
         updateSelectedPosition(true);
     }
 
     void clearTabs() {
         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.
         refreshTabs();
         animateNewTab(tab);
     }
 
     void removeTab(Tab tab) {
--- a/mobile/android/base/tabs/TabsGridLayout.java
+++ b/mobile/android/base/tabs/TabsGridLayout.java
@@ -1,52 +1,68 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabs;
 
 import java.util.ArrayList;
+import java.util.List;
 
 import org.mozilla.gecko.animation.ViewHelper;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.tabs.TabsPanel.TabsLayout;
 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.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Button;
 import android.widget.GridView;
-import android.view.ViewGroup;
-import android.widget.Button;
+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).
  * Expected to replace TabsListLayout once complete.
  */
 
 class TabsGridLayout extends GridView
                      implements TabsLayout,
                                 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<PointF> mTabLocations = new SparseArray<PointF>();
 
     final private boolean mIsPrivate;
 
     private final TabsLayoutAdapter mTabsAdapter;
+    private final int mColumnWidth;
 
     public TabsGridLayout(Context context, AttributeSet attrs) {
         super(context, attrs, R.attr.tabGridLayoutViewStyle);
         mContext = context;
 
         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabsLayout);
         mIsPrivate = (a.getInt(R.styleable.TabsLayout_tabs, 0x0) == 1);
         a.recycle();
@@ -62,19 +78,23 @@ class TabsGridLayout extends GridView
             }
         });
 
         setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
         setStretchMode(GridView.STRETCH_SPACING);
         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);
         setPadding(padding, paddingTop, padding, padding);
     }
 
     private class TabsGridLayoutAdapter extends TabsLayoutAdapter {
 
@@ -82,19 +102,17 @@ class TabsGridLayout extends GridView
         final private View.OnClickListener mSelectClickListener;
 
         public TabsGridLayoutAdapter (Context context) {
             super(context, R.layout.new_tablet_tabs_item_cell);
 
             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);
                 }
             };
 
             mSelectClickListener = new View.OnClickListener() {
                 @Override
                 public void onClick(View v) {
                     TabsLayoutItemView tab = (TabsLayoutItemView) v;
                     Tabs.getInstance().selectTab(tab.getTabId());
@@ -116,16 +134,57 @@ class TabsGridLayout extends GridView
             super.bindView(view, tab);
 
             // If we're recycling this view, there's a chance it was transformed during
             // the close animation. Remove any of those properties.
             resetTransforms(view);
         }
     }
 
+    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;
     }
 
     @Override
     public void show() {
         setVisibility(View.VISIBLE);
@@ -155,16 +214,19 @@ class TabsGridLayout extends GridView
     public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
         switch (msg) {
             case ADDED:
                 // Refresh the list to make sure the new tab is added in the right position.
                 refreshTabsData();
                 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());
                        updateSelectedStyle(selected);
                    }
                }
                break;
 
@@ -239,9 +301,95 @@ class TabsGridLayout extends GridView
         for (Tab tab : tabs) {
             // In the normal panel we want to close all tabs (both private and normal),
             // but in the private panel we only want to close private tabs.
             if (!mIsPrivate || tab.isPrivate()) {
                 Tabs.getInstance().closeTab(tab, false);
             }
         }
     }
+
+    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<Animator> 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;
+            }
+        });
+    }
+
 }
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -137,21 +137,16 @@ SessionStore.prototype = {
             observe: function (aSubject, aTopic, aData) {
               Services.obs.removeObserver(restoreCleanup, "sessionstore-windows-restored");
 
               if (window.BrowserApp.tabs.length == 0) {
                 window.BrowserApp.addTab("about:home", {
                   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);
 
           // Do a restore, triggered by Java
           let data = JSON.parse(aData);
           this.restoreLastSession(data.sessionString);
         } else {
--- a/netwerk/protocol/http/nsHttpHandler.cpp
+++ b/netwerk/protocol/http/nsHttpHandler.cpp
@@ -1514,16 +1514,35 @@ nsHttpHandler::PrefsChanged(nsIPrefBranc
 void
 nsHttpHandler::TimerCallback(nsITimer * aTimer, void * aClosure)
 {
     nsRefPtr<nsHttpHandler> thisObject = static_cast<nsHttpHandler*>(aClosure);
     if (!thisObject->mPipeliningEnabled)
         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
  *  header. Previous q values will be stripped because the order of
  *  the langs imply the q value. The q values are calculated by dividing
  *  1.0 amongst the number of languages present.
  *
  *  Ex: passing: "en, ja"
@@ -1569,16 +1588,18 @@ PrepareAcceptLanguages(const char *i_Acc
     {
         token = net_FindCharNotInSet(token, HTTP_LWS);
         char* trim;
         trim = net_FindCharInSet(token, ";" HTTP_LWS);
         if (trim != (char*)0)  // remove "; q=..." if present
             *trim = '\0';
 
         if (*token != '\0') {
+            NormalizeLanguageTag(token);
+
             comma = count_n++ != 0 ? "," : ""; // delimiter if not first item
             uint32_t u = QVAL_TO_UINT(q);
 
             // Only display q-value if less than 1.00.
             if (u < 100) {
                 const char *qval_str;
 
                 // With a small number of languages, one decimal place is enough to prevent duplicate q-values.
new file mode 100644
--- /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;
+}
--- a/netwerk/test/unit/xpcshell.ini
+++ b/netwerk/test/unit/xpcshell.ini
@@ -193,16 +193,17 @@ skip-if = bits != 32
 [test_file_partial_inputstream.js]
 [test_file_protocol.js]
 [test_filestreams.js]
 [test_freshconnection.js]
 [test_gre_resources.js]
 [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]
 [test_httpcancel.js]
 [test_httpResponseTimeout.js]
 [test_httpsuspend.js]
 [test_idnservice.js]
 [test_idn_urls.js]
--- a/services/mobileid/MobileIdentityVerificationFlow.jsm
+++ b/services/mobileid/MobileIdentityVerificationFlow.jsm
@@ -32,78 +32,77 @@ MobileIdentityVerificationFlow.prototype
     return this.register()
     .then(
       (registerResult) => {
         log.debug("Register result ${}", registerResult);
         if (!registerResult || !registerResult.msisdnSessionToken) {
           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();
+    // 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);
           }
-          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 {
+          // 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);
         }
-      },
-      (reason) => {
-        this.verificationCodeDeferred.reject(reason);
-      }
-    );
+      );
+    } else {
+      this.ui.verify();
+    }
+
     return this.verificationCodeDeferred.promise.then(
       this.onVerificationCode.bind(this)
     );
   },
 
   // When we receive a verification code from the UI, we check it against
   // the server. If the verification code is incorrect, we decrease the
   // number of retries left and allow the user to try again. If there is no
@@ -140,18 +139,21 @@ MobileIdentityVerificationFlow.prototype
         return this.verificationOptions;
       },
       (error) => {
         log.error("Verification code error " + error);
         this.retries--;
         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();
         }
         return this._doVerification();
       }
     );
   },
--- a/services/mobileid/tests/xpcshell/head.js
+++ b/services/mobileid/tests/xpcshell/head.js
@@ -1,19 +1,462 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 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");
 
 (function initMobileIdTestingInfrastructure() {
   do_get_profile();
 
   const PREF_FORCE_HTTPS = "services.mobileid.forcehttps";
   Services.prefs.setBoolPref(PREF_FORCE_HTTPS, false);
   Services.prefs.setCharPref("services.mobileid.loglevel", "Debug");
   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);
+}
--- a/services/mobileid/tests/xpcshell/test_mobileid_manager.js
+++ b/services/mobileid/tests/xpcshell/test_mobileid_manager.js
@@ -1,442 +1,33 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "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;
   MobileIdentityManager.ui = null;
   MobileIdentityManager._iccInfo = [];
   removePermission(ORIGIN);
 }
 
 // Unregister mocks and restore original code.
 do_register_cleanup(cleanup);
-
 // === Tests ===
 function run_test() {
   run_next_test();
 }
 
 add_test(function() {
   do_print("= Initial state =");
   do_check_neq(MobileIdentityManager, undefined);
new file mode 100644
--- /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();
+};
--- 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]