Bug 1131413 (part 2) - add readinglist support to browser-syncui.js. r=adw

This commit is contained in:
Mark Hammond 2015-02-26 18:48:11 +11:00
parent 778da3616b
commit 2b4c3a5df8
4 changed files with 328 additions and 49 deletions

View File

@ -11,13 +11,19 @@ XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
let CloudSync = null;
#endif
XPCOMUtils.defineLazyModuleGetter(this, "ReadingListScheduler",
"resource:///modules/readinglist/Scheduler.jsm");
// gSyncUI handles updating the tools menu and displaying notifications.
let gSyncUI = {
_obs: ["weave:service:sync:start",
"weave:service:sync:finish",
"weave:service:sync:error",
"weave:service:quota:remaining",
"weave:service:setup-complete",
"weave:service:login:start",
"weave:service:login:finish",
"weave:service:login:error",
"weave:service:logout:finish",
"weave:service:start-over",
"weave:service:start-over:finish",
@ -25,9 +31,15 @@ let gSyncUI = {
"weave:ui:sync:error",
"weave:ui:sync:finish",
"weave:ui:clear-error",
"readinglist:sync:start",
"readinglist:sync:finish",
"readinglist:sync:error",
],
_unloaded: false,
// The number of "active" syncs - while this is non-zero, our button will spin
_numActiveSyncTasks: 0,
init: function () {
Cu.import("resource://services-common/stringbundle.js");
@ -95,21 +107,25 @@ let gSyncUI = {
}
},
_needsSetup: function SUI__needsSetup() {
_needsSetup() {
// We want to treat "account needs verification" as "needs setup". So
// "reach in" to Weave.Status._authManager to check whether we the signed-in
// user is verified.
// Referencing Weave.Status spins a nested event loop to initialize the
// authManager, so this should always return a value directly.
// This only applies to fxAccounts-based Sync.
if (Weave.Status._authManager._signedInUser) {
// If we have a signed in user already, and that user is not verified,
// revert to the "needs setup" state.
if (!Weave.Status._authManager._signedInUser.verified) {
return true;
}
if (Weave.Status._authManager._signedInUser !== undefined) {
// So we are using Firefox accounts - in this world, checking Sync isn't
// enough as reading list may be configured but not Sync.
// We consider ourselves setup if we have a verified user.
// XXX - later we should consider checking preferences to ensure at least
// one engine is enabled?
return !Weave.Status._authManager._signedInUser ||
!Weave.Status._authManager._signedInUser.verified;
}
// So we are using legacy sync, and reading-list isn't supported for such
// users, so check sync itself.
let firstSync = "";
try {
firstSync = Services.prefs.getCharPref("services.sync.firstSync");
@ -120,7 +136,8 @@ let gSyncUI = {
},
_loginFailed: function () {
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED ||
ReadingListScheduler.state == ReadingListScheduler.STATE_ERROR_AUTHENTICATION;
},
updateUI: function SUI_updateUI() {
@ -136,6 +153,7 @@ let gSyncUI = {
document.getElementById("sync-syncnow-state").hidden = false;
} else if (loginFailed) {
document.getElementById("sync-reauth-state").hidden = false;
this.showLoginError();
} else if (needsSetup) {
document.getElementById("sync-setup-state").hidden = false;
} else {
@ -146,14 +164,6 @@ let gSyncUI = {
return;
let syncButton = document.getElementById("sync-button");
if (syncButton) {
syncButton.removeAttribute("status");
}
let panelHorizontalButton = document.getElementById("PanelUI-fxa-status");
if (panelHorizontalButton) {
panelHorizontalButton.removeAttribute("syncstatus");
}
if (needsSetup && syncButton)
syncButton.removeAttribute("tooltiptext");
@ -162,17 +172,45 @@ let gSyncUI = {
// Functions called by observers
onActivityStart: function SUI_onActivityStart() {
onActivityStart() {
if (!gBrowser)
return;
let button = document.getElementById("sync-button");
if (button) {
button.setAttribute("status", "active");
this.log.debug("onActivityStart with numActive", this._numActiveSyncTasks);
if (++this._numActiveSyncTasks == 1) {
let button = document.getElementById("sync-button");
if (button) {
button.setAttribute("status", "active");
}
button = document.getElementById("PanelUI-fxa-status");
if (button) {
button.setAttribute("syncstatus", "active");
}
}
button = document.getElementById("PanelUI-fxa-status");
if (button) {
button.setAttribute("syncstatus", "active");
},
onActivityStop() {
if (!gBrowser)
return;
this.log.debug("onActivityStop with numActive", this._numActiveSyncTasks);
if (--this._numActiveSyncTasks) {
if (this._numActiveSyncTasks < 0) {
// This isn't particularly useful (it seems more likely we'll set a
// "start" without a "stop" meaning it forever remains > 0) but it
// might offer some value...
this.log.error("mismatched onActivityStart/Stop calls",
new Error("active=" + this._numActiveSyncTasks));
}
return; // active tasks are still ongoing...
}
let syncButton = document.getElementById("sync-button");
if (syncButton) {
syncButton.removeAttribute("status");
}
let panelHorizontalButton = document.getElementById("PanelUI-fxa-status");
if (panelHorizontalButton) {
panelHorizontalButton.removeAttribute("syncstatus");
}
},
@ -187,6 +225,7 @@ let gSyncUI = {
},
onLoginError: function SUI_onLoginError() {
// Note: This is used for *both* Sync and ReadingList login errors.
// if login fails, any other notifications are essentially moot
Weave.Notifications.removeAll();
@ -200,11 +239,18 @@ let gSyncUI = {
this.updateUI();
return;
}
this.showLoginError();
this.updateUI();
},
showLoginError() {
// Note: This is used for *both* Sync and ReadingList login errors.
let title = this._stringBundle.GetStringFromName("error.login.title");
let description;
if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) {
if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE ||
this.isProlongedReadingListError()) {
this.log.debug("showLoginError has a prolonged login error");
// Convert to days
let lastSync =
Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
@ -214,6 +260,7 @@ let gSyncUI = {
let reason = Weave.Utils.getErrorString(Weave.Status.login);
description =
this._stringBundle.formatStringFromName("error.sync.description", [reason], 1);
this.log.debug("showLoginError has a non-prolonged error", reason);
}
let buttons = [];
@ -226,7 +273,6 @@ let gSyncUI = {
let notification = new Weave.Notification(title, description, null,
Weave.Notifications.PRIORITY_WARNING, buttons);
Weave.Notifications.replaceTitle(notification);
this.updateUI();
},
onLogout: function SUI_onLogout() {
@ -271,6 +317,7 @@ let gSyncUI = {
}
Services.obs.notifyObservers(null, "cloudsync:user-sync", null);
Services.obs.notifyObservers(null, "readinglist:user-sync", null);
},
handleToolbarButton: function SUI_handleStatusbarButton() {
@ -367,7 +414,15 @@ let gSyncUI = {
let lastSync;
try {
lastSync = Services.prefs.getCharPref("services.sync.lastSync");
lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync"));
}
catch (e) { };
// and reading-list time - we want whatever one is the most recent.
try {
let lastRLSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
if (!lastSync || lastRLSync > lastSync) {
lastSync = lastRLSync;
}
}
catch (e) { };
if (!lastSync || this._needsSetup()) {
@ -376,9 +431,9 @@ let gSyncUI = {
}
// Show the day-of-week and time (HH:MM) of last sync
let lastSyncDate = new Date(lastSync).toLocaleFormat("%a %H:%M");
let lastSyncDateString = lastSync.toLocaleFormat("%a %H:%M");
let lastSyncLabel =
this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDate], 1);
this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
syncButton.setAttribute("tooltiptext", lastSyncLabel);
},
@ -395,7 +450,69 @@ let gSyncUI = {
this.clearError(title);
},
// Return true if the reading-list is in a "prolonged" error state. That
// engine doesn't impose what that means, so calculate it here. For
// consistency, we just use the sync prefs.
isProlongedReadingListError() {
let lastSync, threshold, prolonged;
try {
lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout"));
prolonged = lastSync <= threshold;
} catch (ex) {
// no pref, assume not prolonged.
prolonged = false;
}
this.log.debug("isProlongedReadingListError has last successful sync at ${lastSync}, threshold is ${threshold}, prolonged=${prolonged}",
{lastSync, threshold, prolonged});
return prolonged;
},
onRLSyncError() {
// Like onSyncError, but from the reading-list engine.
// However, the current UX around Sync is that error notifications should
// generally *not* be seen as they typically aren't actionable - so only
// authentication errors (which require user action) and "prolonged" errors
// (which technically aren't actionable, but user really should know anyway)
// are shown.
this.log.debug("onRLSyncError with readingList state", ReadingListScheduler.state);
if (ReadingListScheduler.state == ReadingListScheduler.STATE_ERROR_AUTHENTICATION) {
this.onLoginError();
return;
}
// If it's not prolonged there's nothing to do.
if (!this.isProlongedReadingListError()) {
this.log.debug("onRLSyncError has a non-authentication, non-prolonged error, so not showing any error UI");
return;
}
// So it's a prolonged error.
// Unfortunate duplication from below...
this.log.debug("onRLSyncError has a prolonged error");
let title = this._stringBundle.GetStringFromName("error.sync.title");
// XXX - this is somewhat wrong - we are reporting the threshold we consider
// to be prolonged, not how long it actually has been. (ie, lastSync below
// is effectively constant) - bit it too is copied from below.
let lastSync =
Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
let description =
this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
let priority = Weave.Notifications.PRIORITY_INFO;
let buttons = [
new Weave.NotificationButton(
this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"),
this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"),
function() { gSyncUI.doSync(); return true; }
),
];
let notification =
new Weave.Notification(title, description, null, priority, buttons);
Weave.Notifications.replaceTitle(notification);
this.updateUI();
},
onSyncError: function SUI_onSyncError() {
this.log.debug("onSyncError");
let title = this._stringBundle.GetStringFromName("error.sync.title");
if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) {
@ -418,7 +535,9 @@ let gSyncUI = {
let priority = Weave.Notifications.PRIORITY_WARNING;
let buttons = [];
// Check if the client is outdated in some way
// Check if the client is outdated in some way (but note: we've never in the
// past, and probably never will, bump the relevent version numbers, so
// this is effectively dead code!)
let outdated = Weave.Status.sync == Weave.VERSION_OUT_OF_DATE;
for (let [engine, reason] in Iterator(Weave.Status.engines))
outdated = outdated || reason == Weave.VERSION_OUT_OF_DATE;
@ -468,6 +587,7 @@ let gSyncUI = {
},
observe: function SUI_observe(subject, topic, data) {
this.log.debug("observed", topic);
if (this._unloaded) {
Cu.reportError("SyncUI observer called after unload: " + topic);
return;
@ -480,10 +600,26 @@ let gSyncUI = {
subject = subject.wrappedJSObject.object;
}
// First handle "activity" only.
switch (topic) {
case "weave:service:sync:start":
case "weave:service:login:start":
case "readinglist:sync:start":
this.onActivityStart();
break;
case "weave:service:sync:finish":
case "weave:service:sync:error":
case "weave:service:login:finish":
case "weave:service:login:error":
case "readinglist:sync:finish":
case "readinglist:sync:error":
this.onActivityStop();
break;
}
// Now non-activity state (eg, enabled, errors, etc)
// Note that sync uses the ":ui:" notifications for errors because sync.
// ReadingList has no such concept (yet?; hopefully the :error is enough!)
switch (topic) {
case "weave:ui:sync:finish":
this.onSyncFinish();
break;
@ -496,9 +632,6 @@ let gSyncUI = {
case "weave:service:setup-complete":
this.onSetupComplete();
break;
case "weave:service:login:start":
this.onActivityStart();
break;
case "weave:service:login:finish":
this.onLoginFinish();
break;
@ -523,6 +656,13 @@ let gSyncUI = {
case "weave:ui:clear-error":
this.clearError();
break;
case "readinglist:sync:error":
this.onRLSyncError();
break;
case "readinglist:sync:finish":
this.clearError();
break;
}
},
@ -540,3 +680,6 @@ XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() {
createBundle("chrome://weave/locale/services/sync.properties");
});
XPCOMUtils.defineLazyGetter(gSyncUI, "log", function() {
return Log.repository.getLogger("browserwindow.syncui");
});

View File

@ -37,6 +37,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
"resource:///modules/ContentSearch.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
"resource:///modules/AboutHome.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Log",
"resource://gre/modules/Log.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "Favicons",
"@mozilla.org/browser/favicon-service;1",
"mozIAsyncFavicons");

View File

@ -1,13 +1,19 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
let {Log} = Cu.import("resource://gre/modules/Log.jsm", {});
let {Weave} = Cu.import("resource://services-sync/main.js", {});
let {Notifications} = Cu.import("resource://services-sync/notifications.js", {});
// The BackStagePass allows us to get this test-only non-exported function.
let {getInternalScheduler} = Cu.import("resource:///modules/readinglist/Scheduler.jsm", {});
let stringBundle = Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Ci.nsIStringBundleService)
.createBundle("chrome://weave/locale/services/sync.properties");
// ensure test output sees log messages.
Log.repository.getLogger("browserwindow.syncui").addAppender(new Log.DumpAppender());
function promiseObserver(topic) {
return new Promise(resolve => {
let obs = (subject, topic, data) => {
@ -28,13 +34,10 @@ add_task(function* prepare() {
window.gSyncUI._needsSetup = () => false;
registerCleanupFunction(() => {
window.gSyncUI._needsSetup = oldNeedsSetup;
// this test leaves the tab focused which can cause browser_tabopen_reflows
// to fail, so re-select the URLBar.
gURLBar.select();
});
});
add_task(function* testProlongedError() {
add_task(function* testProlongedSyncError() {
let promiseNotificationAdded = promiseObserver("weave:notification:added");
Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
@ -56,7 +59,32 @@ add_task(function* testProlongedError() {
Assert.equal(Notifications.notifications.length, 0, "no notifications left");
});
add_task(function* testLoginError() {
add_task(function* testProlongedRLError() {
let promiseNotificationAdded = promiseObserver("weave:notification:added");
Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
// Pretend the reading-list is in the "prolonged error" state.
let longAgo = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000); // 100 days ago.
Services.prefs.setCharPref("readinglist.scheduler.lastSync", longAgo.toString());
getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_OTHER;
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:error", null);
let subject = yield promiseNotificationAdded;
let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract!
Assert.equal(notification.title, stringBundle.GetStringFromName("error.sync.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// Now pretend we just had a successful sync - the error notification should go away.
let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
Services.prefs.setCharPref("readinglist.scheduler.lastSync", Date.now().toString());
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
yield promiseNotificationRemoved;
Assert.equal(Notifications.notifications.length, 0, "no notifications left");
});
add_task(function* testSyncLoginError() {
let promiseNotificationAdded = promiseObserver("weave:notification:added");
Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
@ -69,6 +97,7 @@ add_task(function* testLoginError() {
let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract!
Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// Now pretend we just had a successful login - the error notification should go away.
Weave.Status.sync = Weave.STATUS_OK;
Weave.Status.login = Weave.LOGIN_SUCCEEDED;
@ -79,20 +108,100 @@ add_task(function* testLoginError() {
Assert.equal(Notifications.notifications.length, 0, "no notifications left");
});
function testButtonActions(startNotification, endNotification) {
let button = document.getElementById("sync-button");
Assert.ok(button, "button exists");
let panelbutton = document.getElementById("PanelUI-fxa-status");
Assert.ok(panelbutton, "panel button exists");
add_task(function* testRLLoginError() {
let promiseNotificationAdded = promiseObserver("weave:notification:added");
Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
// Pretend RL is in an auth error state
getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_AUTHENTICATION;
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:error", null);
let subject = yield promiseNotificationAdded;
let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract!
Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// Now pretend we just had a successful sync - the error notification should go away.
getInternalScheduler().state = ReadingListScheduler.STATE_OK;
let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
yield promiseNotificationRemoved;
Assert.equal(Notifications.notifications.length, 0, "no notifications left");
});
// Here we put readinglist into an "authentication error" state (should see
// the error bar reflecting this), then report a prolonged error from Sync (an
// infobar to reflect the sync error should replace it), then resolve the sync
// error - the authentication error from readinglist should remain.
add_task(function* testRLLoginErrorRemains() {
let promiseNotificationAdded = promiseObserver("weave:notification:added");
Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
// Pretend RL is in an auth error state
getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_AUTHENTICATION;
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:error", null);
let subject = yield promiseNotificationAdded;
let notification = subject.wrappedJSObject.object; // sync's observer abstraction is abstract!
Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// Now Sync into a prolonged auth error state.
promiseNotificationAdded = promiseObserver("weave:notification:added");
Weave.Status.sync = Weave.PROLONGED_SYNC_FAILURE;
Weave.Status.login = Weave.LOGIN_FAILED_LOGIN_REJECTED;
Services.obs.notifyObservers(null, "weave:ui:sync:error", null);
subject = yield promiseNotificationAdded;
// still exactly 1 notification with the "login" title.
notification = subject.wrappedJSObject.object;
Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// Resolve the sync problem.
promiseNotificationAdded = promiseObserver("weave:notification:added");
Weave.Status.sync = Weave.STATUS_OK;
Weave.Status.login = Weave.LOGIN_SUCCEEDED;
Services.obs.notifyObservers(null, "weave:ui:sync:finish", null);
// Expect one notification - the RL login problem.
subject = yield promiseNotificationAdded;
// still exactly 1 notification with the "login" title.
notification = subject.wrappedJSObject.object;
Assert.equal(notification.title, stringBundle.GetStringFromName("error.login.title"));
Assert.equal(Notifications.notifications.length, 1, "exactly 1 notification");
// and cleanup - resolve the readinglist error.
getInternalScheduler().state = ReadingListScheduler.STATE_OK;
let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
yield promiseNotificationRemoved;
Assert.equal(Notifications.notifications.length, 0, "no notifications left");
});
function checkButtonsStatus(shouldBeActive) {
let button = document.getElementById("sync-button");
let panelbutton = document.getElementById("PanelUI-fxa-status");
if (shouldBeActive) {
Assert.equal(button.getAttribute("status"), "active");
Assert.equal(panelbutton.getAttribute("syncstatus"), "active");
} else {
Assert.ok(!button.hasAttribute("status"));
Assert.ok(!panelbutton.hasAttribute("syncstatus"));
}
}
function testButtonActions(startNotification, endNotification) {
checkButtonsStatus(false);
// pretend a sync is starting.
Services.obs.notifyObservers(null, startNotification, null);
Assert.equal(button.getAttribute("status"), "active");
Assert.equal(panelbutton.getAttribute("syncstatus"), "active");
checkButtonsStatus(true);
// and has stopped
Services.obs.notifyObservers(null, endNotification, null);
Assert.ok(!button.hasAttribute("status"));
Assert.ok(!panelbutton.hasAttribute("syncstatus"));
checkButtonsStatus(false);
}
add_task(function* testButtonActivities() {
@ -102,7 +211,32 @@ add_task(function* testButtonActivities() {
yield PanelUI.show();
try {
testButtonActions("weave:service:login:start", "weave:service:login:finish");
testButtonActions("weave:service:sync:start", "weave:ui:sync:finish");
testButtonActions("weave:service:login:start", "weave:service:login:error");
testButtonActions("weave:service:sync:start", "weave:service:sync:finish");
testButtonActions("weave:service:sync:start", "weave:service:sync:error");
testButtonActions("readinglist:sync:start", "readinglist:sync:finish");
testButtonActions("readinglist:sync:start", "readinglist:sync:error");
// and ensure the counters correctly handle multiple in-flight syncs
Services.obs.notifyObservers(null, "weave:service:sync:start", null);
checkButtonsStatus(true);
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
checkButtonsStatus(true);
Services.obs.notifyObservers(null, "readinglist:sync:finish", null);
// sync is still going...
checkButtonsStatus(true);
// another reading list starts
Services.obs.notifyObservers(null, "readinglist:sync:start", null);
checkButtonsStatus(true);
// The initial sync stops.
Services.obs.notifyObservers(null, "weave:service:sync:finish", null);
// RL is still going...
checkButtonsStatus(true);
// RL finishes with an error, so no longer active.
Services.obs.notifyObservers(null, "readinglist:sync:error", null);
checkButtonsStatus(false);
} finally {
PanelUI.hide();
CustomizableUI.removeWidgetFromArea("sync-button");

View File

@ -617,7 +617,7 @@ this.BrowserIDManager.prototype = {
// that there is no authentication dance still under way.
this._shouldHaveSyncKeyBundle = true;
Weave.Status.login = this._authFailureReason;
Services.obs.notifyObservers(null, "weave:service:login:error", null);
Services.obs.notifyObservers(null, "weave:ui:login:error", null);
throw err;
});
},