author | Phil 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 id | 27897 |
push user | philringnalda@gmail.com |
push date | Fri, 28 Nov 2014 06:30:27 +0000 |
treeherder | autoland@1162e4a4d7a2 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 36.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
|
--- 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(); +};