summaryrefslogtreecommitdiffstats
path: root/browser/base/content/browser-syncui.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/base/content/browser-syncui.js')
-rw-r--r--browser/base/content/browser-syncui.js544
1 files changed, 544 insertions, 0 deletions
diff --git a/browser/base/content/browser-syncui.js b/browser/base/content/browser-syncui.js
new file mode 100644
index 000000000..c5c2995c8
--- /dev/null
+++ b/browser/base/content/browser-syncui.js
@@ -0,0 +1,544 @@
+/* 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/. */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+if (AppConstants.MOZ_SERVICES_CLOUDSYNC) {
+ XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
+ "resource://gre/modules/CloudSync.jsm");
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
+ "resource://gre/modules/FxAccounts.jsm");
+
+const MIN_STATUS_ANIMATION_DURATION = 1600;
+
+// gSyncUI handles updating the tools menu and displaying notifications.
+var gSyncUI = {
+ _obs: ["weave:service:sync:start",
+ "weave:service:sync:finish",
+ "weave:service:sync:error",
+ "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",
+ "weave:ui:login:error",
+ "weave:ui:sync:error",
+ "weave:ui:sync:finish",
+ "weave:ui:clear-error",
+ "weave:engine:sync:finish"
+ ],
+
+ _unloaded: false,
+ // The last sync start time. Used to calculate the leftover animation time
+ // once syncing completes (bug 1239042).
+ _syncStartTime: 0,
+ _syncAnimationTimer: 0,
+
+ init: function () {
+ Cu.import("resource://services-common/stringbundle.js");
+
+ // Proceed to set up the UI if Sync has already started up.
+ // Otherwise we'll do it when Sync is firing up.
+ if (this.weaveService.ready) {
+ this.initUI();
+ return;
+ }
+
+ // Sync isn't ready yet, but we can still update the UI with an initial
+ // state - we haven't called initUI() yet, but that's OK - that's more
+ // about observers for state changes, and will be called once Sync is
+ // ready to start sending notifications.
+ this.updateUI();
+
+ Services.obs.addObserver(this, "weave:service:ready", true);
+ Services.obs.addObserver(this, "quit-application", true);
+
+ // Remove the observer if the window is closed before the observer
+ // was triggered.
+ window.addEventListener("unload", function onUnload() {
+ gSyncUI._unloaded = true;
+ window.removeEventListener("unload", onUnload, false);
+ Services.obs.removeObserver(gSyncUI, "weave:service:ready");
+ Services.obs.removeObserver(gSyncUI, "quit-application");
+
+ if (Weave.Status.ready) {
+ gSyncUI._obs.forEach(function(topic) {
+ Services.obs.removeObserver(gSyncUI, topic);
+ });
+ }
+ }, false);
+ },
+
+ initUI: function SUI_initUI() {
+ // If this is a browser window?
+ if (gBrowser) {
+ this._obs.push("weave:notification:added");
+ }
+
+ this._obs.forEach(function(topic) {
+ Services.obs.addObserver(this, topic, true);
+ }, this);
+
+ // initial label for the sync buttons.
+ let broadcaster = document.getElementById("sync-status");
+ broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label"));
+
+ this.maybeMoveSyncedTabsButton();
+
+ this.updateUI();
+ },
+
+
+ // Returns a promise that resolves with true if Sync needs to be configured,
+ // false otherwise.
+ _needsSetup() {
+ // If Sync is configured for FxAccounts then we do that promise-dance.
+ if (this.weaveService.fxAccountsEnabled) {
+ return fxAccounts.getSignedInUser().then(user => {
+ // We want to treat "account needs verification" as "needs setup".
+ return !(user && user.verified);
+ });
+ }
+ // We are using legacy sync - check that.
+ let firstSync = "";
+ try {
+ firstSync = Services.prefs.getCharPref("services.sync.firstSync");
+ } catch (e) { }
+
+ return Promise.resolve(Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED ||
+ firstSync == "notReady");
+ },
+
+ // Returns a promise that resolves with true if the user currently signed in
+ // to Sync needs to be verified, false otherwise.
+ _needsVerification() {
+ // For callers who care about the distinction between "needs setup" and
+ // "needs verification"
+ if (this.weaveService.fxAccountsEnabled) {
+ return fxAccounts.getSignedInUser().then(user => {
+ // If there is no user, they can't be in a "needs verification" state.
+ if (!user) {
+ return false;
+ }
+ return !user.verified;
+ });
+ }
+
+ // Otherwise we are configured for legacy Sync, which has no verification
+ // concept.
+ return Promise.resolve(false);
+ },
+
+ // Note that we don't show login errors in a notification bar here, but do
+ // still need to track a login-failed state so the "Tools" menu updates
+ // with the correct state.
+ _loginFailed: function () {
+ // If Sync isn't already ready, we don't want to force it to initialize
+ // by referencing Weave.Status - and it isn't going to be accurate before
+ // Sync is ready anyway.
+ if (!this.weaveService.ready) {
+ this.log.debug("_loginFailed has sync not ready, so returning false");
+ return false;
+ }
+ this.log.debug("_loginFailed has sync state=${sync}",
+ { sync: Weave.Status.login});
+ return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED;
+ },
+
+ // Kick off an update of the UI - does *not* return a promise.
+ updateUI() {
+ this._promiseUpdateUI().catch(err => {
+ this.log.error("updateUI failed", err);
+ })
+ },
+
+ // Updates the UI - returns a promise.
+ _promiseUpdateUI() {
+ return this._needsSetup().then(needsSetup => {
+ if (!gBrowser)
+ return Promise.resolve();
+
+ let loginFailed = this._loginFailed();
+
+ // Start off with a clean slate
+ document.getElementById("sync-reauth-state").hidden = true;
+ document.getElementById("sync-setup-state").hidden = true;
+ document.getElementById("sync-syncnow-state").hidden = true;
+
+ if (CloudSync && CloudSync.ready && CloudSync().adapters.count) {
+ document.getElementById("sync-syncnow-state").hidden = false;
+ } else if (loginFailed) {
+ // unhiding this element makes the menubar show the login failure state.
+ document.getElementById("sync-reauth-state").hidden = false;
+ } else if (needsSetup) {
+ document.getElementById("sync-setup-state").hidden = false;
+ } else {
+ document.getElementById("sync-syncnow-state").hidden = false;
+ }
+
+ return this._updateSyncButtonsTooltip();
+ });
+ },
+
+ // Functions called by observers
+ onActivityStart() {
+ if (!gBrowser)
+ return;
+
+ this.log.debug("onActivityStart");
+
+ clearTimeout(this._syncAnimationTimer);
+ this._syncStartTime = Date.now();
+
+ let broadcaster = document.getElementById("sync-status");
+ broadcaster.setAttribute("syncstatus", "active");
+ broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncing2.label"));
+ broadcaster.setAttribute("disabled", "true");
+
+ this.updateUI();
+ },
+
+ _updateSyncStatus() {
+ if (!gBrowser)
+ return;
+ let broadcaster = document.getElementById("sync-status");
+ broadcaster.removeAttribute("syncstatus");
+ broadcaster.removeAttribute("disabled");
+ broadcaster.setAttribute("label", this._stringBundle.GetStringFromName("syncnow.label"));
+ this.updateUI();
+ },
+
+ onActivityStop() {
+ if (!gBrowser)
+ return;
+ this.log.debug("onActivityStop");
+
+ let now = Date.now();
+ let syncDuration = now - this._syncStartTime;
+
+ if (syncDuration < MIN_STATUS_ANIMATION_DURATION) {
+ let animationTime = MIN_STATUS_ANIMATION_DURATION - syncDuration;
+ clearTimeout(this._syncAnimationTimer);
+ this._syncAnimationTimer = setTimeout(() => this._updateSyncStatus(), animationTime);
+ } else {
+ this._updateSyncStatus();
+ }
+ },
+
+ onLoginError: function SUI_onLoginError() {
+ this.log.debug("onLoginError: login=${login}, sync=${sync}", Weave.Status);
+
+ // We don't show any login errors here; browser-fxaccounts shows them in
+ // the hamburger menu.
+ this.updateUI();
+ },
+
+ onLogout: function SUI_onLogout() {
+ this.updateUI();
+ },
+
+ _getAppName: function () {
+ let brand = new StringBundle("chrome://branding/locale/brand.properties");
+ return brand.get("brandShortName");
+ },
+
+ // Commands
+ // doSync forces a sync - it *does not* return a promise as it is called
+ // via the various UI components.
+ doSync() {
+ this._needsSetup().then(needsSetup => {
+ if (!needsSetup) {
+ setTimeout(() => Weave.Service.errorHandler.syncAndReportErrors(), 0);
+ }
+ Services.obs.notifyObservers(null, "cloudsync:user-sync", null);
+ }).catch(err => {
+ this.log.error("Failed to force a sync", err);
+ });
+ },
+
+ // Handle clicking the toolbar button - which either opens the Sync setup
+ // pages or forces a sync now. Does *not* return a promise as it is called
+ // via the UI.
+ handleToolbarButton() {
+ this._needsSetup().then(needsSetup => {
+ if (needsSetup || this._loginFailed()) {
+ this.openSetup();
+ } else {
+ this.doSync();
+ }
+ }).catch(err => {
+ this.log.error("Failed to handle toolbar button command", err);
+ });
+ },
+
+ /**
+ * Invoke the Sync setup wizard.
+ *
+ * @param wizardType
+ * Indicates type of wizard to launch:
+ * null -- regular set up wizard
+ * "pair" -- pair a device first
+ * "reset" -- reset sync
+ * @param entryPoint
+ * Indicates the entrypoint from where this method was called.
+ */
+
+ openSetup: function SUI_openSetup(wizardType, entryPoint = "syncbutton") {
+ if (this.weaveService.fxAccountsEnabled) {
+ this.openPrefs(entryPoint);
+ } else {
+ let win = Services.wm.getMostRecentWindow("Weave:AccountSetup");
+ if (win)
+ win.focus();
+ else {
+ window.openDialog("chrome://browser/content/sync/setup.xul",
+ "weaveSetup", "centerscreen,chrome,resizable=no",
+ wizardType);
+ }
+ }
+ },
+
+ // Open the legacy-sync device pairing UI. Note used for FxA Sync.
+ openAddDevice: function () {
+ if (!Weave.Utils.ensureMPUnlocked())
+ return;
+
+ let win = Services.wm.getMostRecentWindow("Sync:AddDevice");
+ if (win)
+ win.focus();
+ else
+ window.openDialog("chrome://browser/content/sync/addDevice.xul",
+ "syncAddDevice", "centerscreen,chrome,resizable=no");
+ },
+
+ openPrefs: function (entryPoint) {
+ openPreferences("paneSync", { urlParams: { entrypoint: entryPoint } });
+ },
+
+ openSignInAgainPage: function (entryPoint = "syncbutton") {
+ gFxAccounts.openSignInAgainPage(entryPoint);
+ },
+
+ openSyncedTabsPanel() {
+ let placement = CustomizableUI.getPlacementOfWidget("sync-button");
+ let area = placement ? placement.area : CustomizableUI.AREA_NAVBAR;
+ let anchor = document.getElementById("sync-button") ||
+ document.getElementById("PanelUI-menu-button");
+ if (area == CustomizableUI.AREA_PANEL) {
+ // The button is in the panel, so we need to show the panel UI, then our
+ // subview.
+ PanelUI.show().then(() => {
+ PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
+ }).catch(Cu.reportError);
+ } else {
+ // It is placed somewhere else - just try and show it.
+ PanelUI.showSubView("PanelUI-remotetabs", anchor, area);
+ }
+ },
+
+ /* After Sync is initialized we perform a once-only check for the sync
+ button being in "customize purgatory" and if so, move it to the panel.
+ This is done primarily for profiles created before SyncedTabs landed,
+ where the button defaulted to being in that purgatory.
+ We use a preference to ensure we only do it once, so people can still
+ customize it away and have it stick.
+ */
+ maybeMoveSyncedTabsButton() {
+ const prefName = "browser.migrated-sync-button";
+ let migrated = false;
+ try {
+ migrated = Services.prefs.getBoolPref(prefName);
+ } catch (_) {}
+ if (migrated) {
+ return;
+ }
+ if (!CustomizableUI.getPlacementOfWidget("sync-button")) {
+ CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL);
+ }
+ Services.prefs.setBoolPref(prefName, true);
+ },
+
+ /* Update the tooltip for the sync-status broadcaster (which will update the
+ Sync Toolbar button and the Sync spinner in the FxA hamburger area.)
+ If Sync is configured, the tooltip is when the last sync occurred,
+ otherwise the tooltip reflects the fact that Sync needs to be
+ (re-)configured.
+ */
+ _updateSyncButtonsTooltip: Task.async(function* () {
+ if (!gBrowser)
+ return;
+
+ let email;
+ try {
+ email = Services.prefs.getCharPref("services.sync.username");
+ } catch (ex) {}
+
+ let needsSetup = yield this._needsSetup();
+ let needsVerification = yield this._needsVerification();
+ let loginFailed = this._loginFailed();
+ // This is a little messy as the Sync buttons are 1/2 Sync related and
+ // 1/2 FxA related - so for some strings we use Sync strings, but for
+ // others we reach into gFxAccounts for strings.
+ let tooltiptext;
+ if (needsVerification) {
+ // "needs verification"
+ tooltiptext = gFxAccounts.strings.formatStringFromName("verifyDescription", [email], 1);
+ } else if (needsSetup) {
+ // "needs setup".
+ tooltiptext = this._stringBundle.GetStringFromName("signInToSync.description");
+ } else if (loginFailed) {
+ // "need to reconnect/re-enter your password"
+ tooltiptext = gFxAccounts.strings.formatStringFromName("reconnectDescription", [email], 1);
+ } else {
+ // Sync appears configured - format the "last synced at" time.
+ try {
+ let lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync"));
+ tooltiptext = this.formatLastSyncDate(lastSync);
+ }
+ catch (e) {
+ // pref doesn't exist (which will be the case until we've seen the
+ // first successful sync) or is invalid (which should be impossible!)
+ // Just leave tooltiptext as the empty string in these cases, which
+ // will cause the tooltip to be removed below.
+ }
+ }
+
+ // We've done all our promise-y work and ready to update the UI - make
+ // sure it hasn't been torn down since we started.
+ if (!gBrowser)
+ return;
+
+ let broadcaster = document.getElementById("sync-status");
+ if (broadcaster) {
+ if (tooltiptext) {
+ broadcaster.setAttribute("tooltiptext", tooltiptext);
+ } else {
+ broadcaster.removeAttribute("tooltiptext");
+ }
+ }
+ }),
+
+ formatLastSyncDate: function(date) {
+ let dateFormat;
+ let sixDaysAgo = (() => {
+ let date = new Date();
+ date.setDate(date.getDate() - 6);
+ date.setHours(0, 0, 0, 0);
+ return date;
+ })();
+ // It may be confusing for the user to see "Last Sync: Monday" when the last sync was a indeed a Monday but 3 weeks ago
+ if (date < sixDaysAgo) {
+ dateFormat = {month: 'long', day: 'numeric'};
+ } else {
+ dateFormat = {weekday: 'long', hour: 'numeric', minute: 'numeric'};
+ }
+ let lastSyncDateString = date.toLocaleDateString(undefined, dateFormat);
+ return this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
+ },
+
+ onClientsSynced: function() {
+ let broadcaster = document.getElementById("sync-syncnow-state");
+ if (broadcaster) {
+ if (Weave.Service.clientsEngine.stats.numClients > 1) {
+ broadcaster.setAttribute("devices-status", "multi");
+ } else {
+ broadcaster.setAttribute("devices-status", "single");
+ }
+ }
+ },
+
+ observe: function SUI_observe(subject, topic, data) {
+ this.log.debug("observed", topic);
+ if (this._unloaded) {
+ Cu.reportError("SyncUI observer called after unload: " + topic);
+ return;
+ }
+
+ // Unwrap, just like Svc.Obs, but without pulling in that dependency.
+ if (subject && typeof subject == "object" &&
+ ("wrappedJSObject" in subject) &&
+ ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) {
+ subject = subject.wrappedJSObject.object;
+ }
+
+ // First handle "activity" only.
+ switch (topic) {
+ case "weave:service:sync:start":
+ this.onActivityStart();
+ break;
+ case "weave:service:sync:finish":
+ case "weave:service:sync:error":
+ this.onActivityStop();
+ break;
+ }
+ // Now non-activity state (eg, enabled, errors, etc)
+ // Note that sync uses the ":ui:" notifications for errors because sync.
+ switch (topic) {
+ case "weave:ui:sync:finish":
+ // Do nothing.
+ break;
+ case "weave:ui:sync:error":
+ case "weave:service:setup-complete":
+ case "weave:service:login:finish":
+ case "weave:service:login:start":
+ case "weave:service:start-over":
+ this.updateUI();
+ break;
+ case "weave:ui:login:error":
+ case "weave:service:login:error":
+ this.onLoginError();
+ break;
+ case "weave:service:logout:finish":
+ this.onLogout();
+ break;
+ case "weave:service:start-over:finish":
+ this.updateUI();
+ break;
+ case "weave:service:ready":
+ this.initUI();
+ break;
+ case "weave:notification:added":
+ this.initNotifications();
+ break;
+ case "weave:engine:sync:finish":
+ if (data != "clients") {
+ return;
+ }
+ this.onClientsSynced();
+ break;
+ case "quit-application":
+ // Stop the animation timer on shutdown, since we can't update the UI
+ // after this.
+ clearTimeout(this._syncAnimationTimer);
+ break;
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference
+ ])
+};
+
+XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() {
+ // XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
+ // but for now just make it work
+ return Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle("chrome://weave/locale/services/sync.properties");
+});
+
+XPCOMUtils.defineLazyGetter(gSyncUI, "log", function() {
+ return Log.repository.getLogger("browserwindow.syncui");
+});
+
+XPCOMUtils.defineLazyGetter(gSyncUI, "weaveService", function() {
+ return Components.classes["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+});