 |
1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
4
5 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
6
7 #ifdef MOZ_SERVICES_CLOUDSYNC
8 XPCOMUtils.defineLazyModuleGetter(this, "CloudSync",
9 "resource://gre/modules/CloudSync.jsm");
10 #else
11 let CloudSync = null;
12 #endif
13
14 XPCOMUtils.defineLazyModuleGetter(this, "ReadingListScheduler",
15 "resource:///modules/readinglist/Scheduler.jsm");
16
17 // gSyncUI handles updating the tools menu and displaying notifications.
18 let gSyncUI = {
19 _obs: ["weave:service:sync:start",
20 "weave:service:sync:finish",
21 "weave:service:sync:error",
22 "weave:service:quota:remaining",
23 "weave:service:setup-complete",
24 "weave:service:login:start",
25 "weave:service:login:finish",
26 "weave:service:login:error",
27 "weave:service:logout:finish",
28 "weave:service:start-over",
29 "weave:service:start-over:finish",
30 "weave:ui:login:error",
31 "weave:ui:sync:error",
32 "weave:ui:sync:finish",
33 "weave:ui:clear-error",
34
35 "readinglist:sync:start",
36 "readinglist:sync:finish",
37 "readinglist:sync:error",
38 ],
39
40 _unloaded: false,
41 // The number of "active" syncs - while this is non-zero, our button will spin
42 _numActiveSyncTasks: 0,
43
44 init: function () {
45 Cu.import("resource://services-common/stringbundle.js");
46
47 // Proceed to set up the UI if Sync has already started up.
48 // Otherwise we'll do it when Sync is firing up.
49 let xps = Components.classes["@mozilla.org/weave/service;1"]
50 .getService(Components.interfaces.nsISupports)
51 .wrappedJSObject;
52 if (xps.ready) {
53 this.initUI();
54 return;
55 }
56
57 Services.obs.addObserver(this, "weave:service:ready", true);
58
59 // Remove the observer if the window is closed before the observer
60 // was triggered.
61 window.addEventListener("unload", function onUnload() {
62 gSyncUI._unloaded = true;
63 window.removeEventListener("unload", onUnload, false);
64 Services.obs.removeObserver(gSyncUI, "weave:service:ready");
65
66 if (Weave.Status.ready) {
67 gSyncUI._obs.forEach(function(topic) {
68 Services.obs.removeObserver(gSyncUI, topic);
69 });
70 }
71 }, false);
72 },
73
74 initUI: function SUI_initUI() {
75 // If this is a browser window?
76 if (gBrowser) {
77 this._obs.push("weave:notification:added");
78 }
79
80 this._obs.forEach(function(topic) {
81 Services.obs.addObserver(this, topic, true);
82 }, this);
83
84 if (gBrowser && Weave.Notifications.notifications.length) {
85 this.initNotifications();
86 }
87 this.updateUI();
88 },
89
90 initNotifications: function SUI_initNotifications() {
91 const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
92 let notificationbox = document.createElementNS(XULNS, "notificationbox");
93 notificationbox.id = "sync-notifications";
94 notificationbox.setAttribute("flex", "1");
95
96 let bottombox = document.getElementById("browser-bottombox");
97 bottombox.insertBefore(notificationbox, bottombox.firstChild);
98
99 // Force a style flush to ensure that our binding is attached.
100 notificationbox.clientTop;
101
102 // notificationbox will listen to observers from now on.
103 Services.obs.removeObserver(this, "weave:notification:added");
104 let idx = this._obs.indexOf("weave:notification:added");
105 if (idx >= 0) {
106 this._obs.splice(idx, 1);
107 }
108 },
109
110 _needsSetup() {
111 // We want to treat "account needs verification" as "needs setup". So
112 // "reach in" to Weave.Status._authManager to check whether we the signed-in
113 // user is verified.
114 // Referencing Weave.Status spins a nested event loop to initialize the
115 // authManager, so this should always return a value directly.
116 // This only applies to fxAccounts-based Sync.
117 if (Weave.Status._authManager._signedInUser !== undefined) {
118 // So we are using Firefox accounts - in this world, checking Sync isn't
119 // enough as reading list may be configured but not Sync.
120 // We consider ourselves setup if we have a verified user.
121 // XXX - later we should consider checking preferences to ensure at least
122 // one engine is enabled?
123 return !Weave.Status._authManager._signedInUser ||
124 !Weave.Status._authManager._signedInUser.verified;
125 }
126
127 // So we are using legacy sync, and reading-list isn't supported for such
128 // users, so check sync itself.
129 let firstSync = "";
130 try {
131 firstSync = Services.prefs.getCharPref("services.sync.firstSync");
132 } catch (e) { }
133
134 return Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED ||
135 firstSync == "notReady";
136 },
137
138 _loginFailed: function () {
139 return Weave.Status.login == Weave.LOGIN_FAILED_LOGIN_REJECTED ||
140 ReadingListScheduler.state == ReadingListScheduler.STATE_ERROR_AUTHENTICATION;
141 },
142
143 updateUI: function SUI_updateUI() {
144 let needsSetup = this._needsSetup();
145 let loginFailed = this._loginFailed();
146
147 // Start off with a clean slate
148 document.getElementById("sync-reauth-state").hidden = true;
149 document.getElementById("sync-setup-state").hidden = true;
150 document.getElementById("sync-syncnow-state").hidden = true;
151
152 if (CloudSync && CloudSync.ready && CloudSync().adapters.count) {
153 document.getElementById("sync-syncnow-state").hidden = false;
154 } else if (loginFailed) {
155 document.getElementById("sync-reauth-state").hidden = false;
156 this.showLoginError();
157 } else if (needsSetup) {
158 document.getElementById("sync-setup-state").hidden = false;
159 } else {
160 document.getElementById("sync-syncnow-state").hidden = false;
161 }
162
163 if (!gBrowser)
164 return;
165
166 let syncButton = document.getElementById("sync-button");
167 if (needsSetup && syncButton)
168 syncButton.removeAttribute("tooltiptext");
169
170 this._updateLastSyncTime();
171 },
172
173
174 // Functions called by observers
175 onActivityStart() {
176 if (!gBrowser)
177 return;
178
179 this.log.debug("onActivityStart with numActive", this._numActiveSyncTasks);
180 if (++this._numActiveSyncTasks == 1) {
181 let button = document.getElementById("sync-button");
182 if (button) {
183 button.setAttribute("status", "active");
184 }
185 button = document.getElementById("PanelUI-fxa-status");
186 if (button) {
187 button.setAttribute("syncstatus", "active");
188 }
189 }
190 },
191
192 onActivityStop() {
193 if (!gBrowser)
194 return;
195 this.log.debug("onActivityStop with numActive", this._numActiveSyncTasks);
196 if (--this._numActiveSyncTasks) {
197 if (this._numActiveSyncTasks < 0) {
198 // This isn't particularly useful (it seems more likely we'll set a
199 // "start" without a "stop" meaning it forever remains > 0) but it
200 // might offer some value...
201 this.log.error("mismatched onActivityStart/Stop calls",
202 new Error("active=" + this._numActiveSyncTasks));
203 }
204 return; // active tasks are still ongoing...
205 }
206
207 let syncButton = document.getElementById("sync-button");
208 if (syncButton) {
209 syncButton.removeAttribute("status");
210 }
211 let panelHorizontalButton = document.getElementById("PanelUI-fxa-status");
212 if (panelHorizontalButton) {
213 panelHorizontalButton.removeAttribute("syncstatus");
214 }
215 },
216
217 onLoginFinish: function SUI_onLoginFinish() {
218 // Clear out any login failure notifications
219 let title = this._stringBundle.GetStringFromName("error.login.title");
220 this.clearError(title);
221 },
222
223 onSetupComplete: function SUI_onSetupComplete() {
224 this.onLoginFinish();
225 },
226
227 onLoginError: function SUI_onLoginError() {
228 // Note: This is used for *both* Sync and ReadingList login errors.
229 // if login fails, any other notifications are essentially moot
230 Weave.Notifications.removeAll();
231
232 // if we haven't set up the client, don't show errors
233 if (this._needsSetup()) {
234 this.updateUI();
235 return;
236 }
237 // if we are still waiting for the identity manager to initialize, don't show errors
238 if (Weave.Status.login == Weave.LOGIN_FAILED_NOT_READY) {
239 this.updateUI();
240 return;
241 }
242 this.showLoginError();
243 this.updateUI();
244 },
245
246 showLoginError() {
247 // Note: This is used for *both* Sync and ReadingList login errors.
248 let title = this._stringBundle.GetStringFromName("error.login.title");
249
250 let description;
251 if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE ||
252 this.isProlongedReadingListError()) {
253 this.log.debug("showLoginError has a prolonged login error");
254 // Convert to days
255 let lastSync =
256 Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
257 description =
258 this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
259 } else {
260 let reason = Weave.Utils.getErrorString(Weave.Status.login);
261 description =
262 this._stringBundle.formatStringFromName("error.sync.description", [reason], 1);
263 this.log.debug("showLoginError has a non-prolonged error", reason);
264 }
265
266 let buttons = [];
267 buttons.push(new Weave.NotificationButton(
268 this._stringBundle.GetStringFromName("error.login.prefs.label"),
269 this._stringBundle.GetStringFromName("error.login.prefs.accesskey"),
270 function() { gSyncUI.openPrefs(); return true; }
271 ));
272
273 let notification = new Weave.Notification(title, description, null,
274 Weave.Notifications.PRIORITY_WARNING, buttons);
275 Weave.Notifications.replaceTitle(notification);
276 },
277
278 onLogout: function SUI_onLogout() {
279 this.updateUI();
280 },
281
282 onStartOver: function SUI_onStartOver() {
283 this.clearError();
284 },
285
286 onQuotaNotice: function onQuotaNotice(subject, data) {
287 let title = this._stringBundle.GetStringFromName("warning.sync.quota.label");
288 let description = this._stringBundle.GetStringFromName("warning.sync.quota.description");
289 let buttons = [];
290 buttons.push(new Weave.NotificationButton(
291 this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.label"),
292 this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.accesskey"),
293 function() { gSyncUI.openQuotaDialog(); return true; }
294 ));
295
296 let notification = new Weave.Notification(
297 title, description, null, Weave.Notifications.PRIORITY_WARNING, buttons);
298 Weave.Notifications.replaceTitle(notification);
299 },
300
301 _getAppName: function () {
302 let brand = new StringBundle("chrome://branding/locale/brand.properties");
303 return brand.get("brandShortName");
304 },
305
306 openServerStatus: function () {
307 let statusURL = Services.prefs.getCharPref("services.sync.statusURL");
308 window.openUILinkIn(statusURL, "tab");
309 },
310
311 // Commands
312 doSync: function SUI_doSync() {
313 let needsSetup = this._needsSetup();
314
315 if (!needsSetup) {
316 setTimeout(function () Weave.Service.errorHandler.syncAndReportErrors(), 0);
317 }
318
319 Services.obs.notifyObservers(null, "cloudsync:user-sync", null);
320 Services.obs.notifyObservers(null, "readinglist:user-sync", null);
321 },
322
323 handleToolbarButton: function SUI_handleStatusbarButton() {
324 if (this._needsSetup())
325 this.openSetup();
326 else
327 this.doSync();
328 },
329
330 //XXXzpao should be part of syncCommon.js - which we might want to make a module...
331 // To be fixed in a followup (bug 583366)
332
333 /**
334 * Invoke the Sync setup wizard.
335 *
336 * @param wizardType
337 * Indicates type of wizard to launch:
338 * null -- regular set up wizard
339 * "pair" -- pair a device first
340 * "reset" -- reset sync
341 * @param entryPoint
342 * Indicates the entrypoint from where this method was called.
343 */
344
345 openSetup: function SUI_openSetup(wizardType, entryPoint = "syncbutton") {
346 let xps = Components.classes["@mozilla.org/weave/service;1"]
347 .getService(Components.interfaces.nsISupports)
348 .wrappedJSObject;
349 if (xps.fxAccountsEnabled) {
350 fxAccounts.getSignedInUser().then(userData => {
351 if (userData) {
352 this.openPrefs();
353 } else {
354 // If the user is also in an uitour, set the entrypoint to `uitour`
355 if (UITour.tourBrowsersByWindow.get(window) &&
356 UITour.tourBrowsersByWindow.get(window).has(gBrowser.selectedBrowser)) {
357 entryPoint = "uitour";
358 }
359 switchToTabHavingURI("about:accounts?entrypoint=" + entryPoint, true, {
360 replaceQueryString: true
361 });
362 }
363 });
364 } else {
365 let win = Services.wm.getMostRecentWindow("Weave:AccountSetup");
366 if (win)
367 win.focus();
368 else {
369 window.openDialog("chrome://browser/content/sync/setup.xul",
370 "weaveSetup", "centerscreen,chrome,resizable=no",
371 wizardType);
372 }
373 }
374 },
375
376 openAddDevice: function () {
377 if (!Weave.Utils.ensureMPUnlocked())
378 return;
379
380 let win = Services.wm.getMostRecentWindow("Sync:AddDevice");
381 if (win)
382 win.focus();
383 else
384 window.openDialog("chrome://browser/content/sync/addDevice.xul",
385 "syncAddDevice", "centerscreen,chrome,resizable=no");
386 },
387
388 openQuotaDialog: function SUI_openQuotaDialog() {
389 let win = Services.wm.getMostRecentWindow("Sync:ViewQuota");
390 if (win)
391 win.focus();
392 else
393 Services.ww.activeWindow.openDialog(
394 "chrome://browser/content/sync/quota.xul", "",
395 "centerscreen,chrome,dialog,modal");
396 },
397
398 openPrefs: function SUI_openPrefs() {
399 openPreferences("paneSync");
400 },
401
402 openSignInAgainPage: function (entryPoint = "syncbutton") {
403 gFxAccounts.openSignInAgainPage(entryPoint);
404 },
405
406 // Helpers
407 _updateLastSyncTime: function SUI__updateLastSyncTime() {
408 if (!gBrowser)
409 return;
410
411 let syncButton = document.getElementById("sync-button");
412 if (!syncButton)
413 return;
414
415 let lastSync;
416 try {
417 lastSync = new Date(Services.prefs.getCharPref("services.sync.lastSync"));
418 }
419 catch (e) { };
420 // and reading-list time - we want whatever one is the most recent.
421 try {
422 let lastRLSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
423 if (!lastSync || lastRLSync > lastSync) {
424 lastSync = lastRLSync;
425 }
426 }
427 catch (e) { };
428 if (!lastSync || this._needsSetup()) {
429 syncButton.removeAttribute("tooltiptext");
430 return;
431 }
432
433 // Show the day-of-week and time (HH:MM) of last sync
434 let lastSyncDateString = lastSync.toLocaleFormat("%a %H:%M");
435 let lastSyncLabel =
436 this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
437
438 syncButton.setAttribute("tooltiptext", lastSyncLabel);
439 },
440
441 clearError: function SUI_clearError(errorString) {
442 Weave.Notifications.removeAll(errorString);
443 this.updateUI();
444 },
445
446 onSyncFinish: function SUI_onSyncFinish() {
447 let title = this._stringBundle.GetStringFromName("error.sync.title");
448
449 // Clear out sync failures on a successful sync
450 this.clearError(title);
451 },
452
453 // Return true if the reading-list is in a "prolonged" error state. That
454 // engine doesn't impose what that means, so calculate it here. For
455 // consistency, we just use the sync prefs.
456 isProlongedReadingListError() {
457 let lastSync, threshold, prolonged;
458 try {
459 lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
460 threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") * 1000);
461 prolonged = lastSync <= threshold;
462 } catch (ex) {
463 // no pref, assume not prolonged.
464 prolonged = false;
465 }
466 this.log.debug("isProlongedReadingListError has last successful sync at ${lastSync}, threshold is ${threshold}, prolonged=${prolonged}",
467 {lastSync, threshold, prolonged});
468 return prolonged;
469 },
470
471 onRLSyncError() {
472 // Like onSyncError, but from the reading-list engine.
473 // However, the current UX around Sync is that error notifications should
474 // generally *not* be seen as they typically aren't actionable - so only
475 // authentication errors (which require user action) and "prolonged" errors
476 // (which technically aren't actionable, but user really should know anyway)
477 // are shown.
478 this.log.debug("onRLSyncError with readingList state", ReadingListScheduler.state);
479 if (ReadingListScheduler.state == ReadingListScheduler.STATE_ERROR_AUTHENTICATION) {
480 this.onLoginError();
481 return;
482 }
483 // If it's not prolonged there's nothing to do.
484 if (!this.isProlongedReadingListError()) {
485 this.log.debug("onRLSyncError has a non-authentication, non-prolonged error, so not showing any error UI");
486 return;
487 }
488 // So it's a prolonged error.
489 // Unfortunate duplication from below...
490 this.log.debug("onRLSyncError has a prolonged error");
491 let title = this._stringBundle.GetStringFromName("error.sync.title");
492 // XXX - this is somewhat wrong - we are reporting the threshold we consider
493 // to be prolonged, not how long it actually has been. (ie, lastSync below
494 // is effectively constant) - bit it too is copied from below.
495 let lastSync =
496 Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
497 let description =
498 this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
499 let priority = Weave.Notifications.PRIORITY_INFO;
500 let buttons = [
501 new Weave.NotificationButton(
502 this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"),
503 this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"),
504 function() { gSyncUI.doSync(); return true; }
505 ),
506 ];
507 let notification =
508 new Weave.Notification(title, description, null, priority, buttons);
509 Weave.Notifications.replaceTitle(notification);
510
511 this.updateUI();
512 },
513
514 onSyncError: function SUI_onSyncError() {
515 this.log.debug("onSyncError");
516 let title = this._stringBundle.GetStringFromName("error.sync.title");
517
518 if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) {
519 this.onLoginError();
520 return;
521 }
522
523 let description;
524 if (Weave.Status.sync == Weave.PROLONGED_SYNC_FAILURE) {
525 // Convert to days
526 let lastSync =
527 Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") / 86400;
528 description =
529 this._stringBundle.formatStringFromName("error.sync.prolonged_failure", [lastSync], 1);
530 } else {
531 let error = Weave.Utils.getErrorString(Weave.Status.sync);
532 description =
533 this._stringBundle.formatStringFromName("error.sync.description", [error], 1);
534 }
535 let priority = Weave.Notifications.PRIORITY_WARNING;
536 let buttons = [];
537
538 // Check if the client is outdated in some way (but note: we've never in the
539 // past, and probably never will, bump the relevent version numbers, so
540 // this is effectively dead code!)
541 let outdated = Weave.Status.sync == Weave.VERSION_OUT_OF_DATE;
542 for (let [engine, reason] in Iterator(Weave.Status.engines))
543 outdated = outdated || reason == Weave.VERSION_OUT_OF_DATE;
544
545 if (outdated) {
546 description = this._stringBundle.GetStringFromName(
547 "error.sync.needUpdate.description");
548 buttons.push(new Weave.NotificationButton(
549 this._stringBundle.GetStringFromName("error.sync.needUpdate.label"),
550 this._stringBundle.GetStringFromName("error.sync.needUpdate.accesskey"),
551 function() { window.openUILinkIn("https://services.mozilla.com/update/", "tab"); return true; }
552 ));
553 }
554 else if (Weave.Status.sync == Weave.OVER_QUOTA) {
555 description = this._stringBundle.GetStringFromName(
556 "error.sync.quota.description");
557 buttons.push(new Weave.NotificationButton(
558 this._stringBundle.GetStringFromName(
559 "error.sync.viewQuotaButton.label"),
560 this._stringBundle.GetStringFromName(
561 "error.sync.viewQuotaButton.accesskey"),
562 function() { gSyncUI.openQuotaDialog(); return true; } )
563 );
564 }
565 else if (Weave.Status.enforceBackoff) {
566 priority = Weave.Notifications.PRIORITY_INFO;
567 buttons.push(new Weave.NotificationButton(
568 this._stringBundle.GetStringFromName("error.sync.serverStatusButton.label"),
569 this._stringBundle.GetStringFromName("error.sync.serverStatusButton.accesskey"),
570 function() { gSyncUI.openServerStatus(); return true; }
571 ));
572 }
573 else {
574 priority = Weave.Notifications.PRIORITY_INFO;
575 buttons.push(new Weave.NotificationButton(
576 this._stringBundle.GetStringFromName("error.sync.tryAgainButton.label"),
577 this._stringBundle.GetStringFromName("error.sync.tryAgainButton.accesskey"),
578 function() { gSyncUI.doSync(); return true; }
579 ));
580 }
581
582 let notification =
583 new Weave.Notification(title, description, null, priority, buttons);
584 Weave.Notifications.replaceTitle(notification);
585
586 this.updateUI();
587 },
588
589 observe: function SUI_observe(subject, topic, data) {
590 this.log.debug("observed", topic);
591 if (this._unloaded) {
592 Cu.reportError("SyncUI observer called after unload: " + topic);
593 return;
594 }
595
596 // Unwrap, just like Svc.Obs, but without pulling in that dependency.
597 if (subject && typeof subject == "object" &&
598 ("wrappedJSObject" in subject) &&
599 ("observersModuleSubjectWrapper" in subject.wrappedJSObject)) {
600 subject = subject.wrappedJSObject.object;
601 }
602
603 // First handle "activity" only.
604 switch (topic) {
605 case "weave:service:sync:start":
606 case "weave:service:login:start":
607 case "readinglist:sync:start":
608 this.onActivityStart();
609 break;
610 case "weave:service:sync:finish":
611 case "weave:service:sync:error":
612 case "weave:service:login:finish":
613 case "weave:service:login:error":
614 case "readinglist:sync:finish":
615 case "readinglist:sync:error":
616 this.onActivityStop();
617 break;
618 }
619 // Now non-activity state (eg, enabled, errors, etc)
620 // Note that sync uses the ":ui:" notifications for errors because sync.
621 // ReadingList has no such concept (yet?; hopefully the :error is enough!)
622 switch (topic) {
623 case "weave:ui:sync:finish":
624 this.onSyncFinish();
625 break;
626 case "weave:ui:sync:error":
627 this.onSyncError();
628 break;
629 case "weave:service:quota:remaining":
630 this.onQuotaNotice();
631 break;
632 case "weave:service:setup-complete":
633 this.onSetupComplete();
634 break;
635 case "weave:service:login:finish":
636 this.onLoginFinish();
637 break;
638 case "weave:ui:login:error":
639 this.onLoginError();
640 break;
641 case "weave:service:logout:finish":
642 this.onLogout();
643 break;
644 case "weave:service:start-over":
645 this.onStartOver();
646 break;
647 case "weave:service:start-over:finish":
648 this.updateUI();
649 break;
650 case "weave:service:ready":
651 this.initUI();
652 break;
653 case "weave:notification:added":
654 this.initNotifications();
655 break;
656 case "weave:ui:clear-error":
657 this.clearError();
658 break;
659
660 case "readinglist:sync:error":
661 this.onRLSyncError();
662 break;
663 case "readinglist:sync:finish":
664 this.clearError();
665 break;
666 }
667 },
668
669 QueryInterface: XPCOMUtils.generateQI([
670 Ci.nsIObserver,
671 Ci.nsISupportsWeakReference
672 ])
673 };
674
675 XPCOMUtils.defineLazyGetter(gSyncUI, "_stringBundle", function() {
676 //XXXzpao these strings should probably be moved from /services to /browser... (bug 583381)
677 // but for now just make it work
678 return Cc["@mozilla.org/intl/stringbundle;1"].
679 getService(Ci.nsIStringBundleService).
680 createBundle("chrome://weave/locale/services/sync.properties");
681 });
682
683 XPCOMUtils.defineLazyGetter(gSyncUI, "log", function() {
684 return Log.repository.getLogger("browserwindow.syncui");
685 });
686