From 28936c75ec6ac356afa5a064b670c5bef2040f44 Mon Sep 17 00:00:00 2001 From: Mark Hammond Date: Mon, 28 Sep 2015 17:21:42 +1000 Subject: [PATCH 01/23] Bug 1205111 - return a transient error on 401 fetching info/collections using FxA. r=rnewman --- services/common/tokenserverclient.js | 5 +++++ services/sync/modules-testing/utils.js | 13 +++++++++++++ services/sync/modules/browserid_identity.js | 17 +++++++++++++++-- services/sync/modules/identity.js | 9 +++++++++ services/sync/modules/service.js | 11 ++++++++--- services/sync/tests/unit/test_errorhandler.js | 5 ++++- services/sync/tests/unit/test_syncscheduler.js | 17 ++++++++++++++--- 7 files changed, 68 insertions(+), 9 deletions(-) diff --git a/services/common/tokenserverclient.js b/services/common/tokenserverclient.js index 419b254acd9..298384b95cc 100644 --- a/services/common/tokenserverclient.js +++ b/services/common/tokenserverclient.js @@ -32,6 +32,9 @@ const PREF_LOG_LEVEL = "services.common.log.logger.tokenserverclient"; this.TokenServerClientError = function TokenServerClientError(message) { this.name = "TokenServerClientError"; this.message = message || "Client error."; + // Without explicitly setting .stack, all stacks from these errors will point + // to the "new Error()" call a few lines down, which isn't helpful. + this.stack = Error().stack; } TokenServerClientError.prototype = new Error(); TokenServerClientError.prototype.constructor = TokenServerClientError; @@ -52,6 +55,7 @@ this.TokenServerClientNetworkError = function TokenServerClientNetworkError(error) { this.name = "TokenServerClientNetworkError"; this.error = error; + this.stack = Error().stack; } TokenServerClientNetworkError.prototype = new TokenServerClientError(); TokenServerClientNetworkError.prototype.constructor = @@ -96,6 +100,7 @@ this.TokenServerClientServerError = this.name = "TokenServerClientServerError"; this.message = message || "Server error."; this.cause = cause; + this.stack = Error().stack; } TokenServerClientServerError.prototype = new TokenServerClientError(); TokenServerClientServerError.prototype.constructor = diff --git a/services/sync/modules-testing/utils.js b/services/sync/modules-testing/utils.js index 45515e3fc04..66a4334f1c5 100644 --- a/services/sync/modules-testing/utils.js +++ b/services/sync/modules-testing/utils.js @@ -7,6 +7,7 @@ this.EXPORTED_SYMBOLS = [ "btoa", // It comes from a module import. "encryptPayload", + "isConfiguredWithLegacyIdentity", "ensureLegacyIdentityManager", "setBasicCredentials", "makeIdentityConfig", @@ -94,6 +95,18 @@ this.waitForZeroTimer = function waitForZeroTimer(callback) { CommonUtils.namedTimer(wait, 150, {}, "timer"); } +/** + * Return true if Sync is configured with the "legacy" identity provider. + */ +this.isConfiguredWithLegacyIdentity = function() { + let ns = {}; + Cu.import("resource://services-sync/service.js", ns); + + // We can't use instanceof as BrowserIDManager (the "other" identity) inherits + // from IdentityManager so that would return true - so check the prototype. + return Object.getPrototypeOf(ns.Service.identity) === IdentityManager.prototype; +} + /** * Ensure Sync is configured with the "legacy" identity provider. */ diff --git a/services/sync/modules/browserid_identity.js b/services/sync/modules/browserid_identity.js index 8751e42907a..5df84251a9b 100644 --- a/services/sync/modules/browserid_identity.js +++ b/services/sync/modules/browserid_identity.js @@ -692,6 +692,10 @@ this.BrowserIDManager.prototype = { _getAuthenticationHeader: function(httpObject, method) { let cb = Async.makeSpinningCallback(); this._ensureValidToken().then(cb, cb); + // Note that in failure states we return null, causing the request to be + // made without authorization headers, thereby presumably causing a 401, + // which causes Sync to log out. If we throw, this may not happen as + // expected. try { cb.wait(); } catch (ex if !Async.isShutdownException(ex)) { @@ -730,8 +734,17 @@ this.BrowserIDManager.prototype = { createClusterManager: function(service) { return new BrowserIDClusterManager(service); - } + }, + // Tell Sync what the login status should be if it saw a 401 fetching + // info/collections as part of login verification (typically immediately + // after login.) + // In our case, it almost certainly means a transient error fetching a token + // (and hitting this will cause us to logout, which will correctly handle an + // authoritative login issue.) + loginStatusFromVerification404() { + return LOGIN_FAILED_NETWORK_ERROR; + }, }; /* An implementation of the ClusterManager for this identity @@ -777,7 +790,7 @@ BrowserIDClusterManager.prototype = { // it's likely a 401 was received using the existing token - in which // case we just discard the existing token and fetch a new one. if (this.service.clusterURL) { - log.debug("_findCluster found existing clusterURL, so discarding the current token"); + log.debug("_findCluster has a pre-existing clusterURL, so discarding the current token"); this.identity._token = null; } return this.identity._ensureValidToken(); diff --git a/services/sync/modules/identity.js b/services/sync/modules/identity.js index 5d7bf24f937..7444bc2b7df 100644 --- a/services/sync/modules/identity.js +++ b/services/sync/modules/identity.js @@ -590,4 +590,13 @@ IdentityManager.prototype = { // Do nothing for Sync 1.1. return {accepted: true}; }, + + // Tell Sync what the login status should be if it saw a 401 fetching + // info/collections as part of login verification (typically immediately + // after login.) + // In our case it means an authoritative "password is incorrect". + loginStatusFromVerification404() { + return LOGIN_FAILED_LOGIN_REJECTED; + } + }; diff --git a/services/sync/modules/service.js b/services/sync/modules/service.js index 82317298c6c..43b1d979c9f 100644 --- a/services/sync/modules/service.js +++ b/services/sync/modules/service.js @@ -765,8 +765,12 @@ Sync11Service.prototype = { return this.verifyLogin(false); } - // We must have the right cluster, but the server doesn't expect us - this.status.login = LOGIN_FAILED_LOGIN_REJECTED; + // We must have the right cluster, but the server doesn't expect us. + // The implications of this depend on the identity being used - for + // the legacy identity, it's an authoritatively "incorrect password", + // (ie, LOGIN_FAILED_LOGIN_REJECTED) but for FxA it probably means + // "transient error fetching auth token". + this.status.login = this.identity.loginStatusFromVerification404(); return false; default: @@ -990,6 +994,7 @@ Sync11Service.prototype = { } // Ask the identity manager to explicitly login now. + this._log.info("Logging in the user."); let cb = Async.makeSpinningCallback(); this.identity.ensureLoggedIn().then( () => cb(null), @@ -1005,9 +1010,9 @@ Sync11Service.prototype = { && (username || password || passphrase)) { Svc.Obs.notify("weave:service:setup-complete"); } - this._log.info("Logging in the user."); this._updateCachedURLs(); + this._log.info("User logged in successfully - verifying login."); if (!this.verifyLogin()) { // verifyLogin sets the failure states here. throw "Login failed: " + this.status.login; diff --git a/services/sync/tests/unit/test_errorhandler.js b/services/sync/tests/unit/test_errorhandler.js index 610270e2c67..9b97a69b2f3 100644 --- a/services/sync/tests/unit/test_errorhandler.js +++ b/services/sync/tests/unit/test_errorhandler.js @@ -184,7 +184,10 @@ add_identity_test(this, function test_401_logout() { let errorCount = sumHistogram("WEAVE_STORAGE_AUTH_ERRORS", { key: "info/collections" }); do_check_eq(errorCount, 2); - do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED); + let expected = isConfiguredWithLegacyIdentity() ? + LOGIN_FAILED_LOGIN_REJECTED : LOGIN_FAILED_NETWORK_ERROR; + + do_check_eq(Status.login, expected); do_check_false(Service.isLoggedIn); // Clean up. diff --git a/services/sync/tests/unit/test_syncscheduler.js b/services/sync/tests/unit/test_syncscheduler.js index 98e4066f4af..f9beddb2cc9 100644 --- a/services/sync/tests/unit/test_syncscheduler.js +++ b/services/sync/tests/unit/test_syncscheduler.js @@ -956,11 +956,22 @@ add_identity_test(this, function test_loginError_fatal_clearsTriggers() { Svc.Obs.add("weave:service:login:error", function onLoginError() { Svc.Obs.remove("weave:service:login:error", onLoginError); Utils.nextTick(function aLittleBitAfterLoginError() { - do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED); - do_check_eq(scheduler.nextSync, 0); - do_check_eq(scheduler.syncTimer, null); + if (isConfiguredWithLegacyIdentity()) { + // for the "legacy" identity, a 401 on info/collections means the + // password is wrong, so we enter a "login rejected" state. + do_check_eq(Status.login, LOGIN_FAILED_LOGIN_REJECTED); + do_check_eq(scheduler.nextSync, 0); + do_check_eq(scheduler.syncTimer, null); + } else { + // For the FxA identity, a 401 on info/collections means a transient + // error, probably due to an inability to fetch a token. + do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR); + // syncs should still be scheduled. + do_check_true(scheduler.nextSync > Date.now()); + do_check_true(scheduler.syncTimer.delay > 0); + } cleanUpAndGo(server).then(deferred.resolve); }); }); From 55f6a4dd02e7855f681850ecb289df3c76e426e3 Mon Sep 17 00:00:00 2001 From: Jared Wein Date: Thu, 24 Sep 2015 14:18:00 +0200 Subject: [PATCH 02/23] Bug 1208000 - Default browser dialog appears only 2 times. r=Gijs --- browser/components/shell/nsGNOMEShellService.cpp | 2 +- browser/components/shell/nsMacShellService.cpp | 2 +- browser/components/shell/nsWindowsShellService.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/browser/components/shell/nsGNOMEShellService.cpp b/browser/components/shell/nsGNOMEShellService.cpp index f2f125f7669..61230ba254a 100644 --- a/browser/components/shell/nsGNOMEShellService.cpp +++ b/browser/components/shell/nsGNOMEShellService.cpp @@ -352,7 +352,7 @@ nsGNOMEShellService::GetShouldSkipCheckDefaultBrowser(bool* aResult) if (NS_FAILED(rv)) { return rv; } - if (defaultBrowserCheckCount < 3) { + if (defaultBrowserCheckCount < 4) { *aResult = false; return prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, defaultBrowserCheckCount + 1); diff --git a/browser/components/shell/nsMacShellService.cpp b/browser/components/shell/nsMacShellService.cpp index 6308440cf70..e40cf591024 100644 --- a/browser/components/shell/nsMacShellService.cpp +++ b/browser/components/shell/nsMacShellService.cpp @@ -130,7 +130,7 @@ nsMacShellService::GetShouldSkipCheckDefaultBrowser(bool* aResult) if (NS_FAILED(rv)) { return rv; } - if (defaultBrowserCheckCount < 3) { + if (defaultBrowserCheckCount < 4) { *aResult = false; return prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, defaultBrowserCheckCount + 1); diff --git a/browser/components/shell/nsWindowsShellService.cpp b/browser/components/shell/nsWindowsShellService.cpp index 6f81e79c393..88c265ed548 100644 --- a/browser/components/shell/nsWindowsShellService.cpp +++ b/browser/components/shell/nsWindowsShellService.cpp @@ -1011,7 +1011,7 @@ nsWindowsShellService::GetShouldSkipCheckDefaultBrowser(bool* aResult) if (NS_FAILED(rv)) { return rv; } - if (defaultBrowserCheckCount < 3) { + if (defaultBrowserCheckCount < 4) { *aResult = false; return prefs->SetIntPref(PREF_DEFAULTBROWSERCHECKCOUNT, defaultBrowserCheckCount + 1); From c6e114bfb7fe6a5b98e3354b50f962d4eeb00bc7 Mon Sep 17 00:00:00 2001 From: Christian Schmitz Date: Thu, 17 Sep 2015 21:53:14 +0200 Subject: [PATCH 03/23] Bug 1085406 - Update new tablet reload assets. r=mcomella --- .../ic_menu_reload.png | Bin 627 -> 779 bytes .../ic_menu_reload.png | Bin 824 -> 998 bytes .../ic_menu_reload.png | Bin 1166 -> 1324 bytes .../resources/values-large-v11/styles.xml | 11 ++++------- .../toolbar/BrowserToolbarTabletBase.java | 3 ++- 5 files changed, 6 insertions(+), 8 deletions(-) diff --git a/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_reload.png b/mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_reload.png index a8eb29ef4b6bc7944e4fa46d0f3d1ac62c80624a..7cc5d918bb35459c38587ba0f91a4cbba264f997 100644 GIT binary patch literal 779 zcmeAS@N?(olHy`uVBq!ia0vp^(jd&i3?z4Pv7|FFFj@!rgt!8^5D*#~>fqpDY@7yS zBqRVyU*9ypq@>W$w79sq(8R>p*x1m7gqWDLh@_WB{nHNJ~=%$F(o!3Jw6dAn-QCw8kd*>6bF%sX|YKl1XL3TWF%(9ffyO_$!S0` z7Dy%nxnPu>77t`3X8?79M3TWKBmvcfxbZ*+ND!nKNPl&NbIR!;V$EIiG9XZ3 zH*DUzeaFr{`wkpDeB$J}^B1mMy>{!)-FpunJ$?S-<-7NvzkL7k^Vjb`fB*HwE%*nF zI&n`I$B+uf(hHU5R)r$RKU(jPPcgjNs*`eZ(%elco?5##xHX-A+WdEaaPG_8*Q<>` z|N370?&qB1ledjJAH?WX*6k6NRGPh!OLW$uN{dgyb&Kkw19G=8+&$cp&%f$y_{Z?K zQmb48zZaFw&~=K)6`J;{?1X}tSJv9iIsQD8lix%~@vIS;kP>$?b^+T1V~v%+#V#F6 z(4JKFS)hc)?7@?id+FNu!Ba+RE)cMn_o>!&^)(g($d;rq6gm9Z<1u+uy^^jMXy(K}%N0 z41>$n29x{XeXY*q9G!_i8J1}1u{^6`%h4uxzPd^T0wO0Umw%xA5(@gAC}x=FKU&2j+Hg1`TT;a2nmnm*yr7|ze2;~4!; zfSf_zO5{CIvJ)s<2D01WysrdK!E~OfhZ8_6fsWbWk4ETh$^?GLpiu@(vaB+o@MT#3 z&0s(!Vs%TlhkuQI@dd+>rav%1bX_l{0+eLiC4u6jzLudvbSUx72h1%Mpj5`n0V`*e zXvAEiA9XH9ssN>PURgB{@x%yRNk3|g)%FFlWKE#@MnutQGI^R*f-6csVsaD6o{Ho| zu%{%{wCX)_90J#|E(s}Re zw&>ofmB<&~8c`Um=?eAn30XH6EY0pdm@v7-wcIfI7U>3=vDjqcKH(?*@7&hlFyv|- z4lAH!Az%u|dV-#rIEVQ14?F5fcdUA16Q&mV&x#qr3P9ZySo~2D=p9-UQ$a8cqqxSL z45Sx)bOhSUU3h*E_uEhq_QQRVY0I?Mj1~0L^$m2Z`Q}_e@&*6^002ovPDHLkV1f=J BDOUgh diff --git a/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_reload.png b/mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_reload.png index 3a9fd8df013797de2e9b679ff265d709b6062eff..a9c7b3f62dbf46b60f8b22928b54a1c6b8f9e249 100644 GIT binary patch literal 998 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k3?#4J%UH$0z&JI)C&U%Vg#ZT!2V>*3(9lp% zPfv%01R%rLH!U_cHZ(LXAR!?tDk?564#PBgpBCq^!TJS zFgraaIV~jgRtBmz_6#m#$vBe(Uy~yZ0YFeDwI)^OtYlzI*@S)8{YWzW@03_ur+yr8&SPQ|0O67*fI5 zd+zyc?m(HNAC1!%ub-N_F~atm*6Phddj$ejssvdUcPLKmaB5L0{+BoJzRk|>=TheV z{;|h0f1f_zzr(67x{>z9C3(-E1!kNxJ+$F%lX$?Fy)|qLD_x~7=JgBef3sq^V}0e~ zm14e1%g!scuU;gyb6OogeI>9l?9!aYpWX$2Jl34$RHBivqOyodsdklEcUZY*Rr)jM z!t-Uy7j3)3*p_w9lJpJUVt#1LHb-Nv;4_D|EOcd__AN#J(3Y>-zB3j_usc6fS1yQt zX3AqM>$Xs-Sn$?7`<{dDGm|eIJNEcP^rF}eztk?)MeLpUf@RGqp}Q?tMLYNH-f+{e zkj3m=dZ!e3V)4{PEsEU>b$&Y*${9|-zii>0v=v(z+FY-D)~m9)#Tka!K3K8*_p3k8 zFE3ejY_?3w&zGXgD?_chO537>SKYd@OpN8qzMRH&b_Nsov$b73?(-pqNAO(Ye8czY zhDXj;F;4xrIfkqA(#_qX|HF6x`Vy9B$y3iLY@n?y^uWdelv_Pr{an^LB{Ts5Z~7tr-lw2wACJ~x1S8dE5kgB2zBq#aK;QCK`(8(tzO)MnyO zWa$MYBL#^+mVbkFn%qD)+>q?7iD#F)2o29i;@eJ1d_$aHMe#+N?5jw; zpi-a>k?eqyAY_-K;hWDs`|OROj06x(Oq%SnTtE_E%D@aj#GN3cBv5MN$(exUZIJAo z5+}Ag>7-;6$RUg@Bk{MHfY9(A?k~fm#YZ5A6xmo{GJl(_M7;T2B5XbwHd$e00>orr z*Fj~H*CB^=#8Dm?nZ(AoIu3p#i7~s+BwDV`9fy#(rNeiIK}N|)tpr9U@t+}S2eKAR z1+swyCL<8pnW_FmHUxi3=-4_4f^93Ez*SmE3V=pP6R%D+S;Ut$3TR@w$=Egus86bc zk$npqA%8UXgLELC4UJHN;;Wo_4DG2|#oRtp6y&7aT?&=Iy6kl3@Ad1g-0vTRY zAUX^xlv2ebkoY$6m6*o`CDHtbFgYA@3Nb!ZlU@9CO1jU_xufL7(j^10$rCIk$06Aj ze~fX-J)WksPnS!661NIbd?o;F0B&s<7(P!M?tkvMk3iiC9@+p6caRIbS|O9XD{reK zzCpf(j`$(z<9%BojQBdQFo0yo)w&@>vVG-QAo7IjSVNf3c^ZcewMkxbJ9_p>e&@(N z9W9lgBz++9z=~ny9y&e>swEZjB}lfn{O8F4^0_8%k7TcaT1oyZ%7{|L&9&kFXm~0b syC+_L<4J9WWP51h52gE%k`yxPdzK%DULgRubN~PV07*qoM6N<$f_m|LcK`qY diff --git a/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_reload.png b/mobile/android/base/resources/drawable-large-xxhdpi-v11/ic_menu_reload.png index adac63d7137b30c0738f743be13d9f734778b0d6..d582128a174c2e5000ec8cec956e7cfc068cd706 100644 GIT binary patch literal 1324 zcmeAS@N?(olHy`uVBq!ia0vp^W+2SL3?z5Yp7oxAf$?jAPlzj!3jv{_p$-lX#>Qzt zhNq{eLqYj?-qNAfjL(>8h5~8A_LPL`yBhx|?6M?dkktv}G2@y$2ad8>Z z(J2v$i4h4Iv9T#JG0AapsWC}OkqK!)ZhU-dY;tl;QhHQEYD{7pP;o4fOiGVS0+MOb z$>}i(Kz3?Oa$0OcdOVmBmy{8clp2?q9-EvNpOgV)#3cY3>2Zl^u}K;6KyETn7R*jc zj|GY+1IaWX8K0N|WW*OINO4zj^E4gGY~_Jbm`!<*V0k-oE?r@zduo zU%!3-@$=X3KY#ztjy!Ubfq}`v)5S5Qf^qKopW!|EGRHnG~6I;n*73(E^qnt|9#9^&1;n!U0-;wzw3YEL+!aCY3t1A zeAkej)YHD|4F~T^m zNldMi16OT|<2o9kv6L&Qm$NqS#Hp1nwIX^BWplGuESbP*9nLJfAUNiI;`4pp>OU0L zncWb*SKN}Xw7RKJbK+Eo<8w~QGyE zPOJP*)c=385@+p=W7~YC&g+n%*Ds@ljqjAj7pn7`?eo)Hq0A_BeJMwBc7b#1b%kV! zQ_amkF2}!A5bl#H@DK#!p_*(>cZucWA7W^ Y@Mq;Y99MDTH3B7nPgg&ebxsLQ0AwCG8~^|S delta 1158 zcmV;11bO?c3XTbo7k@bj1^@s6eG}LS000D4Nkl^Dy$z|x@ z3K#|&-W0MExqt9c6aW>Vd~KZUI@@f1h(CV|akw6RS^}#uPem>~ngl@8Snlb`{nYrD zmGgXLV!8d_`s<*aUzs?3kRG7^_Ownm{-NDhuWwx!ii7-nFISdU=LVMpO9G+?BPZ*% z=(gV{4i}K~2jNmCS7Td9VT!OR6|xs`xCq<3tzedGD}P!SzG?UjxbUuVeqUn02)vlW z%ML~q=EQFRnG~#2_*t!!Pa5=C9}Uj4{Jm4zH-8a^IUJu!{4`DMB?p z7-nC;T7Q1?6i`&i>WY=pgW=#3%f~9HA{>(f%CZpR!l+)+aHuKorq}fkL%kW65Iefb zVAjday4CX4_)vbMBo{OyWz81A%Q&_N#H%@383kmYGTg1_4v4NL^DZFk`CIXoE|{du4O`S18rqk9zak z%zs&%tf!|s7M?oIubOkyYsC~|d6_7Tml}$wHYGWEiCj1ya(F9B(P0h9+D}?rn=Ta; zCK}?=@BZ^lxyqY7 zOiWIm=x_ODlqjKID{IBDmm$&hBdBv1&hw$8mCIQtTkE${qQ-J?ibP{p$z^Fs34iSe z3NNC%E=y^sHZ$zkA}6n=20{RU0-C6)D+S;%#C{V*2nkl`P2?1Mdi?0we`Pqx6|Sv@ z7dB?NrGghH=U^;FOxRw-1&nf7B=eBcwJzL-{=p$2LJ#707*qoM6N<$f}$5Pod5s; diff --git a/mobile/android/base/resources/values-large-v11/styles.xml b/mobile/android/base/resources/values-large-v11/styles.xml index eef90827ecd..4a8d2c95bc7 100644 --- a/mobile/android/base/resources/values-large-v11/styles.xml +++ b/mobile/android/base/resources/values-large-v11/styles.xml @@ -48,14 +48,11 @@ @color/action_bar_menu_item_colors center - - Notes: - * The bookmarks star is larger than the reload button - * The reload button contains whitespace at the top of the image to lower it --> - 19dp - 21dp @dimen/tablet_browser_toolbar_menu_item_padding_horizontal @dimen/tablet_browser_toolbar_menu_item_padding_horizontal diff --git a/mobile/android/base/toolbar/BrowserToolbarTabletBase.java b/mobile/android/base/toolbar/BrowserToolbarTabletBase.java index 40de5626e12..78d5c511ce0 100644 --- a/mobile/android/base/toolbar/BrowserToolbarTabletBase.java +++ b/mobile/android/base/toolbar/BrowserToolbarTabletBase.java @@ -20,6 +20,7 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.util.AttributeSet; import android.view.View; +import android.view.ViewGroup.LayoutParams; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; @@ -102,7 +103,7 @@ abstract class BrowserToolbarTabletBase extends BrowserToolbar { @Override public boolean addActionItem(final View actionItem) { - actionItemBar.addView(actionItem); + actionItemBar.addView(actionItem, LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT); return true; } From 95892470e4b85c539b52478bc25b9f55d0205c93 Mon Sep 17 00:00:00 2001 From: "Jonathan Almeida [:jonalmeida]" Date: Sat, 15 Aug 2015 23:36:37 -0700 Subject: [PATCH 04/23] Bug 1170725 - Click-to-play images. r=mfinkle --- mobile/android/app/mobile.js | 3 + mobile/android/chrome/content/browser.js | 25 +++++++ .../android/components/ImageBlockingPolicy.js | 74 +++++++++++++++++++ .../components/MobileComponents.manifest | 5 ++ mobile/android/components/moz.build | 1 + mobile/android/installer/package-manifest.in | 7 +- .../locales/en-US/chrome/browser.properties | 1 + .../themes/core/images/placeholder_image.svg | 62 ++++++++++++++++ mobile/android/themes/core/jar.mn | 1 + 9 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 mobile/android/components/ImageBlockingPolicy.js create mode 100644 mobile/android/themes/core/images/placeholder_image.svg diff --git a/mobile/android/app/mobile.js b/mobile/android/app/mobile.js index dea3ff66257..374d233bb42 100644 --- a/mobile/android/app/mobile.js +++ b/mobile/android/app/mobile.js @@ -350,6 +350,9 @@ pref("browser.link.open_newwindow", 3); // 0=force all new windows to tabs, 1=don't force, 2=only force those with no features set pref("browser.link.open_newwindow.restriction", 0); +// Image blocking policy +pref("browser.image_blocking.enabled", false); + // controls which bits of private data to clear. by default we clear them all. pref("privacy.item.cache", true); pref("privacy.item.cookies", true); diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index bba8b315f01..23f807f7e3f 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -970,6 +970,14 @@ var BrowserApp = { filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject, aTarget.ownerDocument, true, null); }); + + NativeWindow.contextmenus.add(stringGetter("contextmenu.showImage"), + NativeWindow.contextmenus.imageBlockingPolicyContext, + function(target) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_show_image"); + target.setAttribute("data-ctv-show", "true"); + target.setAttribute("src", target.getAttribute("data-ctv-src")); + }); }, onAppUpdated: function() { @@ -2520,6 +2528,23 @@ var NativeWindow = { } }, + imageBlockingPolicyContext: { + matches: function imageBlockingPolicyContextMatches(aElement) { + if (!Services.prefs.getBoolPref("browser.image_blocking.enabled")) { + return false; + } + + if (aElement instanceof Ci.nsIDOMHTMLImageElement) { + // Only show the menuitem if we are blocking the image + if (aElement.getAttribute("data-ctv-show") == "true") { + return false; + } + return true; + } + return false; + } + }, + mediaContext: function(aMode) { return { matches: function(aElt) { diff --git a/mobile/android/components/ImageBlockingPolicy.js b/mobile/android/components/ImageBlockingPolicy.js new file mode 100644 index 00000000000..de4f047db1a --- /dev/null +++ b/mobile/android/components/ImageBlockingPolicy.js @@ -0,0 +1,74 @@ +/* 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/. */ + +const { classes: Cc, interfaces: Ci, manager: Cm, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Timer.jsm"); + +/** + * Content policy for blocking images + */ + +// SVG placeholder image for blocked image content +let PLACEHOLDER_IMG = "chrome://browser/skin/images/placeholder_image.svg"; + +function ImageBlockingPolicy() {} + +ImageBlockingPolicy.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPolicy]), + classDescription: "Click-To-Play Image", + classID: Components.ID("{f55f77f9-d33d-4759-82fc-60db3ee0bb91}"), + contractID: "@mozilla.org/browser/blockimages-policy;1", + xpcom_categories: [{category: "content-policy", service: true}], + + // nsIContentPolicy interface implementation + shouldLoad: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) { + if (!getEnabled()) { + return Ci.nsIContentPolicy.ACCEPT; + } + + if (contentType === Ci.nsIContentPolicy.TYPE_IMAGE || contentType === Ci.nsIContentPolicy.TYPE_IMAGESET) { + // Accept any non-http(s) image URLs + if (!contentLocation.schemeIs("http") && !contentLocation.schemeIs("https")) { + return Ci.nsIContentPolicy.ACCEPT; + } + + if (node instanceof Ci.nsIDOMHTMLImageElement) { + // Accept if the user has asked to view the image + if (node.getAttribute("data-ctv-show") == "true") { + return Ci.nsIContentPolicy.ACCEPT; + } + + setTimeout(() => { + // Cache the original image URL and swap in our placeholder + node.setAttribute("data-ctv-src", contentLocation.spec); + node.setAttribute("src", PLACEHOLDER_IMG); + + // For imageset (img + srcset) the "srcset" is used even after we reset the "src" causing a loop. + // We are given the final image URL anyway, so it's OK to just remove the "srcset" value. + node.removeAttribute("srcset"); + }, 0); + } + + // Reject any image that is not associated with a DOM element + return Ci.nsIContentPolicy.REJECT; + } + + // Accept all other content types + return Ci.nsIContentPolicy.ACCEPT; + }, + + shouldProcess: function(contentType, contentLocation, requestOrigin, node, mimeTypeGuess, extra) { + return Ci.nsIContentPolicy.ACCEPT; + }, + +}; + +function getEnabled() { + return Services.prefs.getBoolPref("browser.image_blocking.enabled"); +} + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ImageBlockingPolicy]); diff --git a/mobile/android/components/MobileComponents.manifest b/mobile/android/components/MobileComponents.manifest index 32933fa7f51..018357bdbdd 100644 --- a/mobile/android/components/MobileComponents.manifest +++ b/mobile/android/components/MobileComponents.manifest @@ -55,6 +55,11 @@ contract @mozilla.org/embedcomp/prompt-service;1 {9a61149b-2276-4a0a-b79c-be994a category wakeup-request PromptService @mozilla.org/embedcomp/prompt-service;1,nsIPromptService,getService,Prompt:Call #endif +# ImageBlockingPolicy.js +component {f55f77f9-d33d-4759-82fc-60db3ee0bb91} ImageBlockingPolicy.js +contract @mozilla.org/browser/blockimages-policy;1 {f55f77f9-d33d-4759-82fc-60db3ee0bb91} +category content-policy ImageBlockingPolicy @mozilla.org/browser/blockimages-policy;1 + # XPIDialogService.js component {c1242012-27d8-477e-a0f1-0b098ffc329b} XPIDialogService.js contract @mozilla.org/addons/web-install-prompt;1 {c1242012-27d8-477e-a0f1-0b098ffc329b} diff --git a/mobile/android/components/moz.build b/mobile/android/components/moz.build index 0f1313cadbb..1d143460fcb 100644 --- a/mobile/android/components/moz.build +++ b/mobile/android/components/moz.build @@ -21,6 +21,7 @@ EXTRA_COMPONENTS += [ 'DirectoryProvider.js', 'FilePicker.js', 'HelperAppDialog.js', + 'ImageBlockingPolicy.js', 'LoginManagerPrompter.js', 'NSSDialogService.js', 'SiteSpecificUserAgent.js', diff --git a/mobile/android/installer/package-manifest.in b/mobile/android/installer/package-manifest.in index 831b0394889..09479750b7a 100644 --- a/mobile/android/installer/package-manifest.in +++ b/mobile/android/installer/package-manifest.in @@ -2,7 +2,7 @@ ; 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 file for the Fennec build. +; Package file for the Fennec build. ; ; File format: ; @@ -522,7 +522,7 @@ @BINPATH@/defaults/profile/prefs.js ; [Layout Engine Resources] -; Style Sheets, Graphics and other Resources used by the layout engine. +; Style Sheets, Graphics and other Resources used by the layout engine. @BINPATH@/res/EditorOverride.css @BINPATH@/res/contenteditable.css @BINPATH@/res/designmode.css @@ -626,6 +626,7 @@ bin/libfreebl_32int64_3.so @BINPATH@/components/ColorPicker.js @BINPATH@/components/ContentDispatchChooser.js @BINPATH@/components/ContentPermissionPrompt.js +@BINPATH@/components/ImageBlockingPolicy.js @BINPATH@/components/DirectoryProvider.js @BINPATH@/components/FilePicker.js @BINPATH@/components/HelperAppDialog.js @@ -653,7 +654,7 @@ bin/libfreebl_32int64_3.so @BINPATH@/components/browsercomps.xpt #ifdef ENABLE_MARIONETTE -@BINPATH@/chrome/marionette@JAREXT@ +@BINPATH@/chrome/marionette@JAREXT@ @BINPATH@/chrome/marionette.manifest @BINPATH@/components/MarionetteComponents.manifest @BINPATH@/components/marionettecomponent.js diff --git a/mobile/android/locales/en-US/chrome/browser.properties b/mobile/android/locales/en-US/chrome/browser.properties index c540948ffdf..c1b7ab87376 100644 --- a/mobile/android/locales/en-US/chrome/browser.properties +++ b/mobile/android/locales/en-US/chrome/browser.properties @@ -248,6 +248,7 @@ contextmenu.shareImage=Share Image # the text you have selected. %S is the name of the search engine. For example, "Google". contextmenu.search=%S Search contextmenu.saveImage=Save Image +contextmenu.showImage=Show Image contextmenu.setImageAs=Set Image As contextmenu.addSearchEngine2=Add as Search Engine contextmenu.playMedia=Play diff --git a/mobile/android/themes/core/images/placeholder_image.svg b/mobile/android/themes/core/images/placeholder_image.svg new file mode 100644 index 00000000000..a4174b1bba4 --- /dev/null +++ b/mobile/android/themes/core/images/placeholder_image.svg @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/android/themes/core/jar.mn b/mobile/android/themes/core/jar.mn index a751815adef..a9cf4bbb88a 100644 --- a/mobile/android/themes/core/jar.mn +++ b/mobile/android/themes/core/jar.mn @@ -74,6 +74,7 @@ chrome.jar: skin/images/certerror-warning.png (images/certerror-warning.png) skin/images/throbber.png (images/throbber.png) skin/images/search-clear-30.png (images/search-clear-30.png) + skin/images/placeholder_image.svg (images/placeholder_image.svg) skin/images/play-hdpi.png (images/play-hdpi.png) skin/images/pause-hdpi.png (images/pause-hdpi.png) skin/images/cast-ready-hdpi.png (images/cast-ready-hdpi.png) From f6a04d6bdbcbfbd5d378fa2bdbf715b656fbb157 Mon Sep 17 00:00:00 2001 From: "Jonathan Almeida [:jonalmeida]" Date: Wed, 19 Aug 2015 17:08:15 -0700 Subject: [PATCH 05/23] Bug 1195063 - Create a UI Settings option for changing the image threshold image size for click-to-play images r=mfinkle --- mobile/android/base/locales/en-US/android_strings.dtd | 3 +++ mobile/android/base/preferences/GeckoPreferences.java | 8 ++++++++ .../base/resources/xml-v11/preferences_customize.xml | 5 +++++ .../resources/xml-v11/preferences_customize_tablet.xml | 5 +++++ .../android/base/resources/xml/preferences_customize.xml | 5 +++++ mobile/android/base/strings.xml.in | 3 +++ 6 files changed, 29 insertions(+) diff --git a/mobile/android/base/locales/en-US/android_strings.dtd b/mobile/android/base/locales/en-US/android_strings.dtd index 7fd892fec48..df554438034 100644 --- a/mobile/android/base/locales/en-US/android_strings.dtd +++ b/mobile/android/base/locales/en-US/android_strings.dtd @@ -217,6 +217,9 @@ + + + diff --git a/mobile/android/base/preferences/GeckoPreferences.java b/mobile/android/base/preferences/GeckoPreferences.java index a71f9575305..1ae1b3dca4a 100644 --- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -133,6 +133,7 @@ OnSharedPreferenceChangeListener private static final String PREFS_DEVTOOLS = NON_PREF_PREFIX + "devtools.enabled"; private static final String PREFS_DISPLAY = NON_PREF_PREFIX + "display.enabled"; private static final String PREFS_CUSTOMIZE_HOME = NON_PREF_PREFIX + "customize_home"; + private static final String PREFS_CUSTOMIZE_IMAGE_BLOCKING = "browser.image_blocking.enabled"; private static final String PREFS_TRACKING_PROTECTION_PRIVATE_BROWSING = "privacy.trackingprotection.pbmode.enabled"; private static final String PREFS_TRACKING_PROTECTION_LEARN_MORE = NON_PREF_PREFIX + "trackingprotection.learn_more"; private static final String PREFS_CATEGORY_PRIVATE_DATA = NON_PREF_PREFIX + "category_private_data"; @@ -886,6 +887,13 @@ OnSharedPreferenceChangeListener i--; continue; } + } else if (PREFS_CUSTOMIZE_IMAGE_BLOCKING.equals(key)) { + // Only enable the ZoomedView / magnifying pref on Nightly. + if (!AppConstants.NIGHTLY_BUILD) { + preferences.removePreference(pref); + i--; + continue; + } } else if (PREFS_HOMEPAGE.equals(key)) { String setUrl = GeckoSharedPrefs.forProfile(getBaseContext()).getString(PREFS_HOMEPAGE, AboutPages.HOME); setHomePageSummary(pref, setUrl); diff --git a/mobile/android/base/resources/xml-v11/preferences_customize.xml b/mobile/android/base/resources/xml-v11/preferences_customize.xml index 8cbaad39714..9ff2ee0f932 100644 --- a/mobile/android/base/resources/xml-v11/preferences_customize.xml +++ b/mobile/android/base/resources/xml-v11/preferences_customize.xml @@ -31,6 +31,11 @@ android:entryValues="@array/pref_restore_values" android:persistent="true" /> + + + + + + &pref_cookies_not_accept_foreign; &pref_cookies_disabled; + &pref_tap_to_load_images_title; + &pref_tap_to_load_images_summary; + &pref_tracking_protection_title; &pref_tracking_protection_summary3; &pref_donottrack_title; From 0fac17567f8cd71db36abd00c2c4cbe00cd66fa1 Mon Sep 17 00:00:00 2001 From: Sergej Kravcenko Date: Wed, 23 Sep 2015 03:17:44 +0300 Subject: [PATCH 06/23] Bug 1201623 - On tablets URL bar, about:home magnifying glass and globe icon take up different widths. r=mcomella --- .../layout-large-v11/browser_toolbar.xml | 1 - .../layout/toolbar_display_layout.xml | 4 +- .../resources/layout/toolbar_edit_layout.xml | 21 +++++--- .../base/toolbar/ToolbarEditLayout.java | 54 +++++++++++++++++++ .../android/base/toolbar/ToolbarEditText.java | 46 +++++----------- 5 files changed, 84 insertions(+), 42 deletions(-) diff --git a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml index eb19cd84515..442fac910b3 100644 --- a/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml +++ b/mobile/android/base/resources/layout-large-v11/browser_toolbar.xml @@ -68,7 +68,6 @@ + we add a bottom margin to align their bottoms. + Site security icon must have exact position and size as search icon in + edit layout --> - + + + diff --git a/mobile/android/base/toolbar/ToolbarEditLayout.java b/mobile/android/base/toolbar/ToolbarEditLayout.java index 5140c88878b..b46341ac18d 100644 --- a/mobile/android/base/toolbar/ToolbarEditLayout.java +++ b/mobile/android/base/toolbar/ToolbarEditLayout.java @@ -7,6 +7,7 @@ package org.mozilla.gecko.toolbar; import android.app.Activity; import android.content.Intent; +import android.graphics.drawable.Drawable; import android.speech.RecognizerIntent; import android.widget.Button; import android.widget.ImageButton; @@ -24,6 +25,8 @@ import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener; import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener; import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState; import org.mozilla.gecko.util.ActivityResultHandler; +import org.mozilla.gecko.util.DrawableUtil; +import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.StringUtils; import org.mozilla.gecko.util.InputOptionsUtils; import org.mozilla.gecko.widget.themed.ThemedLinearLayout; @@ -33,6 +36,7 @@ import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.inputmethod.InputMethodManager; +import android.widget.ImageView; import java.util.List; @@ -44,6 +48,12 @@ import java.util.List; */ public class ToolbarEditLayout extends ThemedLinearLayout { + public interface OnSearchStateChangeListener { + public void onSearchStateChange(boolean isActive); + } + + private final ImageView mSearchIcon; + private final ToolbarEditText mEditText; private final ImageButton mVoiceInput; @@ -59,6 +69,8 @@ public class ToolbarEditLayout extends ThemedLinearLayout { setOrientation(HORIZONTAL); LayoutInflater.from(context).inflate(R.layout.toolbar_edit_layout, this); + mSearchIcon = (ImageView) findViewById(R.id.search_icon); + mEditText = (ToolbarEditText) findViewById(R.id.url_edit_text); mVoiceInput = (ImageButton) findViewById(R.id.mic); @@ -67,6 +79,10 @@ public class ToolbarEditLayout extends ThemedLinearLayout { @Override public void onAttachedToWindow() { + if (HardwareUtils.isTablet()) { + mSearchIcon.setVisibility(View.VISIBLE); + } + mEditText.setOnFocusChangeListener(new OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { @@ -91,6 +107,13 @@ public class ToolbarEditLayout extends ThemedLinearLayout { } }); + mEditText.setOnSearchStateChangeListener(new OnSearchStateChangeListener() { + @Override + public void onSearchStateChange(boolean isActive) { + updateSearchIcon(isActive); + } + }); + mVoiceInput.setOnClickListener(new Button.OnClickListener() { @Override public void onClick(View v) { @@ -104,6 +127,37 @@ public class ToolbarEditLayout extends ThemedLinearLayout { launchQRCodeReader(); } }); + + // Set an inactive search icon on tablet devices when in editing mode + updateSearchIcon(false); + } + + /** + * Update the search icon at the left of the edittext based + * on its state. + * + * @param isActive The state of the edittext. Active is when the initialized + * text has changed and is not empty. + */ + void updateSearchIcon(boolean isActive) { + if (!HardwareUtils.isTablet()) { + return; + } + + // When on tablet show a magnifying glass in editing mode + final int searchDrawableId = R.drawable.search_icon_active; + final Drawable searchDrawable; + if (!isActive) { + searchDrawable = DrawableUtil.tintDrawable(getContext(), searchDrawableId, R.color.placeholder_grey); + } else { + if (isPrivateMode()) { + searchDrawable = DrawableUtil.tintDrawable(getContext(), searchDrawableId, R.color.tabs_tray_icon_grey); + } else { + searchDrawable = getResources().getDrawable(searchDrawableId); + } + } + + mSearchIcon.setImageDrawable(searchDrawable); } @Override diff --git a/mobile/android/base/toolbar/ToolbarEditText.java b/mobile/android/base/toolbar/ToolbarEditText.java index 1ce7ff25b9b..a709a242352 100644 --- a/mobile/android/base/toolbar/ToolbarEditText.java +++ b/mobile/android/base/toolbar/ToolbarEditText.java @@ -13,13 +13,11 @@ import org.mozilla.gecko.R; import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener; import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener; import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener; -import org.mozilla.gecko.util.DrawableUtil; +import org.mozilla.gecko.toolbar.ToolbarEditLayout.OnSearchStateChangeListener; import org.mozilla.gecko.util.GamepadUtils; import org.mozilla.gecko.util.StringUtils; -import org.mozilla.gecko.util.HardwareUtils; import android.content.Context; -import android.graphics.drawable.Drawable; import android.graphics.Rect; import android.text.Editable; import android.text.NoCopySpan; @@ -56,6 +54,7 @@ public class ToolbarEditText extends CustomEditText private OnCommitListener mCommitListener; private OnDismissListener mDismissListener; private OnFilterListener mFilterListener; + private OnSearchStateChangeListener mSearchStateChangeListener; private ToolbarPrefs mPrefs; @@ -87,14 +86,16 @@ public class ToolbarEditText extends CustomEditText mFilterListener = listener; } + void setOnSearchStateChangeListener(OnSearchStateChangeListener listener) { + mSearchStateChangeListener = listener; + } + @Override public void onAttachedToWindow() { setOnKeyListener(new KeyListener()); setOnKeyPreImeListener(new KeyPreImeListener()); setOnSelectionChangedListener(new SelectionChangeListener()); addTextChangedListener(new TextChangeListener()); - // Set an inactive search icon on tablet devices when in editing mode - updateSearchIcon(false); } @Override @@ -104,7 +105,9 @@ public class ToolbarEditText extends CustomEditText // Make search icon inactive when edit toolbar search term isn't a user entered // search term final boolean isActive = !TextUtils.isEmpty(getText()); - updateSearchIcon(isActive); + if (mSearchStateChangeListener != null) { + mSearchStateChangeListener.onSearchStateChange(isActive); + } if (gainFocus) { resetAutocompleteState(); @@ -160,33 +163,6 @@ public class ToolbarEditText extends CustomEditText mPrefs = prefs; } - /** - * Update the search icon at the left of the edittext based - * on its state. - * - * @param isActive The state of the edittext. Active is when the initialized - * text has changed and is not empty. - */ - void updateSearchIcon(boolean isActive) { - if (!HardwareUtils.isTablet()) { - return; - } - - // When on tablet show a magnifying glass in editing mode - final int searchDrawableId = R.drawable.search_icon_active; - final Drawable searchDrawable; - if (!isActive) { - searchDrawable = DrawableUtil.tintDrawable(getContext(), searchDrawableId, R.color.placeholder_grey); - } else { - if (isPrivateMode()) { - searchDrawable = DrawableUtil.tintDrawable(getContext(), searchDrawableId, R.color.tabs_tray_icon_grey); - } else { - searchDrawable = getResources().getDrawable(searchDrawableId); - } - } - setCompoundDrawablesWithIntrinsicBounds(searchDrawable, null, null, null); - } - /** * Mark the start of autocomplete changes so our text change * listener does not react to changes in autocomplete text @@ -564,7 +540,9 @@ public class ToolbarEditText extends CustomEditText } // Update search icon with an active state since user is typing - updateSearchIcon(textLength > 0); + if (mSearchStateChangeListener != null) { + mSearchStateChangeListener.onSearchStateChange(textLength > 0); + } if (mFilterListener != null) { mFilterListener.onFilter(text, doAutocomplete ? ToolbarEditText.this : null); From 4eed96df1fd84e9438ba8db1b49356194094ff0a Mon Sep 17 00:00:00 2001 From: Sergej Kravcenko Date: Thu, 24 Sep 2015 05:30:18 +0300 Subject: [PATCH 07/23] Bug 1205149 - Update SearchEngineRow.java to use resource values.xml over private constants. r=mcomella --- mobile/android/base/home/SearchEngineRow.java | 34 ++++++++++++------- .../resources/values-large-v11/integers.xml | 2 ++ .../base/resources/values/integers.xml | 2 ++ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/mobile/android/base/home/SearchEngineRow.java b/mobile/android/base/home/SearchEngineRow.java index 15a9141a342..38f1fc1b875 100644 --- a/mobile/android/base/home/SearchEngineRow.java +++ b/mobile/android/base/home/SearchEngineRow.java @@ -68,9 +68,12 @@ class SearchEngineRow extends AnimatedHeightLayout { // Selected suggestion view private int mSelectedView; - // Maximums for suggestions based on form factor - private static final int TABLET_MAX = 4; - private static final int PHONE_MAX = 2; + // Maximums for suggestions + private int mMaxSavedSuggestions; + private int mMaxSearchSuggestions; + + // Remove this default limit value in Bug 1201325 + private static final int SUGGESTIONS_MAX = 4; public SearchEngineRow(Context context) { this(context, null); @@ -132,6 +135,10 @@ class SearchEngineRow extends AnimatedHeightLayout { mUserEnteredView.setOnClickListener(mClickListener); mUserEnteredTextView = (TextView) findViewById(R.id.suggestion_text); + + // Suggestion limits + mMaxSavedSuggestions = getResources().getInteger(R.integer.max_saved_suggestions); + mMaxSearchSuggestions = getResources().getInteger(R.integer.max_search_suggestions); } private void setDescriptionOnSuggestion(View v, String suggestion) { @@ -262,14 +269,14 @@ class SearchEngineRow extends AnimatedHeightLayout { if (!AppConstants.NIGHTLY_BUILD) { return null; } + final ContentResolver cr = getContext().getContentResolver(); String[] columns = new String[] { SearchHistory.QUERY }; String actualQuery = SearchHistory.QUERY + " LIKE ?"; String[] queryArgs = new String[] { '%' + searchTerm + '%' }; - final int limit = HardwareUtils.isTablet() ? TABLET_MAX : PHONE_MAX; - String sortOrderAndLimit = SearchHistory.DATE +" DESC LIMIT "+limit; + String sortOrderAndLimit = SearchHistory.DATE +" DESC LIMIT " + mMaxSavedSuggestions; return cr.query(SearchHistory.CONTENT_URI, columns, actualQuery, queryArgs, sortOrderAndLimit); } @@ -278,25 +285,26 @@ class SearchEngineRow extends AnimatedHeightLayout { * * @param animate whether or not to animate suggestions for visual polish * @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls - * @param savedCount how many saved searches this searchTerm has + * @param savedSuggestionCount how many saved searches this searchTerm has * @return the global count of how many suggestions have been bound/shown in the search engine row */ - private int updateFromSearchEngine(boolean animate, int recycledSuggestionCount, int savedCount) { - + private int updateFromSearchEngine(boolean animate, int recycledSuggestionCount, int savedSuggestionCount) { // Remove this default limit value in Bug 1201325 - int limit = TABLET_MAX; + int maxSuggestions = SUGGESTIONS_MAX; if (AppConstants.NIGHTLY_BUILD) { - limit = HardwareUtils.isTablet() ? TABLET_MAX : PHONE_MAX; + maxSuggestions = mMaxSearchSuggestions; // If there are less than max saved searches on phones, fill the space with more search engine suggestions - if (!HardwareUtils.isTablet() && savedCount < PHONE_MAX) { - limit += PHONE_MAX - savedCount; + if (!HardwareUtils.isTablet() && savedSuggestionCount < mMaxSavedSuggestions) { + maxSuggestions += mMaxSavedSuggestions - savedSuggestionCount; } } + int suggestionCounter = 0; for (String suggestion : mSearchEngine.getSuggestions()) { - if (suggestionCounter == limit) { + if (suggestionCounter == maxSuggestions) { break; } + // Since the search engine suggestions are listed first, we can use suggestionCounter to get their relative positions for telemetry String telemetryTag = "engine." + suggestionCounter; bindSuggestionView(suggestion, animate, recycledSuggestionCount, suggestionCounter, false, telemetryTag); diff --git a/mobile/android/base/resources/values-large-v11/integers.xml b/mobile/android/base/resources/values-large-v11/integers.xml index 4143cd1eff9..2688e5de500 100644 --- a/mobile/android/base/resources/values-large-v11/integers.xml +++ b/mobile/android/base/resources/values-large-v11/integers.xml @@ -6,5 +6,7 @@ 3 + 4 + 4 diff --git a/mobile/android/base/resources/values/integers.xml b/mobile/android/base/resources/values/integers.xml index ff0aee5e65c..07276a4035a 100644 --- a/mobile/android/base/resources/values/integers.xml +++ b/mobile/android/base/resources/values/integers.xml @@ -10,5 +10,7 @@ 4 3 2 + 2 + 2 From e3dd813d53b01849d15d9983887c46ea08cb7339 Mon Sep 17 00:00:00 2001 From: Mike de Boer Date: Mon, 28 Sep 2015 12:55:46 +0200 Subject: [PATCH 08/23] Backed out changeset 77b2f22f48c7 (bug 1140512) due to invalid fix. rs=me,backout. --- .../BrowserTestUtils/BrowserTestUtils.jsm | 26 ------- .../tests/SimpleTest/AsyncUtilsContent.js | 9 --- toolkit/content/tests/browser/browser.ini | 1 + .../content/tests/browser/browser_findbar.js | 77 ++++++------------- toolkit/content/widgets/findbar.xml | 19 ----- 5 files changed, 26 insertions(+), 106 deletions(-) diff --git a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm index a0f4c1780f4..c8bed4c9335 100644 --- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm +++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.jsm @@ -451,31 +451,5 @@ this.BrowserTestUtils = { tab.ownerDocument.defaultView.gBrowser.removeTab(tab); } }); - }, - - /** - * Version of EventUtils' `sendChar` function; it will synthesize a keypress - * event in a child process and returns a Promise that will result when the - * event was fired. Instead of a Window, a Browser object is required to be - * passed to this function. - * - * @param {String} char - * A character for the keypress event that is sent to the browser. - * @param {Browser} browser - * Browser element, must not be null. - * - * @returns {Promise} - * @resolves True if the keypress event was synthesized. - */ - sendChar(char, browser) { - return new Promise(resolve => { - let mm = browser.messageManager; - mm.addMessageListener("Test:SendCharDone", function charMsg(message) { - mm.removeMessageListener("Test:SendCharDone", charMsg); - resolve(message.data.sendCharResult); - }); - - mm.sendAsyncMessage("Test:SendChar", { char: char }); - }); } }; diff --git a/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js b/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js index 5aab894cd14..ad2f0ac5fff 100644 --- a/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js +++ b/testing/mochitest/tests/SimpleTest/AsyncUtilsContent.js @@ -11,10 +11,6 @@ EventUtils.window = {}; EventUtils.parent = EventUtils.window; EventUtils._EU_Ci = Components.interfaces; EventUtils._EU_Cc = Components.classes; -// EventUtils' `sendChar` function relies on the navigator to synthetize events. -EventUtils.navigator = content.document.defaultView.navigator; -EventUtils.KeyboardEvent = content.document.defaultView.KeyboardEvent; - Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils); addMessageListener("Test:SynthesizeMouse", (message) => { @@ -43,8 +39,3 @@ addMessageListener("Test:SynthesizeMouse", (message) => { let result = EventUtils.synthesizeMouseAtPoint(left, top, data.event, content); sendAsyncMessage("Test:SynthesizeMouseDone", { defaultPrevented: result }); }); - -addMessageListener("Test:SendChar", message => { - let result = EventUtils.sendChar(message.data.char, content); - sendAsyncMessage("Test:SendCharDone", { sendCharResult: result }); -}); diff --git a/toolkit/content/tests/browser/browser.ini b/toolkit/content/tests/browser/browser.ini index c449ce9f69a..e94fe67d7f5 100644 --- a/toolkit/content/tests/browser/browser.ini +++ b/toolkit/content/tests/browser/browser.ini @@ -18,6 +18,7 @@ skip-if = e10s # Bug 1064580 [browser_f7_caret_browsing.js] skip-if = e10s [browser_findbar.js] +skip-if = e10s # Disabled for e10s: Bug ?????? - seems to be a timing issue with RemoteFinder.jsm messages coming later than the tests expect. [browser_input_file_tooltips.js] skip-if = e10s # Bug ?????? - test directly manipulates content (TypeError: doc.createElement is not a function) [browser_isSynthetic.js] diff --git a/toolkit/content/tests/browser/browser_findbar.js b/toolkit/content/tests/browser/browser_findbar.js index 9521ebee104..6dc5a6fe995 100644 --- a/toolkit/content/tests/browser/browser_findbar.js +++ b/toolkit/content/tests/browser/browser_findbar.js @@ -2,8 +2,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/Promise.jsm"); Components.utils.import("resource://gre/modules/Timer.jsm", this); -const TEST_PAGE_URI = "data:text/html;charset=utf-8,The letter s."; - /** * Makes sure that the findbar hotkeys (' and /) event listeners * are added to the system event group and do not get blocked @@ -13,7 +11,7 @@ add_task(function* test_hotkey_event_propagation() { info("Ensure hotkeys are not affected by stopPropagation."); // Opening new tab - let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + let tab = yield promiseTestPageLoad(); let browser = gBrowser.getBrowserForTab(tab); let findbar = gBrowser.getFindBar(); @@ -25,29 +23,24 @@ add_task(function* test_hotkey_event_propagation() { is(findbar.hidden, true, "Findbar is hidden now."); gBrowser.selectedTab = tab; yield promiseFocus(); - yield BrowserTestUtils.sendChar(key, browser); + EventUtils.sendChar(key, browser.contentWindow); is(findbar.hidden, false, "Findbar should not be hidden."); yield closeFindbarAndWait(findbar); } // Stop propagation for all keyboard events. - let frameScript = () => { - const stopPropagation = e => e.stopImmediatePropagation(); - let window = content.document.defaultView; - window.removeEventListener("keydown", stopPropagation); - window.removeEventListener("keypress", stopPropagation); - window.removeEventListener("keyup", stopPropagation); - }; - - let mm = browser.messageManager; - mm.loadFrameScript("data:,(" + frameScript.toString() + ")();", false); + let window = browser.contentWindow; + let stopPropagation = function(e) { e.stopImmediatePropagation(); }; + window.addEventListener("keydown", stopPropagation, true); + window.addEventListener("keypress", stopPropagation, true); + window.addEventListener("keyup", stopPropagation, true); // Checking if findbar still appears when any hotkey is pressed. for (let key of HOTKEYS) { is(findbar.hidden, true, "Findbar is hidden now."); gBrowser.selectedTab = tab; yield promiseFocus(); - yield BrowserTestUtils.sendChar(key, browser); + EventUtils.sendChar(key, browser.contentWindow); is(findbar.hidden, false, "Findbar should not be hidden."); yield closeFindbarAndWait(findbar); } @@ -58,7 +51,7 @@ add_task(function* test_hotkey_event_propagation() { add_task(function* test_not_found() { info("Check correct 'Phrase not found' on new tab"); - let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + let tab = yield promiseTestPageLoad(); // Search for the first word. yield promiseFindFinished("--- THIS SHOULD NEVER MATCH ---", false); @@ -70,7 +63,7 @@ add_task(function* test_not_found() { }); add_task(function* test_found() { - let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + let tab = yield promiseTestPageLoad(); // Search for a string that WILL be found, with 'Highlight All' on yield promiseFindFinished("S", true); @@ -83,10 +76,10 @@ add_task(function* test_found() { // Setting first findbar to case-sensitive mode should not affect // new tab find bar. add_task(function* test_tabwise_case_sensitive() { - let tab1 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + let tab1 = yield promiseTestPageLoad(); let findbar1 = gBrowser.getFindBar(); - let tab2 = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); + let tab2 = yield promiseTestPageLoad(); let findbar2 = gBrowser.getFindBar(); // Toggle case sensitivity for first findbar @@ -109,33 +102,22 @@ add_task(function* test_tabwise_case_sensitive() { gBrowser.removeTab(tab2); }); -/** - * Navigating from a web page (for example mozilla.org) to an internal page - * (like about:addons) might trigger a change of browser's remoteness. - * 'Remoteness change' means that rendering page content moves from child - * process into the parent process or the other way around. - * This test ensures that findbar properly handles such a change. - */ -add_task(function * test_reinitialization_at_remoteness_change() { - info("Ensure findbar re-initialization at remoteness change."); +function promiseTestPageLoad() { + let deferred = Promise.defer(); - // Load a remote page and trigger findbar construction. - let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE_URI); - let browser = gBrowser.getBrowserForTab(tab); - let findbar = gBrowser.getFindBar(); + let tab = gBrowser.selectedTab = gBrowser.addTab("data:text/html;charset=utf-8,The letter s."); + let browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function listener() { + if (browser.currentURI.spec == "about:blank") + return; + info("Page loaded: " + browser.currentURI.spec); + browser.removeEventListener("load", listener, true); - // Findbar should operate normally. - yield promiseFindFinished("s", false); - ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty"); + deferred.resolve(tab); + }, true); - gBrowser.updateBrowserRemoteness(browser, false); - - // Findbar should keep operating normally. - yield promiseFindFinished("s", false); - ok(!findbar._findStatusDesc.textContent, "Findbar status should be empty"); - - yield BrowserTestUtils.removeTab(tab); -}); + return deferred.promise; +} function promiseFindFinished(searchText, highlightOn) { let deferred = Promise.defer(); @@ -149,17 +131,8 @@ function promiseFindFinished(searchText, highlightOn) { findbar._findField.value = searchText; let resultListener; - // When highlighting is on the finder sends a second "FOUND" message after - // the search wraps. This causes timing problems with e10s. waitMore - // forces foundOrTimeout wait for the second "FOUND" message before - // resolving the promise. - let waitMore = highlightOn; let findTimeout = setTimeout(() => foundOrTimedout(null), 2000); let foundOrTimedout = function(aData) { - if (aData !== null && waitMore) { - waitMore = false; - return; - } if (aData === null) info("Result listener not called, timeout reached."); clearTimeout(findTimeout); diff --git a/toolkit/content/widgets/findbar.xml b/toolkit/content/widgets/findbar.xml index 14c49b13472..b9f996794a3 100644 --- a/toolkit/content/widgets/findbar.xml +++ b/toolkit/content/widgets/findbar.xml @@ -373,28 +373,12 @@ // browser property if (this.getAttribute("browserid")) setTimeout(function(aSelf) { aSelf.browser = aSelf.browser; }, 0, this); - - if (typeof gBrowser !== 'undefined') - gBrowser.tabContainer.addEventListener("TabRemotenessChange", this); ]]> - - - - - @@ -418,9 +402,6 @@ // Clear all timers that might still be running. this._cancelTimers(); - - if (typeof gBrowser !== 'undefined') - gBrowser.tabContainer.removeEventListener("TabRemotenessChange", this); ]]> From fa039750ba3b740c76d1e397d5999400a5aaece1 Mon Sep 17 00:00:00 2001 From: Sebastian Kaspari Date: Thu, 24 Sep 2015 15:27:56 +0200 Subject: [PATCH 09/23] Bug 1202052 - Bypass cache on reload button long press. r=margaret --- mobile/android/base/BrowserApp.java | 19 +++++++++++++++++-- mobile/android/base/GeckoView.java | 2 +- mobile/android/base/Tab.java | 4 ++-- mobile/android/base/menu/GeckoMenu.java | 24 ++++++++++++++++++------ mobile/android/chrome/content/browser.js | 5 +++++ 5 files changed, 43 insertions(+), 11 deletions(-) diff --git a/mobile/android/base/BrowserApp.java b/mobile/android/base/BrowserApp.java index 6950bb388f0..b4312c97728 100644 --- a/mobile/android/base/BrowserApp.java +++ b/mobile/android/base/BrowserApp.java @@ -669,7 +669,7 @@ public class BrowserApp extends GeckoApp return true; case KeyEvent.KEYCODE_R: - tab.doReload(); + tab.doReload(false); return true; case KeyEvent.KEYCODE_PERIOD: @@ -3410,7 +3410,7 @@ public class BrowserApp extends GeckoApp if (itemId == R.id.reload) { tab = Tabs.getInstance().getSelectedTab(); if (tab != null) - tab.doReload(); + tab.doReload(false); return true; } @@ -3527,6 +3527,21 @@ public class BrowserApp extends GeckoApp return super.onOptionsItemSelected(item); } + @Override + public boolean onMenuItemLongClick(MenuItem item) { + if (item.getItemId() == R.id.reload) { + Tab tab = Tabs.getInstance().getSelectedTab(); + if (tab != null) { + tab.doReload(true); + + Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.MENU, "reload_force"); + } + return true; + } + + return super.onMenuItemLongClick(item); + } + public void showGuestModeDialog(final GuestModeDialog type) { final Prompt ps = new Prompt(this, new Prompt.PromptCallback() { @Override diff --git a/mobile/android/base/GeckoView.java b/mobile/android/base/GeckoView.java index 704bcd28bbe..238ebb9991d 100644 --- a/mobile/android/base/GeckoView.java +++ b/mobile/android/base/GeckoView.java @@ -465,7 +465,7 @@ public class GeckoView extends LayerView public void reload() { Tab tab = Tabs.getInstance().getTab(mId); if (tab != null) { - tab.doReload(); + tab.doReload(true); } } diff --git a/mobile/android/base/Tab.java b/mobile/android/base/Tab.java index 9baca0ae757..4b36bc67287 100644 --- a/mobile/android/base/Tab.java +++ b/mobile/android/base/Tab.java @@ -614,8 +614,8 @@ public class Tab { return mEnteringReaderMode; } - public void doReload() { - GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", ""); + public void doReload(boolean bypassCache) { + GeckoEvent e = GeckoEvent.createBroadcastEvent("Session:Reload", "{\"bypassCache\":" + String.valueOf(bypassCache) + "}"); GeckoAppShell.sendEventToGecko(e); } diff --git a/mobile/android/base/menu/GeckoMenu.java b/mobile/android/base/menu/GeckoMenu.java index f8b6b076014..6029c2e2d78 100644 --- a/mobile/android/base/menu/GeckoMenu.java +++ b/mobile/android/base/menu/GeckoMenu.java @@ -5,6 +5,7 @@ package org.mozilla.gecko.menu; import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.R; import org.mozilla.gecko.util.ThreadUtils; import org.mozilla.gecko.util.ThreadUtils.AssertBehavior; @@ -256,8 +257,12 @@ public class GeckoMenu extends ListView }); ((MenuItemActionBar) actionView).setOnLongClickListener(new View.OnLongClickListener() { @Override - public boolean onLongClick(View v) { - return handleMenuItemLongClick(menuItem); + public boolean onLongClick(View view) { + if (handleMenuItemLongClick(menuItem)) { + GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec)); + return true; + } + return false; } }); } else if (actionView instanceof MenuItemActionView) { @@ -270,7 +275,11 @@ public class GeckoMenu extends ListView ((MenuItemActionView) actionView).setMenuItemLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { - return handleMenuItemLongClick(menuItem); + if (handleMenuItemLongClick(menuItem)) { + GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getIntArray(R.array.long_press_vibrate_msec)); + return true; + } + return false; } }); } @@ -644,12 +653,15 @@ public class GeckoMenu extends ListView } boolean handleMenuItemLongClick(GeckoMenuItem item) { - if(!item.isEnabled()) { + if (!item.isEnabled()) { return false; } - if(mCallback != null) { - return mCallback.onMenuItemLongClick(item); + if (mCallback != null) { + if (mCallback.onMenuItemLongClick(item)) { + close(); + return true; + } } return false; } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 23f807f7e3f..fb9917b8a83 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -1727,6 +1727,11 @@ var BrowserApp = { // Check to see if this is a message to enable/disable mixed content blocking. if (aData) { let data = JSON.parse(aData); + + if (data.bypassCache) { + flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; + } + if (data.contentType === "tracking") { // Convert document URI into the format used by // nsChannelClassifier::ShouldEnableTrackingProtection From 49c5eb24744fd0659c233f8f8240ba4a4381f5a7 Mon Sep 17 00:00:00 2001 From: Gijs Kruitbosch Date: Fri, 25 Sep 2015 12:05:40 +0100 Subject: [PATCH 10/23] Bug 1187905 - use normal titlebar buttons for devtools light theme, r=bgrins --- browser/themes/windows/devedition.css | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/browser/themes/windows/devedition.css b/browser/themes/windows/devedition.css index caefcbb1138..ec945c22c11 100644 --- a/browser/themes/windows/devedition.css +++ b/browser/themes/windows/devedition.css @@ -285,4 +285,22 @@ :root[devtoolstheme="dark"] #titlebar-close { list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white); } + + /* ... and normal ones for the light theme on Windows 10 */ + :root[devtoolstheme="light"] #titlebar-min { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize); + } + :root[devtoolstheme="light"] #titlebar-max { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize); + } + #main-window[devtoolstheme="light"][sizemode="maximized"] #titlebar-max { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore); + } + :root[devtoolstheme="light"] #titlebar-close { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#close); + } + + :root[devtoolstheme="light"] #titlebar-close:hover { + list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white); + } } From 8592690a273852da5246ff76a7698d9032b55f9c Mon Sep 17 00:00:00 2001 From: Gijs Kruitbosch Date: Fri, 25 Sep 2015 16:17:27 +0100 Subject: [PATCH 11/23] Bug 1196144 - source window disappearance still breaks downloading to custom directories on windows, r=felipe --- toolkit/mozapps/downloads/nsHelperAppDlg.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/toolkit/mozapps/downloads/nsHelperAppDlg.js b/toolkit/mozapps/downloads/nsHelperAppDlg.js index 186fcf15a8c..cc4903cc0b6 100644 --- a/toolkit/mozapps/downloads/nsHelperAppDlg.js +++ b/toolkit/mozapps/downloads/nsHelperAppDlg.js @@ -228,7 +228,21 @@ nsUnknownContentTypeDialog.prototype = { // because the original one is definitely gone (and nsIFilePicker doesn't like // a null parent): gDownloadLastDir = this._mDownloadDir; - parent = Services.wm.getMostRecentWindow(""); + let windowsEnum = Services.wm.getEnumerator(""); + while (windowsEnum.hasMoreElements()) { + let someWin = windowsEnum.getNext(); + // We need to make sure we don't end up with this dialog, because otherwise + // that's going to go away when the user clicks "Save", and that breaks the + // windows file picker that's supposed to show up if we let the user choose + // where to save files... + if (someWin != this.mDialog) { + parent = someWin; + } + } + if (!parent) { + Cu.reportError("No candidate parent windows were found for the save filepicker." + + "This should never happen."); + } } Task.spawn(function() { From 8e3bd3727cf44b9d6099836ddaabb3e9320e15c8 Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Mon, 28 Sep 2015 15:14:53 +0100 Subject: [PATCH 12/23] Bug 1208466 - Part 1. Create a new ToS view for Loop's standalone, ready for integration into the handled-in-Firefox views. r=mikedeboer --- .../content/js/standaloneRoomViews.js | 80 +++++++++++-------- .../content/js/standaloneRoomViews.jsx | 80 +++++++++++-------- .../standalone/standaloneRoomViews_test.js | 62 ++++++++++++++ 3 files changed, 158 insertions(+), 64 deletions(-) diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.js b/browser/components/loop/standalone/content/js/standaloneRoomViews.js index 44a54db7bf5..242701ef83f 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js @@ -14,6 +14,50 @@ loop.standaloneRoomViews = (function(mozL10n) { var sharedUtils = loop.shared.utils; var sharedViews = loop.shared.views; + var ToSView = React.createClass({displayName: "ToSView", + propTypes: { + dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired + }, + + _getContent: function() { + // We use this technique of static markup as it means we get + // just one overall string for L10n to define the structure of + // the whole item. + return mozL10n.get("legal_text_and_links", { + "clientShortname": mozL10n.get("clientShortname2"), + "terms_of_use_url": React.renderToStaticMarkup( + React.createElement("a", {href: loop.config.legalWebsiteUrl, rel: "noreferrer", target: "_blank"}, + mozL10n.get("terms_of_use_link_text") + ) + ), + "privacy_notice_url": React.renderToStaticMarkup( + React.createElement("a", {href: loop.config.privacyWebsiteUrl, rel: "noreferrer", target: "_blank"}, + mozL10n.get("privacy_notice_link_text") + ) + ) + }); + }, + + recordClick: function(event) { + // Check for valid href, as this is clicking on the paragraph - + // so the user may be clicking on the text rather than the link. + if (event.target && event.target.href) { + this.props.dispatcher.dispatch(new sharedActions.RecordClick({ + linkInfo: event.target.href + })); + } + }, + + render: function() { + return ( + React.createElement("p", { + className: "terms-service", + dangerouslySetInnerHTML: {__html: this._getContent()}, + onClick: this.recordClick}) + ); + } + }); + /** * Handles display of failures, determining the correct messages and * displaying the retry button at appropriate times. @@ -306,41 +350,12 @@ loop.standaloneRoomViews = (function(mozL10n) { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired }, - _getContent: function() { - // We use this technique of static markup as it means we get - // just one overall string for L10n to define the structure of - // the whole item. - return mozL10n.get("legal_text_and_links", { - "clientShortname": mozL10n.get("clientShortname2"), - "terms_of_use_url": React.renderToStaticMarkup( - React.createElement("a", {href: loop.config.legalWebsiteUrl, rel: "noreferrer", target: "_blank"}, - mozL10n.get("terms_of_use_link_text") - ) - ), - "privacy_notice_url": React.renderToStaticMarkup( - React.createElement("a", {href: loop.config.privacyWebsiteUrl, rel: "noreferrer", target: "_blank"}, - mozL10n.get("privacy_notice_link_text") - ) - ) - }); - }, - - recordClick: function(event) { - // Check for valid href, as this is clicking on the paragraph - - // so the user may be clicking on the text rather than the link. - if (event.target && event.target.href) { - this.props.dispatcher.dispatch(new sharedActions.RecordClick({ - linkInfo: event.target.href - })); - } - }, - render: function() { return ( React.createElement("footer", {className: "rooms-footer"}, React.createElement("div", {className: "footer-logo"}), - React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()}, - onClick: this.recordClick}) + React.createElement(ToSView, { + dispatcher: this.props.dispatcher}) ) ); } @@ -601,6 +616,7 @@ loop.standaloneRoomViews = (function(mozL10n) { StandaloneRoomFooter: StandaloneRoomFooter, StandaloneRoomHeader: StandaloneRoomHeader, StandaloneRoomInfoArea: StandaloneRoomInfoArea, - StandaloneRoomView: StandaloneRoomView + StandaloneRoomView: StandaloneRoomView, + ToSView: ToSView }; })(navigator.mozL10n); diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx index 0e1bab9d4cb..63a9ac96623 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx @@ -14,6 +14,50 @@ loop.standaloneRoomViews = (function(mozL10n) { var sharedUtils = loop.shared.utils; var sharedViews = loop.shared.views; + var ToSView = React.createClass({ + propTypes: { + dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired + }, + + _getContent: function() { + // We use this technique of static markup as it means we get + // just one overall string for L10n to define the structure of + // the whole item. + return mozL10n.get("legal_text_and_links", { + "clientShortname": mozL10n.get("clientShortname2"), + "terms_of_use_url": React.renderToStaticMarkup( + + {mozL10n.get("terms_of_use_link_text")} + + ), + "privacy_notice_url": React.renderToStaticMarkup( + + {mozL10n.get("privacy_notice_link_text")} + + ) + }); + }, + + recordClick: function(event) { + // Check for valid href, as this is clicking on the paragraph - + // so the user may be clicking on the text rather than the link. + if (event.target && event.target.href) { + this.props.dispatcher.dispatch(new sharedActions.RecordClick({ + linkInfo: event.target.href + })); + } + }, + + render: function() { + return ( +

+ ); + } + }); + /** * Handles display of failures, determining the correct messages and * displaying the retry button at appropriate times. @@ -306,41 +350,12 @@ loop.standaloneRoomViews = (function(mozL10n) { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired }, - _getContent: function() { - // We use this technique of static markup as it means we get - // just one overall string for L10n to define the structure of - // the whole item. - return mozL10n.get("legal_text_and_links", { - "clientShortname": mozL10n.get("clientShortname2"), - "terms_of_use_url": React.renderToStaticMarkup( - - {mozL10n.get("terms_of_use_link_text")} - - ), - "privacy_notice_url": React.renderToStaticMarkup( - - {mozL10n.get("privacy_notice_link_text")} - - ) - }); - }, - - recordClick: function(event) { - // Check for valid href, as this is clicking on the paragraph - - // so the user may be clicking on the text rather than the link. - if (event.target && event.target.href) { - this.props.dispatcher.dispatch(new sharedActions.RecordClick({ - linkInfo: event.target.href - })); - } - }, - render: function() { return (
-

+
); } @@ -601,6 +616,7 @@ loop.standaloneRoomViews = (function(mozL10n) { StandaloneRoomFooter: StandaloneRoomFooter, StandaloneRoomHeader: StandaloneRoomHeader, StandaloneRoomInfoArea: StandaloneRoomInfoArea, - StandaloneRoomView: StandaloneRoomView + StandaloneRoomView: StandaloneRoomView, + ToSView: ToSView }; })(navigator.mozL10n); diff --git a/browser/components/loop/test/standalone/standaloneRoomViews_test.js b/browser/components/loop/test/standalone/standaloneRoomViews_test.js index 398acc32bc0..3c4643d6164 100644 --- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js +++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js @@ -49,6 +49,8 @@ describe("loop.standaloneRoomViews", function() { switch(key) { case "standalone_title_with_room_name": return args.roomName + " — " + args.clientShortname; + case "legal_text_and_links": + return args.terms_of_use_url + " " + args.privacy_notice_url; default: return key; } @@ -66,6 +68,66 @@ describe("loop.standaloneRoomViews", function() { view = null; }); + + describe("TosView", function() { + var origConfig, node; + + function mountTestComponent() { + return TestUtils.renderIntoDocument( + React.createElement( + loop.standaloneRoomViews.ToSView, { + dispatcher: dispatcher + })); + } + + beforeEach(function() { + origConfig = loop.config; + loop.config = { + legalWebsiteUrl: "http://fakelegal/", + privacyWebsiteUrl: "http://fakeprivacy/" + }; + + view = mountTestComponent(); + node = view.getDOMNode(); + }); + + afterEach(function() { + loop.config = origConfig; + }); + + it("should dispatch a link click action when the ToS link is clicked", function() { + // [0] is the first link, the legal one. + var link = node.querySelectorAll("a")[0]; + + TestUtils.Simulate.click(node, { target: link }); + + sinon.assert.calledOnce(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.RecordClick({ + linkInfo: loop.config.legalWebsiteUrl + })); + }); + + it("should dispatch a link click action when the Privacy link is clicked", function() { + // [0] is the first link, the legal one. + var link = node.querySelectorAll("a")[1]; + + TestUtils.Simulate.click(node, { target: link }); + + sinon.assert.calledOnce(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.RecordClick({ + linkInfo: loop.config.privacyWebsiteUrl + })); + }); + + it("should not dispatch an action when the text is clicked", function() { + TestUtils.Simulate.click(node, { target: node }); + + sinon.assert.notCalled(dispatcher.dispatch); + }); + }); + describe("StandaloneRoomHeader", function() { function mountTestComponent() { return TestUtils.renderIntoDocument( From 50db7bf6c867a69942f5cff152f315a1ab27b75a Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Mon, 28 Sep 2015 15:14:53 +0100 Subject: [PATCH 13/23] Bug 1208466 - Part 2. If an owner of a Loop link clicks their own link and join, make it open the conversation window. r=mikedeboer --- .../loop/content/shared/img/mozilla-logo.png | Bin 2963 -> 0 bytes .../loop/content/shared/js/actions.js | 17 + .../loop/content/shared/js/activeRoomStore.js | 286 +++++++++--- .../loop/standalone/content/css/webapp.css | 61 ++- .../content/img/hello-logo-text.svg | 1 + .../standalone/content/img/mozilla-logo.svg | 1 + .../content/js/standaloneMetricsStore.js | 18 +- .../content/js/standaloneRoomViews.js | 91 ++++ .../content/js/standaloneRoomViews.jsx | 91 ++++ .../loop/standalone/content/js/webapp.js | 2 +- .../loop/standalone/content/js/webapp.jsx | 2 +- .../content/l10n/en-US/loop.properties | 1 + .../loop/test/shared/activeRoomStore_test.js | 437 +++++++++++++++--- .../standalone/standaloneMetricsStore_test.js | 46 +- .../standalone/standaloneRoomViews_test.js | 100 ++++ .../loop/test/standalone/webapp_test.js | 4 +- browser/components/loop/ui/ui-showcase.js | 16 + browser/components/loop/ui/ui-showcase.jsx | 16 + 18 files changed, 1048 insertions(+), 142 deletions(-) delete mode 100644 browser/components/loop/content/shared/img/mozilla-logo.png create mode 100644 browser/components/loop/standalone/content/img/hello-logo-text.svg create mode 100644 browser/components/loop/standalone/content/img/mozilla-logo.svg diff --git a/browser/components/loop/content/shared/img/mozilla-logo.png b/browser/components/loop/content/shared/img/mozilla-logo.png deleted file mode 100644 index 672940c34745888fea43b9de7bd26de351c38049..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2963 zcmaJ@dpy&7AK%Q8L@tlHHfGADu$#*^L^GFZ(;6$s-GD6zn|~x{eFM0-`DGxNeS}T(K6El z0026~00LFD7OGxbO%2sor=ZdfOzi>n*f%>jy1*>D5~jzTyhus9bq4uOJx zeqbtZLM9tWCHQ^zrLsI>F(Q!w2ZtvlBse9Yo%q6NI1-D+!VxGq3gxImIL7VeiRfZS zUYzw81p+IMA>;@|96k@atVoaI$BR5+s!0D%fh+hb%ZvM5CRM@UV!8m1bV4kr^aV&J z|9>c#`xPB0qO$&p_kRk`H+15whqazA%K(-~FYElo-B< z9~Z+HKz)2ZRSjxKrZYIa9jIS>m ziS%{GyJFFJ7Xp&tpIil(Pc^-UGp{VgFS`05l%tYkzI3ET`!KS+Xj>w5EL?R-nV z5t@|m%OQgS8)&u8klAC>B~zL0k(O(cyTC5M#FMk-l;)EFII(#H zZRapR=B9lSaw9xesf~%L%}%|oCGfkvOC2*5{&=-4MfqIJPJHfDX|MM2`yPHs(M1y6 z%P|wz0kC5?6$DQ}wrrq@Z*P0}K)LXPMtkq=A-1@B3bHc%aI@Q#ls~-l`Ob)pb{vE8ahS5-a1EyuO$e(SQns3vM_Vq8iDrP1WI#J z-#YNBdhSX42!Bsto?6GvH+do>3gN}aCvz7Tff;q7igVvhY{CAPq0`l_${IZ2P& z{)%T1dP;}nPl=kh?f3!hZASukoLf~;0sBfbd!9*>P4) z^xHL0wiLU%Yy1)d;d0%W$`@Jr)Lacio8iY%-W6Ts*1BmE>7{AOV-Hek`>Au_)8@m& zscVAHz%}EIfb4~`!5&0^T+!O^(0o^U#AGrmHqmx?bqOgFr!#*7bpCg%R-1j*wx$;A z?^UV4Z1CDqEk}D4KsA4(Fbi}0VDrCXT2_8z)a5k&e z(T*Qp7`l1o-@^e*`5beds|mnEGzS{f18XRiOvcEf+ggGpeNOKrN!NBuzR&5&8G0-- zu)h;4G`g@u`H=MID$}PEF=d8x`&Uh7B>v!Se}?_D+|_jW3OzgAU}TKT4DDf=4Zmxs z4D?@&{e?aTM~!N}M5By>rWR{J?navLA=9-ddQ7^uw$zoa9Eu5=TvNQI|OlD&e%59zV-;s zqH@uHac48dm{B0jIqYy&e?HuDksR(lwCd$1>5Oi7lCf4wFnl~Xa^;dg6{>k5@Vy(j zhk5Tf@m+rJ{m6{GMoaiO-ZSm&X5nhmQGebn()d58)08DK8`CM&+~HH*YrBsrZaj!8 z%_J396>hSljh64cF?+yRu~|L&!m8@TfWx;=JqjXP`F4X07(i=4!mW`UCzp)L`=G&Qb$Gtk2k#8{NwieFtx})cBj*9waK`R+=8vK#`!{V>yao@ zCbinqyaq?4W(}ynEu&tLMiracydypxdcDp%NiQEy+bi8#UiZOpAznS1o5Fv+qXQw& z$K(d+r>{S0eAxP4|3q~Zy*gW=a3GIN>7I=btoxUZPsBi?@axQKU~^ssK5@rYOzwxHa{9tCh3G0rCn#3O@7E3z^CHDYtN9=-*{er z=Fqz*THJ0QF=J;w^XLY|bn8^U-Q}L_vv(*JFRn?mTBu2bvx#=e`#6}(aXh)AL(?*c zw)9l46i`s0qx}ZY2emBk_62EF<3X>S8MRx`vopcXdDNnq^hvj%xp0fyuk~NAK;-?F2 zk38r~44bW=lxJiXeWV`hs|mZR7#Wx=@;Fp^=+4!)Ic<^nA zPk4lFkJa6t>hKF>X%~2PBRS)8)q!Esl-ov8A^S9_s?$L>73qJ(7 z))G=7XCArJub4~{6La@pNi8wGe$He1TK8)@Y`{*owxw{R#>y#CWNQO>_HuaTtJ5U& z))z&4GCIU5qP_DbPrdAVv0EIwR)$VDpQ!0g9&3`zkMkl1r3-3_gn8#zxe&YqXoA|G z3S5Bvf$Cs-_OW^z%{@jXE39m4M^|j)-@=$@+Pt#7sUsZg!2W)+GuCALy7in}58{tl{@6 z%h+PaSk{(5`N?fRABrf|Fv`6?xxS>Q@4epO{fQV_d(eCLdd2M%r1_V&KnEdJ&~|Zc yuN>KMzjaLU$C@%&NpX+~-nsnfi^C&cYTAGtu&lAtqrhtUH<{=gL} .beta-logo { position: fixed; top: 0; @@ -43,7 +64,7 @@ body, margin: 0 auto; height: 30px; background-size: contain; - background-image: url("../shared/img/mozilla-logo.png"); + background-image: url("../img/mozilla-logo.svg#logo-white"); background-repeat: no-repeat; } @@ -138,6 +159,44 @@ html[dir="rtl"] .rooms-footer .footer-logo { line-height: 24px; } +/** + * Handle in Firefox views + */ + +.handle-user-agent-view-scroller { + height: 100%; + overflow: scroll; +} + +.handle-user-agent-view { + margin: 2rem auto; + width: 500px; +} + +.handle-user-agent-view > .info-panel { + padding-bottom: 40px; + font-size: 1.6rem; +} + +.handle-user-agent-view > p, +.handle-user-agent-view > .info-panel > p { + margin-top: 0; + margin: 2rem auto; +} + +.handle-user-agent-view > .info-panel > button { + width: 80%; + height: 4rem; + font-size: 1.6rem; + font-weight: bold; +} + +.handle-user-agent-view > .info-panel > button.disabled { + background-color: #EBEBEB; + border-color: #EBEBEB; + color: #B2B0B3; +} + /* Room wrapper layout */ .room-conversation-wrapper { diff --git a/browser/components/loop/standalone/content/img/hello-logo-text.svg b/browser/components/loop/standalone/content/img/hello-logo-text.svg new file mode 100644 index 00000000000..f79baaf8f2a --- /dev/null +++ b/browser/components/loop/standalone/content/img/hello-logo-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/browser/components/loop/standalone/content/img/mozilla-logo.svg b/browser/components/loop/standalone/content/img/mozilla-logo.svg new file mode 100644 index 00000000000..234abe10645 --- /dev/null +++ b/browser/components/loop/standalone/content/img/mozilla-logo.svg @@ -0,0 +1 @@ +Fill 1 Copy \ No newline at end of file diff --git a/browser/components/loop/standalone/content/js/standaloneMetricsStore.js b/browser/components/loop/standalone/content/js/standaloneMetricsStore.js index 5c8e4f04072..b99681b1790 100644 --- a/browser/components/loop/standalone/content/js/standaloneMetricsStore.js +++ b/browser/components/loop/standalone/content/js/standaloneMetricsStore.js @@ -44,7 +44,7 @@ loop.store.StandaloneMetricsStore = (function() { "connectedToSdkServers", "connectionFailure", "gotMediaPermission", - "joinRoom", + "metricsLogJoinRoom", "joinedRoom", "leaveRoom", "mediaConnected", @@ -144,10 +144,20 @@ loop.store.StandaloneMetricsStore = (function() { /** * Handles the user clicking the join room button. + * + * @param {sharedActions.MetricsLogJoinRoom} actionData */ - joinRoom: function() { - this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button, - "Join the conversation"); + metricsLogJoinRoom: function(actionData) { + var label; + + if (actionData.userAgentHandledRoom) { + label = actionData.ownRoom ? "Joined own room in Firefox" : + "Joined in Firefox"; + } else { + label = "Join the conversation"; + } + + this._storeEvent(METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button, label); }, /** diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.js b/browser/components/loop/standalone/content/js/standaloneRoomViews.js index 242701ef83f..ca8bb31a644 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js @@ -58,6 +58,59 @@ loop.standaloneRoomViews = (function(mozL10n) { } }); + var StandaloneHandleUserAgentView = React.createClass({displayName: "StandaloneHandleUserAgentView", + mixins: [ + loop.store.StoreMixin("activeRoomStore") + ], + + propTypes: { + dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired + }, + + getInitialState: function() { + return this.getStoreState(); + }, + + handleJoinButton: function() { + this.props.dispatcher.dispatch(new sharedActions.JoinRoom()); + }, + + render: function() { + var buttonMessage = this.state.roomState === ROOM_STATES.JOINED ? + mozL10n.get("rooms_room_joined_own_conversation_label") : + mozL10n.get("rooms_room_join_label"); + + var buttonClasses = React.addons.classSet({ + btn: true, + "btn-info": true, + disabled: this.state.roomState === ROOM_STATES.JOINED + }); + + // The extra scroller div here is for providing a scroll view for shorter + // screens, as the common.css specifies overflow:hidden for the body which + // we need in some places. + return ( + React.createElement("div", {className: "handle-user-agent-view-scroller"}, + React.createElement("div", {className: "handle-user-agent-view"}, + React.createElement("div", {className: "info-panel"}, + React.createElement("p", {className: "loop-logo-text", title: mozL10n.get("clientShortname2") }), + React.createElement("p", {className: "roomName"}, this.state.roomName), + React.createElement("p", {className: "loop-logo"}), + React.createElement("button", { + className: buttonClasses, + onClick: this.handleJoinButton}, + buttonMessage + ) + ), + React.createElement(ToSView, { + dispatcher: this.props.dispatcher}), + React.createElement("p", {className: "mozilla-logo"}) + ) + ) + ); + } + }); + /** * Handles display of failures, determining the correct messages and * displaying the retry button at appropriate times. @@ -611,7 +664,45 @@ loop.standaloneRoomViews = (function(mozL10n) { } }); + var StandaloneRoomControllerView = React.createClass({displayName: "StandaloneRoomControllerView", + mixins: [ + loop.store.StoreMixin("activeRoomStore") + ], + + propTypes: { + dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, + isFirefox: React.PropTypes.bool.isRequired + }, + + getInitialState: function() { + return this.getStoreState(); + }, + + render: function() { + // If we don't know yet, don't display anything. + if (this.state.firefoxHandlesRoom === undefined) { + return null; + } + + if (this.state.firefoxHandlesRoom) { + return ( + React.createElement(StandaloneHandleUserAgentView, { + dispatcher: this.props.dispatcher}) + ); + } + + return ( + React.createElement(StandaloneRoomView, { + activeRoomStore: this.getStore(), + dispatcher: this.props.dispatcher, + isFirefox: this.props.isFirefox}) + ); + } + }); + return { + StandaloneHandleUserAgentView: StandaloneHandleUserAgentView, + StandaloneRoomControllerView: StandaloneRoomControllerView, StandaloneRoomFailureView: StandaloneRoomFailureView, StandaloneRoomFooter: StandaloneRoomFooter, StandaloneRoomHeader: StandaloneRoomHeader, diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx index 63a9ac96623..3d6478ae1c7 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx @@ -58,6 +58,59 @@ loop.standaloneRoomViews = (function(mozL10n) { } }); + var StandaloneHandleUserAgentView = React.createClass({ + mixins: [ + loop.store.StoreMixin("activeRoomStore") + ], + + propTypes: { + dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired + }, + + getInitialState: function() { + return this.getStoreState(); + }, + + handleJoinButton: function() { + this.props.dispatcher.dispatch(new sharedActions.JoinRoom()); + }, + + render: function() { + var buttonMessage = this.state.roomState === ROOM_STATES.JOINED ? + mozL10n.get("rooms_room_joined_own_conversation_label") : + mozL10n.get("rooms_room_join_label"); + + var buttonClasses = React.addons.classSet({ + btn: true, + "btn-info": true, + disabled: this.state.roomState === ROOM_STATES.JOINED + }); + + // The extra scroller div here is for providing a scroll view for shorter + // screens, as the common.css specifies overflow:hidden for the body which + // we need in some places. + return ( +
+
+
+

+

{ this.state.roomName }

+

+ +

+ +

+

+
+ ); + } + }); + /** * Handles display of failures, determining the correct messages and * displaying the retry button at appropriate times. @@ -611,7 +664,45 @@ loop.standaloneRoomViews = (function(mozL10n) { } }); + var StandaloneRoomControllerView = React.createClass({ + mixins: [ + loop.store.StoreMixin("activeRoomStore") + ], + + propTypes: { + dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, + isFirefox: React.PropTypes.bool.isRequired + }, + + getInitialState: function() { + return this.getStoreState(); + }, + + render: function() { + // If we don't know yet, don't display anything. + if (this.state.firefoxHandlesRoom === undefined) { + return null; + } + + if (this.state.firefoxHandlesRoom) { + return ( + + ); + } + + return ( + + ); + } + }); + return { + StandaloneHandleUserAgentView: StandaloneHandleUserAgentView, + StandaloneRoomControllerView: StandaloneRoomControllerView, StandaloneRoomFailureView: StandaloneRoomFailureView, StandaloneRoomFooter: StandaloneRoomFooter, StandaloneRoomHeader: StandaloneRoomHeader, diff --git a/browser/components/loop/standalone/content/js/webapp.js b/browser/components/loop/standalone/content/js/webapp.js index ec9a3bef800..60bfea3f289 100644 --- a/browser/components/loop/standalone/content/js/webapp.js +++ b/browser/components/loop/standalone/content/js/webapp.js @@ -153,7 +153,7 @@ loop.webapp = (function(_, OT, mozL10n) { } case "room": { return ( - React.createElement(loop.standaloneRoomViews.StandaloneRoomView, { + React.createElement(loop.standaloneRoomViews.StandaloneRoomControllerView, { activeRoomStore: this.props.activeRoomStore, dispatcher: this.props.dispatcher, isFirefox: this.state.isFirefox}) diff --git a/browser/components/loop/standalone/content/js/webapp.jsx b/browser/components/loop/standalone/content/js/webapp.jsx index a01946996ef..a16254a763a 100644 --- a/browser/components/loop/standalone/content/js/webapp.jsx +++ b/browser/components/loop/standalone/content/js/webapp.jsx @@ -153,7 +153,7 @@ loop.webapp = (function(_, OT, mozL10n) { } case "room": { return ( - diff --git a/browser/components/loop/standalone/content/l10n/en-US/loop.properties b/browser/components/loop/standalone/content/l10n/en-US/loop.properties index f054ec58159..1f5ef17a14e 100644 --- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties +++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties @@ -68,6 +68,7 @@ rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} » rooms_room_joined_label=Someone has joined the conversation! rooms_room_join_label=Join the conversation +rooms_room_joined_own_conversation_label=Enjoy your conversation rooms_display_name_guest=Guest rooms_unavailable_notification_message=Sorry, you cannot join this conversation. The link may be expired or invalid. rooms_media_denied_message=We could not get access to your microphone or camera. Please reload the page to try again. diff --git a/browser/components/loop/test/shared/activeRoomStore_test.js b/browser/components/loop/test/shared/activeRoomStore_test.js index 7c6ccdf8bc5..e15b962a344 100644 --- a/browser/components/loop/test/shared/activeRoomStore_test.js +++ b/browser/components/loop/test/shared/activeRoomStore_test.js @@ -6,6 +6,7 @@ describe("loop.store.ActiveRoomStore", function () { var expect = chai.expect; var sharedActions = loop.shared.actions; + var sharedUtils = loop.shared.utils; var REST_ERRNOS = loop.shared.utils.REST_ERRNOS; var ROOM_STATES = loop.store.ROOM_STATES; var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS; @@ -434,20 +435,20 @@ describe("loop.store.ActiveRoomStore", function () { sinon.assert.calledOnce(fakeMozLoop.rooms.get); }); - it("should dispatch an UpdateRoomInfo message with 'no data' failure if neither roomName nor context are supplied", function() { + it("should dispatch an UpdateRoomInfo message with failure if neither roomName nor context are supplied", function() { fakeMozLoop.rooms.get.callsArgWith(1, null, { roomUrl: "http://invalid" }); - store.fetchServerData(fetchServerAction); - - sinon.assert.calledOnce(dispatcher.dispatch); - sinon.assert.calledWithExactly(dispatcher.dispatch, - new sharedActions.UpdateRoomInfo({ - roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA, - roomState: ROOM_STATES.READY, - roomUrl: "http://invalid" - })); + return store.fetchServerData(fetchServerAction).then(function() { + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UpdateRoomInfo({ + roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA, + roomState: ROOM_STATES.READY, + roomUrl: "http://invalid" + })); + }); }); describe("mozLoop.rooms.get returns roomName as a separate field (no context)", function() { @@ -459,13 +460,13 @@ describe("loop.store.ActiveRoomStore", function () { fakeMozLoop.rooms.get.callsArgWith(1, null, roomDetails); - store.fetchServerData(fetchServerAction); - - sinon.assert.calledOnce(dispatcher.dispatch); - sinon.assert.calledWithExactly(dispatcher.dispatch, - new sharedActions.UpdateRoomInfo(_.extend({ - roomState: ROOM_STATES.READY - }, roomDetails))); + return store.fetchServerData(fetchServerAction).then(function() { + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UpdateRoomInfo(_.extend({ + roomState: ROOM_STATES.READY + }, roomDetails))); + }); }); }); @@ -491,25 +492,25 @@ describe("loop.store.ActiveRoomStore", function () { it("should dispatch UpdateRoomInfo message with 'unsupported' failure if WebCrypto is unsupported", function() { loop.crypto.isSupported.returns(false); - store.fetchServerData(fetchServerAction); - - sinon.assert.calledOnce(dispatcher.dispatch); - sinon.assert.calledWithExactly(dispatcher.dispatch, - new sharedActions.UpdateRoomInfo(_.extend({ - roomInfoFailure: ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED, - roomState: ROOM_STATES.READY - }, expectedDetails))); + return store.fetchServerData(fetchServerAction).then(function() { + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UpdateRoomInfo(_.extend({ + roomInfoFailure: ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED, + roomState: ROOM_STATES.READY + }, expectedDetails))); + }); }); it("should dispatch UpdateRoomInfo message with 'no crypto key' failure if there is no crypto key", function() { - store.fetchServerData(fetchServerAction); - - sinon.assert.calledOnce(dispatcher.dispatch); - sinon.assert.calledWithExactly(dispatcher.dispatch, - new sharedActions.UpdateRoomInfo(_.extend({ - roomInfoFailure: ROOM_INFO_FAILURES.NO_CRYPTO_KEY, - roomState: ROOM_STATES.READY - }, expectedDetails))); + return store.fetchServerData(fetchServerAction).then(function() { + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UpdateRoomInfo(_.extend({ + roomInfoFailure: ROOM_INFO_FAILURES.NO_CRYPTO_KEY, + roomState: ROOM_STATES.READY + }, expectedDetails))); + }); }); it("should dispatch UpdateRoomInfo message with 'decrypt failed' failure if decryption failed", function() { @@ -525,14 +526,14 @@ describe("loop.store.ActiveRoomStore", function () { }; }); - store.fetchServerData(fetchServerAction); - - sinon.assert.calledOnce(dispatcher.dispatch); - sinon.assert.calledWithExactly(dispatcher.dispatch, - new sharedActions.UpdateRoomInfo(_.extend({ - roomInfoFailure: ROOM_INFO_FAILURES.DECRYPT_FAILED, - roomState: ROOM_STATES.READY - }, expectedDetails))); + return store.fetchServerData(fetchServerAction).then(function() { + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UpdateRoomInfo(_.extend({ + roomInfoFailure: ROOM_INFO_FAILURES.DECRYPT_FAILED, + roomState: ROOM_STATES.READY + }, expectedDetails))); + }); }); it("should dispatch UpdateRoomInfo message with the context if decryption was successful", function() { @@ -558,18 +559,175 @@ describe("loop.store.ActiveRoomStore", function () { }; }); - store.fetchServerData(fetchServerAction); + return store.fetchServerData(fetchServerAction).then(function() { + var expectedData = _.extend({ + roomContextUrls: roomContext.urls, + roomDescription: roomContext.description, + roomName: roomContext.roomName, + roomState: ROOM_STATES.READY + }, expectedDetails); - var expectedData = _.extend({ - roomContextUrls: roomContext.urls, - roomDescription: roomContext.description, - roomName: roomContext.roomName, - roomState: ROOM_STATES.READY - }, expectedDetails); + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UpdateRoomInfo(expectedData)); + }); + }); + }); - sinon.assert.calledOnce(dispatcher.dispatch); - sinon.assert.calledWithExactly(dispatcher.dispatch, - new sharedActions.UpdateRoomInfo(expectedData)); + describe("User Agent Room Handling", function() { + var channelListener, roomDetails; + + beforeEach(function() { + sandbox.stub(sharedUtils, "isFirefox").returns(true); + + roomDetails = { + roomName: "fakeName", + roomUrl: "http://invalid" + }; + fakeMozLoop.rooms.get.callsArgWith(1, null, roomDetails); + + sandbox.stub(window, "addEventListener", function(eventName, listener) { + if (eventName === "WebChannelMessageToContent") { + channelListener = listener; + } + }); + sandbox.stub(window, "removeEventListener", function(eventName, listener) { + if (eventName === "WebChannelMessageToContent" && + listener === channelListener) { + channelListener = null; + } + }); + }); + + it("should dispatch UserAgentHandlesRoom with false if the user agent is not Firefox", function() { + sharedUtils.isFirefox.returns(false); + + return store.fetchServerData(fetchServerAction).then(function() { + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UserAgentHandlesRoom({ + handlesRoom: false + })); + }); + }); + + it("should dispatch with false after a timeout if there is no response from the channel", function() { + // When the dispatchEvent is called, we know the setup code has run, so + // advance the timer. + sandbox.stub(window, "dispatchEvent", function() { + sandbox.clock.tick(250); + }); + + return store.fetchServerData(fetchServerAction).then(function() { + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UserAgentHandlesRoom({ + handlesRoom: false + })); + }); + }); + + it("should not dispatch if a message is returned not for the link-clicker", function() { + // When the dispatchEvent is called, we know the setup code has run, so + // advance the timer. + sandbox.stub(window, "dispatchEvent", function() { + // We call the listener twice, but the first time with an invalid id. + // Hence we should only get the dispatch once. + channelListener({ + detail: { + id: "invalid-id", + message: null + } + }); + channelListener({ + detail: { + id: "loop-link-clicker", + message: null + } + }); + }); + + return store.fetchServerData(fetchServerAction).then(function() { + // Although this is only called once for the UserAgentHandlesRoom, + // it gets called twice due to the UpdateRoomInfo. Therefore, + // we test both results here. + sinon.assert.calledTwice(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UserAgentHandlesRoom({ + handlesRoom: false + })); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UpdateRoomInfo(_.extend({ + roomState: ROOM_STATES.READY + }, roomDetails))); + }); + }); + + it("should dispatch with false if the user agent does not understand the message", function() { + // When the dispatchEvent is called, we know the setup code has run, so + // advance the timer. + sandbox.stub(window, "dispatchEvent", function() { + channelListener({ + detail: { + id: "loop-link-clicker", + message: null + } + }); + }); + + return store.fetchServerData(fetchServerAction).then(function() { + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UserAgentHandlesRoom({ + handlesRoom: false + })); + }); + }); + + it("should dispatch with false if the user agent cannot handle the message", function() { + // When the dispatchEvent is called, we know the setup code has run, so + // advance the timer. + sandbox.stub(window, "dispatchEvent", function() { + channelListener({ + detail: { + id: "loop-link-clicker", + message: { + response: false + } + } + }); + }); + + return store.fetchServerData(fetchServerAction).then(function() { + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UserAgentHandlesRoom({ + handlesRoom: false + })); + }); + }); + + it("should dispatch with true if the user agent can handle the message", function() { + // When the dispatchEvent is called, we know the setup code has run, so + // advance the timer. + sandbox.stub(window, "dispatchEvent", function() { + channelListener({ + detail: { + id: "loop-link-clicker", + message: { + response: true + } + } + }); + }); + + return store.fetchServerData(fetchServerAction).then(function() { + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.UserAgentHandlesRoom({ + handlesRoom: true + })); + }); }); }); }); @@ -624,6 +782,20 @@ describe("loop.store.ActiveRoomStore", function () { }); }); + describe("#userAgentHandlesRoom", function() { + it("should update the store state", function() { + store.setStoreState({ + UserAgentHandlesRoom: false + }); + + store.userAgentHandlesRoom(new sharedActions.UserAgentHandlesRoom({ + handlesRoom: true + })); + + expect(store.getStoreState().userAgentHandlesRoom).eql(true); + }); + }); + describe("#updateSocialShareInfo", function() { var fakeSocialShareInfo; @@ -659,32 +831,138 @@ describe("loop.store.ActiveRoomStore", function () { expect(store.getStoreState().failureReason).eql(undefined); }); - it("should set the state to MEDIA_WAIT if media devices are present", function() { - sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, true); + describe("Standalone Handles Room", function() { + it("should dispatch a MetricsLogJoinRoom action", function() { + store.joinRoom(); - store.joinRoom(); + sinon.assert.calledOnce(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.MetricsLogJoinRoom({ + userAgentHandledRoom: false + })); + }); - expect(store.getStoreState().roomState).eql(ROOM_STATES.MEDIA_WAIT); + it("should set the state to MEDIA_WAIT if media devices are present", function() { + sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, true); + + store.joinRoom(); + + expect(store.getStoreState().roomState).eql(ROOM_STATES.MEDIA_WAIT); + }); + + it("should not set the state to MEDIA_WAIT if no media devices are present", function() { + sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, false); + + store.joinRoom(); + + expect(store.getStoreState().roomState).eql(ROOM_STATES.READY); + }); + + it("should dispatch `ConnectionFailure` if no media devices are present", function() { + sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, false); + + store.joinRoom(); + + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.ConnectionFailure({ + reason: FAILURE_DETAILS.NO_MEDIA + })); + }); }); - it("should not set the state to MEDIA_WAIT if no media devices are present", function() { - sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, false); + describe("Firefox Handles Room", function() { + var channelListener; - store.joinRoom(); + beforeEach(function() { + store.setStoreState({ + userAgentHandlesRoom: true, + roomToken: "fakeToken", + standalone: true + }); - expect(store.getStoreState().roomState).eql(ROOM_STATES.READY); - }); + sandbox.stub(window, "addEventListener", function(eventName, listener) { + if (eventName === "WebChannelMessageToContent") { + channelListener = listener; + } + }); + sandbox.stub(window, "removeEventListener", function(eventName, listener) { + if (eventName === "WebChannelMessageToContent" && + listener === channelListener) { + channelListener = null; + } + }); - it("should dispatch `ConnectionFailure` if no media devices are present", function() { - sandbox.stub(loop.shared.utils, "hasAudioOrVideoDevices").callsArgWith(0, false); + sandbox.stub(console, "error"); + }); - store.joinRoom(); + it("should dispatch a MetricsLogJoinRoom action", function() { + store.joinRoom(); - sinon.assert.calledOnce(dispatcher.dispatch); - sinon.assert.calledWithExactly(dispatcher.dispatch, - new sharedActions.ConnectionFailure({ - reason: FAILURE_DETAILS.NO_MEDIA + sinon.assert.calledOnce(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.MetricsLogJoinRoom({ + userAgentHandledRoom: true, + ownRoom: true + })); + }); + + it("should dispatch an event to Firefox", function() { + sandbox.stub(window, "dispatchEvent"); + + store.joinRoom(); + + sinon.assert.calledOnce(window.dispatchEvent); + sinon.assert.calledWithExactly(window.dispatchEvent, new window.CustomEvent( + "WebChannelMessageToChrome", { + detail: { + id: "loop-link-clicker", + message: { + command: "openRoom", + roomToken: "fakeToken" + } + } })); + }); + + it("should log an error if Firefox doesn't handle the room", function() { + // Start the join. + store.joinRoom(); + + // Pretend Firefox calls back. + channelListener({ + detail: { + id: "loop-link-clicker", + message: null + } + }); + + sinon.assert.calledOnce(console.error); + }); + + it("should dispatch a JoinedRoom action if the room was successfully opened", function() { + // Start the join. + store.joinRoom(); + + // Pretend Firefox calls back. + channelListener({ + detail: { + id: "loop-link-clicker", + message: { + response: true + } + } + }); + + sinon.assert.called(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, + new sharedActions.JoinedRoom({ + apiKey: "", + sessionToken: "", + sessionId: "", + expires: 0 + })); + }); }); }); @@ -762,6 +1040,17 @@ describe("loop.store.ActiveRoomStore", function () { expect(store._storeState.roomState).eql(ROOM_STATES.JOINED); }); + it("should set the state to `JOINED` when Firefox handles the room", function() { + store.setStoreState({ + userAgentHandlesRoom: true, + standalone: true + }); + + store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData)); + + expect(store._storeState.roomState).eql(ROOM_STATES.JOINED); + }); + it("should store the session and api values", function() { store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData)); @@ -771,6 +1060,20 @@ describe("loop.store.ActiveRoomStore", function () { expect(state.sessionId).eql(fakeJoinedData.sessionId); }); + it("should not store the session and api values when Firefox handles the room", function() { + store.setStoreState({ + userAgentHandlesRoom: true, + standalone: true + }); + + store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData)); + + var state = store.getStoreState(); + expect(state.apiKey).eql(undefined); + expect(state.sessionToken).eql(undefined); + expect(state.sessionId).eql(undefined); + }); + it("should start the session connection with the sdk", function() { var actionData = new sharedActions.JoinedRoom(fakeJoinedData); diff --git a/browser/components/loop/test/standalone/standaloneMetricsStore_test.js b/browser/components/loop/test/standalone/standaloneMetricsStore_test.js index e256dbdb7c5..c264d5275ce 100644 --- a/browser/components/loop/test/standalone/standaloneMetricsStore_test.js +++ b/browser/components/loop/test/standalone/standaloneMetricsStore_test.js @@ -86,15 +86,6 @@ describe("loop.store.StandaloneMetricsStore", function() { "Media granted"); }); - it("should log an event on JoinRoom", function() { - store.joinRoom(); - - sinon.assert.calledOnce(window.ga); - sinon.assert.calledWithExactly(window.ga, - "send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button, - "Join the conversation"); - }); - it("should log an event on JoinedRoom", function() { store.joinedRoom(); @@ -150,6 +141,43 @@ describe("loop.store.StandaloneMetricsStore", function() { "send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button, "Retry failed room"); }); + + describe("MetricsLogJoinRoom", function() { + it("should log a 'Join the conversation' event if not joined by Firefox", function() { + store.metricsLogJoinRoom({ + userAgentHandledRoom: false + }); + + sinon.assert.calledOnce(window.ga); + sinon.assert.calledWithExactly(window.ga, + "send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button, + "Join the conversation"); + }); + + it("should log a 'Joined own room in Firefox' event if joining the own room in Firefox", function() { + store.metricsLogJoinRoom({ + userAgentHandledRoom: true, + ownRoom: true + }); + + sinon.assert.calledOnce(window.ga); + sinon.assert.calledWithExactly(window.ga, + "send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button, + "Joined own room in Firefox"); + }); + + it("should log a 'Joined in Firefox' event if joining a non-own room in Firefox", function() { + store.metricsLogJoinRoom({ + userAgentHandledRoom: true, + ownRoom: false + }); + + sinon.assert.calledOnce(window.ga); + sinon.assert.calledWithExactly(window.ga, + "send", "event", METRICS_GA_CATEGORY.general, METRICS_GA_ACTIONS.button, + "Joined in Firefox"); + }); + }); }); describe("Store Change Handlers", function() { diff --git a/browser/components/loop/test/standalone/standaloneRoomViews_test.js b/browser/components/loop/test/standalone/standaloneRoomViews_test.js index 3c4643d6164..d863a7adfd1 100644 --- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js +++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js @@ -128,6 +128,63 @@ describe("loop.standaloneRoomViews", function() { }); }); + describe("StandaloneHandleUserAgentView", function() { + function mountTestComponent() { + return TestUtils.renderIntoDocument( + React.createElement( + loop.standaloneRoomViews.StandaloneHandleUserAgentView, { + dispatcher: dispatcher + })); + } + + it("should display a join room button if the state is not ROOM_JOINED", function() { + activeRoomStore.setStoreState({ + roomState: ROOM_STATES.READY + }); + + view = mountTestComponent(); + var button = view.getDOMNode().querySelector(".info-panel > button"); + + expect(button.textContent).eql("rooms_room_join_label"); + }); + + it("should dispatch a JoinRoom action when the join room button is clicked", function() { + activeRoomStore.setStoreState({ + roomState: ROOM_STATES.READY + }); + + view = mountTestComponent(); + var button = view.getDOMNode().querySelector(".info-panel > button"); + + TestUtils.Simulate.click(button); + + sinon.assert.calledOnce(dispatcher.dispatch); + sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.JoinRoom()); + }); + + it("should display a enjoy your conversation button if the state is ROOM_JOINED", function() { + activeRoomStore.setStoreState({ + roomState: ROOM_STATES.JOINED + }); + + view = mountTestComponent(); + var button = view.getDOMNode().querySelector(".info-panel > button"); + + expect(button.textContent).eql("rooms_room_joined_own_conversation_label"); + }); + + it("should disable the enjoy your conversation button if the state is ROOM_JOINED", function() { + activeRoomStore.setStoreState({ + roomState: ROOM_STATES.JOINED + }); + + view = mountTestComponent(); + var button = view.getDOMNode().querySelector(".info-panel > button"); + + expect(button.classList.contains("disabled")).eql(true); + }); + }); + describe("StandaloneRoomHeader", function() { function mountTestComponent() { return TestUtils.renderIntoDocument( @@ -866,4 +923,47 @@ describe("loop.standaloneRoomViews", function() { }); }); }); + + describe("StandaloneRoomControllerView", function() { + function mountTestComponent() { + return TestUtils.renderIntoDocument( + React.createElement( + loop.standaloneRoomViews.StandaloneRoomControllerView, { + dispatcher: dispatcher, + isFirefox: true + })); + } + + it("should not display anything if it is not known if Firefox can handle the room", function() { + activeRoomStore.setStoreState({ + firefoxHandlesRoom: undefined + }); + + view = mountTestComponent(); + + expect(view.getDOMNode()).eql(null); + }); + + it("should render StandaloneHandleUserAgentView if Firefox can handle the room", function() { + activeRoomStore.setStoreState({ + firefoxHandlesRoom: true + }); + + view = mountTestComponent(); + + TestUtils.findRenderedComponentWithType(view, + loop.standaloneRoomViews.StandaloneHandleUserAgentView); + }); + + it("should render StandaloneRoomView if Firefox cannot handle the room", function() { + activeRoomStore.setStoreState({ + firefoxHandlesRoom: false + }); + + view = mountTestComponent(); + + TestUtils.findRenderedComponentWithType(view, + loop.standaloneRoomViews.StandaloneRoomView); + }); + }); }); diff --git a/browser/components/loop/test/standalone/webapp_test.js b/browser/components/loop/test/standalone/webapp_test.js index 93cd372841a..115fb6ad09b 100644 --- a/browser/components/loop/test/standalone/webapp_test.js +++ b/browser/components/loop/test/standalone/webapp_test.js @@ -119,14 +119,14 @@ describe("loop.webapp", function() { loop.webapp.UnsupportedBrowserView); }); - it("should display the StandaloneRoomView for `room` window type", + it("should display the StandaloneRoomControllerView for `room` window type", function() { standaloneAppStore.setStoreState({windowType: "room", isFirefox: true}); var webappRootView = mountTestComponent(); TestUtils.findRenderedComponentWithType(webappRootView, - loop.standaloneRoomViews.StandaloneRoomView); + loop.standaloneRoomViews.StandaloneRoomControllerView); }); it("should display the HomeView for `home` window type", function() { diff --git a/browser/components/loop/ui/ui-showcase.js b/browser/components/loop/ui/ui-showcase.js index 6960832228a..12fd2441c33 100644 --- a/browser/components/loop/ui/ui-showcase.js +++ b/browser/components/loop/ui/ui-showcase.js @@ -36,6 +36,7 @@ var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView; var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView; var StandaloneRoomView = loop.standaloneRoomViews.StandaloneRoomView; + var StandaloneHandleUserAgentView = loop.standaloneRoomViews.StandaloneHandleUserAgentView; // 3. Shared components var ConversationToolbar = loop.shared.views.ConversationToolbar; @@ -1515,6 +1516,21 @@ ) ), + React.createElement(Section, {name: "StandaloneHandleUserAgentView"}, + React.createElement(FramedExample, { + cssClass: "standalone", + dashed: true, + height: 483, + summary: "Standalone Room Handle Join in Firefox", + width: 644}, + React.createElement("div", {className: "standalone"}, + React.createElement(StandaloneHandleUserAgentView, { + activeRoomStore: readyRoomStore, + dispatcher: dispatcher}) + ) + ) + ), + React.createElement(Section, {name: "StandaloneRoomView"}, React.createElement(FramedExample, {cssClass: "standalone", dashed: true, diff --git a/browser/components/loop/ui/ui-showcase.jsx b/browser/components/loop/ui/ui-showcase.jsx index 3b7dbdd279d..8b080e7fe46 100644 --- a/browser/components/loop/ui/ui-showcase.jsx +++ b/browser/components/loop/ui/ui-showcase.jsx @@ -36,6 +36,7 @@ var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView; var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView; var StandaloneRoomView = loop.standaloneRoomViews.StandaloneRoomView; + var StandaloneHandleUserAgentView = loop.standaloneRoomViews.StandaloneHandleUserAgentView; // 3. Shared components var ConversationToolbar = loop.shared.views.ConversationToolbar; @@ -1515,6 +1516,21 @@ +
+ +
+ +
+
+
+
Date: Mon, 28 Sep 2015 11:06:37 -0400 Subject: [PATCH 14/23] Bug 1201076 - Don't clobber the page rect update if one comes in while in the middle of a bounce animation. r=snorp --- mobile/android/base/gfx/ImmutableViewportMetrics.java | 11 +++++++++++ mobile/android/base/gfx/JavaPanZoomController.java | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/mobile/android/base/gfx/ImmutableViewportMetrics.java b/mobile/android/base/gfx/ImmutableViewportMetrics.java index fa51a52ef9f..9317a3aa373 100644 --- a/mobile/android/base/gfx/ImmutableViewportMetrics.java +++ b/mobile/android/base/gfx/ImmutableViewportMetrics.java @@ -219,6 +219,17 @@ public class ImmutableViewportMetrics { zoomFactor, isRTL); } + public ImmutableViewportMetrics setPageRectFrom(ImmutableViewportMetrics aMetrics) { + if (aMetrics.cssPageRectLeft == cssPageRectLeft && + aMetrics.cssPageRectTop == cssPageRectTop && + aMetrics.cssPageRectRight == cssPageRectRight && + aMetrics.cssPageRectBottom == cssPageRectBottom) { + return this; + } + RectF css = aMetrics.getCssPageRect(); + return setPageRect(RectUtils.scale(css, zoomFactor), css); + } + public ImmutableViewportMetrics setIsRTL(boolean aIsRTL) { if (isRTL == aIsRTL) { return this; diff --git a/mobile/android/base/gfx/JavaPanZoomController.java b/mobile/android/base/gfx/JavaPanZoomController.java index 0cb0bd00ae6..897746b8fa5 100644 --- a/mobile/android/base/gfx/JavaPanZoomController.java +++ b/mobile/android/base/gfx/JavaPanZoomController.java @@ -950,14 +950,14 @@ class JavaPanZoomController synchronized (mTarget.getLock()) { float t = easeOut((float)mBounceDuration / BOUNCE_ANIMATION_DURATION); ImmutableViewportMetrics newMetrics = mBounceStartMetrics.interpolate(mBounceEndMetrics, t); - mTarget.setViewportMetrics(newMetrics); + mTarget.setViewportMetrics(newMetrics.setPageRectFrom(getMetrics())); } } /* Concludes a bounce animation and snaps the viewport into place. */ private void finishBounce() { synchronized (mTarget.getLock()) { - mTarget.setViewportMetrics(mBounceEndMetrics); + mTarget.setViewportMetrics(mBounceEndMetrics.setPageRectFrom(getMetrics())); } } } From c33c3d4761c936d1ae7972b1c389b5ee186900a9 Mon Sep 17 00:00:00 2001 From: Mark Banner Date: Mon, 28 Sep 2015 16:08:27 +0100 Subject: [PATCH 15/23] Bug 1208466 - Part 3. Fix display when opening room, and make the opened room text non-bold. r=mikedeboer --- browser/components/loop/standalone/content/css/webapp.css | 1 + .../loop/standalone/content/js/standaloneRoomViews.js | 4 ++-- .../loop/standalone/content/js/standaloneRoomViews.jsx | 4 ++-- .../loop/test/standalone/standaloneRoomViews_test.js | 6 +++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/browser/components/loop/standalone/content/css/webapp.css b/browser/components/loop/standalone/content/css/webapp.css index 2dca984c61f..cec66f461dd 100644 --- a/browser/components/loop/standalone/content/css/webapp.css +++ b/browser/components/loop/standalone/content/css/webapp.css @@ -195,6 +195,7 @@ html[dir="rtl"] .rooms-footer .footer-logo { background-color: #EBEBEB; border-color: #EBEBEB; color: #B2B0B3; + font-weight: normal; } /* Room wrapper layout */ diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.js b/browser/components/loop/standalone/content/js/standaloneRoomViews.js index ca8bb31a644..6723f6c5626 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js @@ -680,11 +680,11 @@ loop.standaloneRoomViews = (function(mozL10n) { render: function() { // If we don't know yet, don't display anything. - if (this.state.firefoxHandlesRoom === undefined) { + if (this.state.userAgentHandlesRoom === undefined) { return null; } - if (this.state.firefoxHandlesRoom) { + if (this.state.userAgentHandlesRoom) { return ( React.createElement(StandaloneHandleUserAgentView, { dispatcher: this.props.dispatcher}) diff --git a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx index 3d6478ae1c7..d08db3f4cc6 100644 --- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx +++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx @@ -680,11 +680,11 @@ loop.standaloneRoomViews = (function(mozL10n) { render: function() { // If we don't know yet, don't display anything. - if (this.state.firefoxHandlesRoom === undefined) { + if (this.state.userAgentHandlesRoom === undefined) { return null; } - if (this.state.firefoxHandlesRoom) { + if (this.state.userAgentHandlesRoom) { return ( diff --git a/browser/components/loop/test/standalone/standaloneRoomViews_test.js b/browser/components/loop/test/standalone/standaloneRoomViews_test.js index d863a7adfd1..7f7f78e8f3d 100644 --- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js +++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js @@ -936,7 +936,7 @@ describe("loop.standaloneRoomViews", function() { it("should not display anything if it is not known if Firefox can handle the room", function() { activeRoomStore.setStoreState({ - firefoxHandlesRoom: undefined + userAgentHandlesRoom: undefined }); view = mountTestComponent(); @@ -946,7 +946,7 @@ describe("loop.standaloneRoomViews", function() { it("should render StandaloneHandleUserAgentView if Firefox can handle the room", function() { activeRoomStore.setStoreState({ - firefoxHandlesRoom: true + userAgentHandlesRoom: true }); view = mountTestComponent(); @@ -957,7 +957,7 @@ describe("loop.standaloneRoomViews", function() { it("should render StandaloneRoomView if Firefox cannot handle the room", function() { activeRoomStore.setStoreState({ - firefoxHandlesRoom: false + userAgentHandlesRoom: false }); view = mountTestComponent(); From fae317d4f71c3811e0961033619b72e364a2d8ee Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Fri, 25 Sep 2015 11:17:42 -0700 Subject: [PATCH 16/23] Bug 1208534 - Ensure about:logins animated CSS spinner is painted before janky main-thread load. r=ally Right now, in response to "load" (on the window), we're: 1) updating the DOM to show the spinner; 2) loading the logins with a main-thread janking synchronous load; 3) updating the DOM to hide the spinner. This is all on the main-thread, so we only see a layout and paint after 3). Thus no interstitial is ever visible, and the logins list pops in after a long delay. This patch ensures that 2) occurs at least one layout after 1). This allows a paint to occur with the interstitial visible. Since the animated CSS spinner is carefully designed to hit the off-main-thread animation pipeline, it animates smoothly even though the main-thread janking synchronous load blocks JavaScript progress. There is a small race window between the promises resolving and the _logins member being accessed by the filter. It's not clear that this was ever well guarded, so I haven't tried to mitigate. --- mobile/android/chrome/content/aboutLogins.js | 119 ++++++++++++++----- 1 file changed, 87 insertions(+), 32 deletions(-) diff --git a/mobile/android/chrome/content/aboutLogins.js b/mobile/android/chrome/content/aboutLogins.js index 21bce0f23a3..24b9eeea0d7 100644 --- a/mobile/android/chrome/content/aboutLogins.js +++ b/mobile/android/chrome/content/aboutLogins.js @@ -4,8 +4,9 @@ var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils; +Cu.import("resource://services-common/utils.js"); /*global: CommonUtils */ Cu.import("resource://gre/modules/Messaging.jsm"); -Cu.import("resource://gre/modules/Services.jsm") +Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/TelemetryStopwatch.jsm"); @@ -49,39 +50,95 @@ var Logins = { _filterTimer: null, _selectedLogin: null, - _getLogins: function() { - let logins; + // Load the logins list, displaying interstitial UI (see + // #logins-list-loading-body) while loading. There are careful + // jank-avoiding measures taken in this function; be careful when + // modifying it! + // + // Returns a Promise that resolves to the list of logins, ordered by + // hostname. + _promiseLogins: function() { let contentBody = document.getElementById("content-body"); let emptyBody = document.getElementById("empty-body"); let filterIcon = document.getElementById("filter-button"); - this._toggleListBody(true); - emptyBody.classList.add("hidden"); - - try { - TelemetryStopwatch.start("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS"); - logins = Services.logins.getAllLogins(); - TelemetryStopwatch.finish("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS"); - } catch(e) { - // Master password was not entered - debug("Master password permissions error: " + e); - logins = []; - } - this._toggleListBody(false); - - if (!logins.length) { - emptyBody.classList.remove("hidden"); - - filterIcon.classList.add("hidden"); - contentBody.classList.add("hidden"); - } else { + let showSpinner = () => { + this._toggleListBody(true); emptyBody.classList.add("hidden"); + }; - filterIcon.classList.remove("hidden"); - } + let getAllLogins = () => { + try { + TelemetryStopwatch.start("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS"); + let logins = Services.logins.getAllLogins(); + TelemetryStopwatch.finish("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS"); + } catch(e) { + // It's likely that the Master Password was not entered; give + // a hint to the next person. + throw new Error("Possible Master Password permissions error: " + e.toString()); + } - logins.sort((a, b) => a.hostname.localeCompare(b.hostname)); - return this._logins = logins; + logins.sort((a, b) => a.hostname.localeCompare(b.hostname)); + + return logins; + }; + + let hideSpinner = (logins) => { + this._toggleListBody(false); + + if (!logins.length) { + contentBody.classList.add("hidden"); + filterIcon.classList.add("hidden"); + emptyBody.classList.remove("hidden"); + } else { + contentBody.classList.remove("hidden"); + emptyBody.classList.add("hidden"); + } + + return logins; + }; + + // Return a promise that is resolved after a paint. + let waitForPaint = () => { + // We're changing 'display'. We need to wait for the new value to take + // effect; otherwise, we'll block and never paint a change. Since + // requestAnimationFrame callback is generally triggered *before* any + // style flush and layout, we wait for two animation frames. This + // approach was cribbed from + // https://dxr.mozilla.org/mozilla-central/rev/5abe3c4deab94270440422c850bbeaf512b1f38d/browser/base/content/browser-fullScreen.js?offset=0#469. + return new Promise(function(resolve, reject) { + requestAnimationFrame(() => { + requestAnimationFrame(() => { + resolve(); + }); + }); + }); + }; + + // getAllLogins janks the main-thread. We need to paint before that jank; + // by throwing the janky load onto the next tick, we paint the spinner; the + // spinner is CSS animated off-main-thread. + return Promise.resolve() + .then(showSpinner) + .then(waitForPaint) + .then(getAllLogins) + .then(hideSpinner); + }, + + // Reload the logins list, displaying interstitial UI while loading. + // Update the stored and displayed list upon completion. + _reloadList: function() { + this._promiseLogins() + .then((logins) => { + this._logins = logins; + this._loadList(logins); + }) + .catch((e) => { + // There's no way to recover from errors, sadly. Log and make + // it obvious that something is up. + this._logins = []; + debug("Failed to _reloadList: " + e.toString()); + }); }, _toggleListBody: function(isLoading) { @@ -95,7 +152,6 @@ var Logins = { loadingBody.classList.add("hidden"); contentBody.classList.remove("hidden"); } - }, init: function () { @@ -105,8 +161,6 @@ var Logins = { document.getElementById("update-btn").addEventListener("click", this._onSaveEditLogin.bind(this), false); document.getElementById("password-btn").addEventListener("click", this._onPasswordBtn.bind(this), false); - this._loadList(this._getLogins()); - let filterInput = document.getElementById("filter-input"); let filterContainer = document.getElementById("filter-input-container"); @@ -147,6 +201,8 @@ var Logins = { this._showList(); this._updatePasswordBtn(true); + + this._reloadList(); }, uninit: function () { @@ -439,8 +495,7 @@ var Logins = { observe: function (subject, topic, data) { switch(topic) { case "passwordmgr-storage-changed": { - // Reload logins content. - this._loadList(this._getLogins()); + this._reloadList(); break; } } From 676e855b47acb93347fef99b25be256b24f4a17a Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 28 Sep 2015 04:35:19 -0700 Subject: [PATCH 17/23] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/a798b3c0180a Author: albertopq Desc: Merge pull request #32033 from albertopq/1207928-long-press-link2 Bug 1207928 - Stopping event propagation before showing the context menu r=etienne ======== https://hg.mozilla.org/integration/gaia-central/rev/023466de5b56 Author: albertopq Desc: Bug 1207928 - Stopping event propagation before showing the context menu --- b2g/config/gaia.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 5d5cf5620e8..dde6c8e9f40 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,9 +1,9 @@ { "git": { - "git_revision": "01ffe82cf088ca8fda9fe6783dc5cad2c3dde01c", + "git_revision": "64cbcb532b538b36e92d99f66833a208e4a62e3e", "remote": "https://git.mozilla.org/releases/gaia.git", "branch": "" }, - "revision": "d7e928f87e2cc34121db52e65f2eeb7598a01412", + "revision": "a798b3c0180a37343f787b29ed73c29a758ba6fb", "repo_path": "integration/gaia-central" } From ccd29dc7931ae1f09a495adb4952a1ec82364ac3 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 28 Sep 2015 04:38:11 -0700 Subject: [PATCH 18/23] Bumping manifests a=b2g-bump --- b2g/config/aries/sources.xml | 2 +- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator-l/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/nexus-4-kk/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/nexus-5-l/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/aries/sources.xml b/b2g/config/aries/sources.xml index abf6d774bc0..5bb294435d1 100644 --- a/b2g/config/aries/sources.xml +++ b/b2g/config/aries/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index f9e883380b0..94f7d366309 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index e2e0dfcbd69..296e642e10c 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 2cd389d6f37..f5027e16fd4 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 197fe7f496d..28515daaa98 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-l/sources.xml b/b2g/config/emulator-l/sources.xml index acc41da23b6..606e9f8a9b3 100644 --- a/b2g/config/emulator-l/sources.xml +++ b/b2g/config/emulator-l/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index e2e0dfcbd69..296e642e10c 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index 1b75dfe60c5..c2f8a22bd40 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4-kk/sources.xml b/b2g/config/nexus-4-kk/sources.xml index 1a90a387437..1f820ea6f9e 100644 --- a/b2g/config/nexus-4-kk/sources.xml +++ b/b2g/config/nexus-4-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index fb2a13ec0b8..489a8b5b3da 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -18,7 +18,7 @@ - + diff --git a/b2g/config/nexus-5-l/sources.xml b/b2g/config/nexus-5-l/sources.xml index 1387f5e541e..980d2f28b64 100644 --- a/b2g/config/nexus-5-l/sources.xml +++ b/b2g/config/nexus-5-l/sources.xml @@ -15,7 +15,7 @@ - + From 322acba77c43f881d7c767b92fcc68ef701cdbd4 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 28 Sep 2015 05:00:25 -0700 Subject: [PATCH 19/23] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/74e47ac66811 Author: albertopq Desc: Merge pull request #32006 from albertopq/1168962-expand-collapse Bug 1168962 - Expand/Collapse Browser Chrome on Navigation r=etienne ======== https://hg.mozilla.org/integration/gaia-central/rev/f4debf00d50f Author: albertopq Desc: Bug 1168962 - Expand/Collapse Browser Chrome on Navigation --- b2g/config/gaia.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index dde6c8e9f40..e82829fcecb 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,9 +1,9 @@ { "git": { - "git_revision": "64cbcb532b538b36e92d99f66833a208e4a62e3e", + "git_revision": "c1cccd5e74373c9f93f9505386a70daf3f566ae0", "remote": "https://git.mozilla.org/releases/gaia.git", "branch": "" }, - "revision": "a798b3c0180a37343f787b29ed73c29a758ba6fb", + "revision": "74e47ac66811e8c7392c56dc84c2deb50562b5fa", "repo_path": "integration/gaia-central" } From c2819b6a1bdc8a7f9a19d650aab7d14e03995e9d Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 28 Sep 2015 05:03:19 -0700 Subject: [PATCH 20/23] Bumping manifests a=b2g-bump --- b2g/config/aries/sources.xml | 2 +- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator-l/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/nexus-4-kk/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/nexus-5-l/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/aries/sources.xml b/b2g/config/aries/sources.xml index 5bb294435d1..edb2d83efa7 100644 --- a/b2g/config/aries/sources.xml +++ b/b2g/config/aries/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index 94f7d366309..1f95e9a9e40 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 296e642e10c..bae88c1f491 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index f5027e16fd4..3965e3aeecb 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 28515daaa98..5141fd45124 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-l/sources.xml b/b2g/config/emulator-l/sources.xml index 606e9f8a9b3..fd0f06f7bd1 100644 --- a/b2g/config/emulator-l/sources.xml +++ b/b2g/config/emulator-l/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 296e642e10c..bae88c1f491 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index c2f8a22bd40..7e9a8aa6162 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4-kk/sources.xml b/b2g/config/nexus-4-kk/sources.xml index 1f820ea6f9e..850e9d27d09 100644 --- a/b2g/config/nexus-4-kk/sources.xml +++ b/b2g/config/nexus-4-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 489a8b5b3da..c3cfdcbd583 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -18,7 +18,7 @@ - + diff --git a/b2g/config/nexus-5-l/sources.xml b/b2g/config/nexus-5-l/sources.xml index 980d2f28b64..85628c1a243 100644 --- a/b2g/config/nexus-5-l/sources.xml +++ b/b2g/config/nexus-5-l/sources.xml @@ -15,7 +15,7 @@ - + From 2aaae7ac8ab0e861cbf640d596430d3fe37e44b9 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 28 Sep 2015 07:35:19 -0700 Subject: [PATCH 21/23] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/61c5a1255e15 Author: No-Jun Park Desc: Merge pull request #32069 from npark-mozilla/1208625 Bug 1208625 - update _cell_broadcast_switch_locator since it's a gaia-switch now ======== https://hg.mozilla.org/integration/gaia-central/rev/4bd95a7df029 Author: No-Jun Park Desc: Bug 1208625 - update _cell_broadcast_switch_locator since it's a gaia-switch now --- b2g/config/gaia.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index e82829fcecb..cb968b8bef7 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -1,9 +1,9 @@ { "git": { - "git_revision": "c1cccd5e74373c9f93f9505386a70daf3f566ae0", + "git_revision": "4c0a6d4e8501368db8e5d6029a41db985ef1252a", "remote": "https://git.mozilla.org/releases/gaia.git", "branch": "" }, - "revision": "74e47ac66811e8c7392c56dc84c2deb50562b5fa", + "revision": "61c5a1255e159b89caebf736d3c009a3778a5c42", "repo_path": "integration/gaia-central" } From 0961e3789d9d07e14a8ab6ecf64e49c4e888f380 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Mon, 28 Sep 2015 07:38:10 -0700 Subject: [PATCH 22/23] Bumping manifests a=b2g-bump --- b2g/config/aries/sources.xml | 2 +- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator-l/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/nexus-4-kk/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/nexus-5-l/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/aries/sources.xml b/b2g/config/aries/sources.xml index edb2d83efa7..5284e735dee 100644 --- a/b2g/config/aries/sources.xml +++ b/b2g/config/aries/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index 1f95e9a9e40..9145de9dfbc 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index bae88c1f491..d3dfb165f08 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 3965e3aeecb..d1d0edc1252 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 5141fd45124..b7f6f594224 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-l/sources.xml b/b2g/config/emulator-l/sources.xml index fd0f06f7bd1..570cb6c5213 100644 --- a/b2g/config/emulator-l/sources.xml +++ b/b2g/config/emulator-l/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index bae88c1f491..d3dfb165f08 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index 7e9a8aa6162..2470842cc5b 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4-kk/sources.xml b/b2g/config/nexus-4-kk/sources.xml index 850e9d27d09..596bbe36273 100644 --- a/b2g/config/nexus-4-kk/sources.xml +++ b/b2g/config/nexus-4-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index c3cfdcbd583..186f2f067a3 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -18,7 +18,7 @@ - + diff --git a/b2g/config/nexus-5-l/sources.xml b/b2g/config/nexus-5-l/sources.xml index 85628c1a243..239bfdbd248 100644 --- a/b2g/config/nexus-5-l/sources.xml +++ b/b2g/config/nexus-5-l/sources.xml @@ -15,7 +15,7 @@ - + From ff97c75b91cc5199d2e6bb769064a291b4f217f7 Mon Sep 17 00:00:00 2001 From: Wes Kocher Date: Mon, 28 Sep 2015 12:09:26 -0700 Subject: [PATCH 23/23] Backed out changeset 161d0b4fcccd (bug 1208534) for test_about_logins failures CLOSED TREE --- mobile/android/chrome/content/aboutLogins.js | 119 +++++-------------- 1 file changed, 32 insertions(+), 87 deletions(-) diff --git a/mobile/android/chrome/content/aboutLogins.js b/mobile/android/chrome/content/aboutLogins.js index 24b9eeea0d7..21bce0f23a3 100644 --- a/mobile/android/chrome/content/aboutLogins.js +++ b/mobile/android/chrome/content/aboutLogins.js @@ -4,9 +4,8 @@ var Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils; -Cu.import("resource://services-common/utils.js"); /*global: CommonUtils */ Cu.import("resource://gre/modules/Messaging.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Services.jsm") Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/TelemetryStopwatch.jsm"); @@ -50,95 +49,39 @@ var Logins = { _filterTimer: null, _selectedLogin: null, - // Load the logins list, displaying interstitial UI (see - // #logins-list-loading-body) while loading. There are careful - // jank-avoiding measures taken in this function; be careful when - // modifying it! - // - // Returns a Promise that resolves to the list of logins, ordered by - // hostname. - _promiseLogins: function() { + _getLogins: function() { + let logins; let contentBody = document.getElementById("content-body"); let emptyBody = document.getElementById("empty-body"); let filterIcon = document.getElementById("filter-button"); - let showSpinner = () => { - this._toggleListBody(true); + this._toggleListBody(true); + emptyBody.classList.add("hidden"); + + try { + TelemetryStopwatch.start("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS"); + logins = Services.logins.getAllLogins(); + TelemetryStopwatch.finish("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS"); + } catch(e) { + // Master password was not entered + debug("Master password permissions error: " + e); + logins = []; + } + this._toggleListBody(false); + + if (!logins.length) { + emptyBody.classList.remove("hidden"); + + filterIcon.classList.add("hidden"); + contentBody.classList.add("hidden"); + } else { emptyBody.classList.add("hidden"); - }; - let getAllLogins = () => { - try { - TelemetryStopwatch.start("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS"); - let logins = Services.logins.getAllLogins(); - TelemetryStopwatch.finish("PWMGR_ABOUT_LOGINS_GET_ALL_LOGINS_MS"); - } catch(e) { - // It's likely that the Master Password was not entered; give - // a hint to the next person. - throw new Error("Possible Master Password permissions error: " + e.toString()); - } + filterIcon.classList.remove("hidden"); + } - logins.sort((a, b) => a.hostname.localeCompare(b.hostname)); - - return logins; - }; - - let hideSpinner = (logins) => { - this._toggleListBody(false); - - if (!logins.length) { - contentBody.classList.add("hidden"); - filterIcon.classList.add("hidden"); - emptyBody.classList.remove("hidden"); - } else { - contentBody.classList.remove("hidden"); - emptyBody.classList.add("hidden"); - } - - return logins; - }; - - // Return a promise that is resolved after a paint. - let waitForPaint = () => { - // We're changing 'display'. We need to wait for the new value to take - // effect; otherwise, we'll block and never paint a change. Since - // requestAnimationFrame callback is generally triggered *before* any - // style flush and layout, we wait for two animation frames. This - // approach was cribbed from - // https://dxr.mozilla.org/mozilla-central/rev/5abe3c4deab94270440422c850bbeaf512b1f38d/browser/base/content/browser-fullScreen.js?offset=0#469. - return new Promise(function(resolve, reject) { - requestAnimationFrame(() => { - requestAnimationFrame(() => { - resolve(); - }); - }); - }); - }; - - // getAllLogins janks the main-thread. We need to paint before that jank; - // by throwing the janky load onto the next tick, we paint the spinner; the - // spinner is CSS animated off-main-thread. - return Promise.resolve() - .then(showSpinner) - .then(waitForPaint) - .then(getAllLogins) - .then(hideSpinner); - }, - - // Reload the logins list, displaying interstitial UI while loading. - // Update the stored and displayed list upon completion. - _reloadList: function() { - this._promiseLogins() - .then((logins) => { - this._logins = logins; - this._loadList(logins); - }) - .catch((e) => { - // There's no way to recover from errors, sadly. Log and make - // it obvious that something is up. - this._logins = []; - debug("Failed to _reloadList: " + e.toString()); - }); + logins.sort((a, b) => a.hostname.localeCompare(b.hostname)); + return this._logins = logins; }, _toggleListBody: function(isLoading) { @@ -152,6 +95,7 @@ var Logins = { loadingBody.classList.add("hidden"); contentBody.classList.remove("hidden"); } + }, init: function () { @@ -161,6 +105,8 @@ var Logins = { document.getElementById("update-btn").addEventListener("click", this._onSaveEditLogin.bind(this), false); document.getElementById("password-btn").addEventListener("click", this._onPasswordBtn.bind(this), false); + this._loadList(this._getLogins()); + let filterInput = document.getElementById("filter-input"); let filterContainer = document.getElementById("filter-input-container"); @@ -201,8 +147,6 @@ var Logins = { this._showList(); this._updatePasswordBtn(true); - - this._reloadList(); }, uninit: function () { @@ -495,7 +439,8 @@ var Logins = { observe: function (subject, topic, data) { switch(topic) { case "passwordmgr-storage-changed": { - this._reloadList(); + // Reload logins content. + this._loadList(this._getLogins()); break; } }