Merge m-c to m-i
authorPhilipp von Weitershausen <philipp@weitershausen.de>
Tue, 23 Aug 2011 18:33:02 -0700
changeset 75787 7857bbf3a5238c236be1971dbb3bda70f49d4773
parent 75786 07ae78c82432245262b2a61e37360b17ac21fb92 (current diff)
parent 75736 fc9a011616cce0d0c77ec48f56bdbe1e1e2c2f98 (diff)
child 75788 0bc16cab7c8235e222c2d202ca3349cb1343dfcd
push id3
push userfelipc@gmail.com
push dateFri, 30 Sep 2011 20:09:13 +0000
milestone9.0a1
Merge m-c to m-i
--- a/browser/base/content/syncAddDevice.xul
+++ b/browser/base/content/syncAddDevice.xul
@@ -115,30 +115,30 @@
            onclick="gSyncAddDevice.goToSyncKeyPage();"/>
   </wizardpage>
 
   <!-- Need a non-empty label here, otherwise we get a default label on Mac -->
   <wizardpage id="syncKeyPage"
               label=" "
               onpageshow="gSyncAddDevice.onPageShow();">
     <description>
-      &addDevice.dialog.syncKey.label;
+      &addDevice.dialog.recoveryKey.label;
     </description>
     <spacer/>
 
     <groupbox>
-      <label value="&syncKeyEntry.label;"
-             accesskey="&syncKeyEntry.accesskey;"
+      <label value="&recoveryKeyEntry.label;"
+             accesskey="&recoveryKeyEntry.accesskey;"
              control="weavePassphrase"/>
       <textbox id="weavePassphrase"
                readonly="true"/>
     </groupbox>
 
     <groupbox align="center">
-      <description>&syncKeyBackup.description;</description>
+      <description>&recoveryKeyBackup.description;</description>
       <hbox>
         <button id="printSyncKeyButton"
                 label="&button.syncKeyBackup.print.label;"
                 accesskey="&button.syncKeyBackup.print.accesskey;"
                 oncommand="gSyncUtils.passphrasePrint('weavePassphrase');"/>
         <button id="saveSyncKeyButton"
                 label="&button.syncKeyBackup.save.label;"
                 accesskey="&button.syncKeyBackup.save.accesskey;"
--- a/browser/base/content/syncGenericChange.js
+++ b/browser/base/content/syncGenericChange.js
@@ -87,40 +87,40 @@ let Change = {
       Services.strings.createBundle("chrome://browser/locale/syncGenericChange.properties");
 
     switch (this._dialogType) {
       case "UpdatePassphrase":
       case "ResetPassphrase":
         document.getElementById("textBox1Row").hidden = true;
         document.getElementById("textBox2Row").hidden = true;
         document.getElementById("passphraseLabel").value
-          = this._str("new.synckey.label");
+          = this._str("new.recoverykey.label");
         document.getElementById("passphraseSpacer").hidden = false;
 
         if (this._updatingPassphrase) {
           document.getElementById("passphraseHelpBox").hidden = false;
-          document.title = this._str("new.synckey.title");
-          introText.textContent = this._str("new.synckey2.introText");
+          document.title = this._str("new.recoverykey.title");
+          introText.textContent = this._str("new.recoverykey.introText");
           this._dialog.getButton("finish").label
-            = this._str("new.synckey.acceptButton");
+            = this._str("new.recoverykey.acceptButton");
         }
         else {
           document.getElementById("generatePassphraseButton").hidden = false;
           document.getElementById("passphraseBackupButtons").hidden = false;
           this._passphraseBox.setAttribute("readonly", "true");
           let pp = Weave.Service.passphrase;
           if (Weave.Utils.isPassphrase(pp))
              pp = Weave.Utils.hyphenatePassphrase(pp);
           this._passphraseBox.value = pp;
           this._passphraseBox.focus();
-          document.title = this._str("change.synckey2.title");
+          document.title = this._str("change.recoverykey.title");
           introText.textContent = this._str("change.synckey.introText2");
-          warningText.textContent = this._str("change.synckey2.warningText");
+          warningText.textContent = this._str("change.recoverykey.warningText");
           this._dialog.getButton("finish").label
-            = this._str("change.synckey.acceptButton");
+            = this._str("change.recovery.acceptButton");
           if (this._duringSetup) {
             this._dialog.getButton("finish").disabled = false;
           }
         }
         break;
       case "ChangePassword":
         document.getElementById("passphraseRow").hidden = true;
         let box1label = document.getElementById("textBox1Label");
@@ -132,17 +132,17 @@ let Change = {
           introText.textContent = this._str("new.password.introText");
           this._dialog.getButton("finish").label
             = this._str("new.password.acceptButton");
           document.getElementById("textBox2Row").hidden = true;
         }
         else {
           document.title = this._str("change.password.title");
           box2label.value = this._str("new.password.confirm");
-          introText.textContent = this._str("change.password2.introText");
+          introText.textContent = this._str("change.password3.introText");
           warningText.textContent = this._str("change.password.warningText");
           this._dialog.getButton("finish").label
             = this._str("change.password.acceptButton");
         }
         break;
     }
     document.getElementById("change-page")
             .setAttribute("label", document.title);
@@ -190,30 +190,30 @@ let Change = {
     this._dialog.getButton("finish").disabled = false;
   },
 
   doChangePassphrase: function Change_doChangePassphrase() {
     let pp = Weave.Utils.normalizePassphrase(this._passphraseBox.value);
     if (this._updatingPassphrase) {
       Weave.Service.passphrase = pp;
       if (Weave.Service.login()) {
-        this._updateStatus("change.synckey2.success", "success");
+        this._updateStatus("change.recoverykey.success", "success");
         Weave.Service.persistLogin();
       }
       else {
         this._updateStatus("new.passphrase.status.incorrect", "error");
       }
     }
     else {
-      this._updateStatus("change.synckey.label", "active");
+      this._updateStatus("change.recoverykey.label", "active");
 
       if (Weave.Service.changePassphrase(pp))
-        this._updateStatus("change.synckey2.success", "success");
+        this._updateStatus("change.recoverykey.success", "success");
       else
-        this._updateStatus("change.synckey2.error", "error");
+        this._updateStatus("change.recoverykey.error", "error");
     }
 
     return false;
   },
 
   doChangePassword: function Change_doChangePassword() {
     if (this._currentPasswordInvalid) {
       Weave.Service.password = this._firstBox.value;
--- a/browser/base/content/syncGenericChange.xul
+++ b/browser/base/content/syncGenericChange.xul
@@ -133,17 +133,17 @@
               label="&button.syncKeyBackup.save.label;"
               accesskey="&button.syncKeyBackup.save.accesskey;"
               oncommand="gSyncUtils.passphraseSave('passphraseBox');"/>
     </hbox>
 
     <vbox id="passphraseHelpBox"
           hidden="true">
       <description>
-        &existingSyncKey.description;
+        &existingRecoveryKey.description;
         <label class="text-link"
                href="https://services.mozilla.com/sync/help/manual-setup">
           &addDevice.showMeHow.label;
         </label>
       </description>
     </vbox>
 
     <spacer id="passphraseSpacer"
--- a/browser/base/content/syncSetup.xul
+++ b/browser/base/content/syncSetup.xul
@@ -191,35 +191,35 @@
               &setup.tosAgree3.label;
             </description>
           </hbox>
         </row>
       </rows>
     </grid>
   </wizardpage>
 
-  <wizardpage label="&setup.newSyncKeyPage.title.label;"
+  <wizardpage label="&setup.newRecoveryKeyPage.title.label;"
               onextra1="gSyncSetup.onSyncOptions()"
               onpageshow="gSyncSetup.onPageShow();">
     <description>
-      &setup.newSyncKeyPage.description.label;
+      &setup.newRecoveryKeyPage.description.label;
     </description>
     <spacer/>
 
     <groupbox>
-      <label value="&syncKeyEntry.label;"
-             accesskey="&syncKeyEntry.accesskey;"
+      <label value="&recoveryKeyEntry.label;"
+             accesskey="&recoveryKeyEntry.accesskey;"
              control="weavePassphrase"/>
       <textbox id="weavePassphrase"
                readonly="true"
                onfocus="this.select();"/>
     </groupbox>
 
     <groupbox align="center">
-      <description>&syncKeyBackup.description;</description>
+      <description>&recoveryKeyBackup.description;</description>
       <hbox>
         <button id="printSyncKeyButton"
                 label="&button.syncKeyBackup.print.label;"
                 accesskey="&button.syncKeyBackup.print.accesskey;"
                 oncommand="gSyncUtils.passphrasePrint('weavePassphrase');"/>
         <button id="saveSyncKeyButton"
                 label="&button.syncKeyBackup.save.label;"
                 accesskey="&button.syncKeyBackup.save.accesskey;"
@@ -346,18 +346,18 @@
               </vbox>
             </hbox>
           </row>
         </rows>
       </grid>
 
     <groupbox>
       <label id="existingPassphraseLabel"
-             value="&signIn.syncKey.label;"
-             accesskey="&signIn.syncKey.accesskey;"
+             value="&signIn.recoveryKey.label;"
+             accesskey="&signIn.recoveryKey.accesskey;"
              control="existingPassphrase"/>
       <textbox id="existingPassphrase"
                oninput="gSyncSetup.checkFields()"/>
       <hbox id="login-throbber" hidden="true">
         <image/>
         <label value="&verifying.label;"/>
       </hbox>
       <vbox align="left" id="existingPassphraseFeedbackRow" hidden="true">
@@ -365,17 +365,17 @@
           <image class="statusIcon"/>
           <label class="status" value=" "/>
         </hbox>
       </vbox>
     </groupbox>
 
     <vbox id="passphraseHelpBox">
       <description>
-        &existingSyncKey.description;
+        &existingRecoveryKey.description;
         <label class="text-link"
                href="https://services.mozilla.com/sync/help/manual-setup">
           &addDevice.showMeHow.label;
         </label>
         <spacer id="passphraseHelpSpacer"/>
         <label class="text-link"
                onclick="gSyncSetup.resetPassphrase(); return false;">
           &resetSyncKey.label;
--- a/browser/base/content/syncUtils.js
+++ b/browser/base/content/syncUtils.js
@@ -188,18 +188,18 @@ let gSyncUtils = {
   },
 
   /**
    * Save passphrase backup document to disk as HTML file.
    * 
    * @param elid : ID of the form element containing the passphrase.
    */
   passphraseSave: function(elid) {
-    let dialogTitle = this.bundle.GetStringFromName("save.synckey.title");
-    let defaultSaveName = this.bundle.GetStringFromName("save.default.label");
+    let dialogTitle = this.bundle.GetStringFromName("save.recoverykey.title");
+    let defaultSaveName = this.bundle.GetStringFromName("save.recoverykey.defaultfilename");
     this._preparePPiframe(elid, function(iframe) {
       let filepicker = Cc["@mozilla.org/filepicker;1"]
                          .createInstance(Ci.nsIFilePicker);
       filepicker.init(window, dialogTitle, Ci.nsIFilePicker.modeSave);
       filepicker.appendFilters(Ci.nsIFilePicker.filterHTML);
       filepicker.defaultString = defaultSaveName;
       let rv = filepicker.show();
       if (rv == Ci.nsIFilePicker.returnOK
@@ -238,17 +238,17 @@ let gSyncUtils = {
       valid = val1.length >= Weave.MIN_PASS_LENGTH;
     else if (val1 && val1 == Weave.Service.username)
       error = "change.password.pwSameAsUsername";
     else if (val1 && val1 == Weave.Service.account)
       error = "change.password.pwSameAsEmail";
     else if (val1 && val1 == Weave.Service.password)
       error = "change.password.pwSameAsPassword";
     else if (val1 && val1 == Weave.Service.passphrase)
-      error = "change.password.pwSameAsSyncKey";
+      error = "change.password.pwSameAsRecoveryKey";
     else if (val1 && val2) {
       if (val1 == val2 && val1.length >= Weave.MIN_PASS_LENGTH)
         valid = true;
       else if (val1.length < Weave.MIN_PASS_LENGTH)
         error = "change.password.tooShort";
       else if (val1 != val2)
         error = "change.password.mismatch";
     }
--- a/browser/locales/en-US/chrome/browser/syncGenericChange.properties
+++ b/browser/locales/en-US/chrome/browser/syncGenericChange.properties
@@ -1,35 +1,34 @@
-# LOCALIZATION NOTE (change.password.title): This (and associated change.password/passphrase) are used when the user elects to change their password.
+#LOCALIZATION NOTE (change.password.title): This (and associated change.password/passphrase) are used when the user elects to change their password.
 change.password.title = Change your Password
 change.password.acceptButton = Change Password
 change.password.status.active = Changing your password…
 change.password.status.success = Your password has been changed.
 change.password.status.error = There was an error changing your password.
 
-change.password2.introText = Your password must be at least 8 characters long.  It cannot be the same as either your user name or your Sync Key.
+change.password3.introText = Your password must be at least 8 characters long.  It cannot be the same as either your user name or your Recovery Key.
 change.password.warningText = Note: All of your other devices will be unable to connect to your account once you change this password.
 
-change.synckey2.title = My Sync Key
-change.synckey.acceptButton = Change Sync Key
-change.synckey.label = Changing Sync Key and uploading local data, please wait…
-change.synckey2.error = There was an error while changing your Sync Key!
-change.synckey2.success = Your Sync Key was successfully changed!
+change.recoverykey.title = My Recovery Key
+change.synckey.acceptButton = Change Recovery Key
+change.recoverykey.label = Changing Recovery Key and uploading local data, please wait…
+change.recoverykey.error = There was an error while changing your Recovery Key!
+change.recoverykey.success = Your Recovery Key was successfully changed!
 
 change.synckey.introText = Firefox Cares About Your Privacy
 change.synckey.introText2 = To ensure your total privacy, all of your data is encrypted prior to being uploaded. The key to decrypt your data is not uploaded.
-# LOCALIZATION NOTE (change.synckey2.warningText) "Sync" should match &syncBrand.shortName.label; from syncBrand.dtd
-change.synckey2.warningText = Note: Changing this will erase all data stored on the Sync server and upload new data secured by this Sync Key. Your other devices will not sync until the new Sync Key is entered for that device.
+# LOCALIZATION NOTE (change.recoverykey.warningText) "Sync" should match &syncBrand.shortName.label; from syncBrand.dtd
+change.recovery.warningText = Note: Changing this will erase all data stored on the Sync server and upload new data secured by this Recovery Key. Your other devices will not sync until the new Recovery Key is entered for that device.
 
-new.synckey.label = Your Sync Key
+new.recoverykey.label = Your Recovery Key
 
 # LOCALIZATION NOTE (new.password.title): This (and associated new.password/passphrase) are used on a second computer when it detects that your password or passphrase has been changed on a different device.
 new.password.title            = Update Password
 new.password.introText        = Your password was rejected by the server, please update your password.
 new.password.label            = Enter your new password
 new.password.confirm          = Confirm your new password
 new.password.acceptButton     = Update Password
 new.password.status.incorrect = Password incorrect, please try again.
 
-new.synckey.title          = Update Sync Key
-new.synckey2.introText       = Your Sync Key was changed using another device, please enter your updated Sync Key.
-new.synckey.acceptButton     = Update Sync Key
-new.synckey.status.incorrect = Sync Key incorrect, please try again.
+new.recoverykey.title          = Update Recovery Key
+new.recoverykey.introText      = Your Recovery Key was changed using another device, please enter your updated Recovery Key.
+new.recoverykey.acceptButton     = Update Recovery Key
--- a/browser/locales/en-US/chrome/browser/syncSetup.dtd
+++ b/browser/locales/en-US/chrome/browser/syncSetup.dtd
@@ -14,18 +14,18 @@
 <!-- New Account AND Existing Account -->
 <!ENTITY server.label               "Server">
 <!ENTITY serverType.main.label      "&syncBrand.fullName.label; Server">
 <!ENTITY serverType.custom2.label   "Use a custom server…">
 <!ENTITY signIn.account2.label      "Account">
 <!ENTITY signIn.account2.accesskey  "A">
 <!ENTITY signIn.password.label      "Password">
 <!ENTITY signIn.password.accesskey  "P">
-<!ENTITY signIn.syncKey.label       "Sync Key">
-<!ENTITY signIn.syncKey.accesskey   "K">
+<!ENTITY signIn.recoveryKey.label       "Recovery Key">
+<!ENTITY signIn.recoveryKey.accesskey   "K">
 
 <!-- New Account Page 1: Basic Account Info -->
 <!ENTITY setup.newAccountDetailsPage.title.label "Account Details">
 <!ENTITY setup.emailAddress.label     "Email Address">
 <!ENTITY setup.emailAddress.accesskey "E">
 <!ENTITY setup.choosePassword.label      "Choose a Password">
 <!ENTITY setup.choosePassword.accesskey  "P">
 <!ENTITY setup.confirmPassword.label     "Confirm Password">
@@ -36,22 +36,22 @@
 <!ENTITY setup.tosAgree1.accesskey  "a">
 <!ENTITY setup.tosLink.label        "Terms of Service">
 <!ENTITY setup.tosAgree2.label      "and the">
 <!ENTITY setup.ppLink.label         "Privacy Policy">
 <!ENTITY setup.tosAgree3.label      "">
 <!ENTITY setup.tosAgree2.accesskey  "">
 
 <!-- New Account Page 2: Sync Key -->
-<!ENTITY setup.newSyncKeyPage.title.label "&brandShortName; Cares About Your Privacy">
-<!ENTITY setup.newSyncKeyPage.description.label "To ensure your total privacy, all of your data is encrypted prior to being uploaded. The Sync Key which is necessary to decrypt your data is not uploaded.">
-<!ENTITY syncKeyEntry.label        "Your Sync Key">
-<!ENTITY syncKeyEntry.accesskey    "K">
+<!ENTITY setup.newRecoveryKeyPage.title.label "&brandShortName; Cares About Your Privacy">
+<!ENTITY setup.newRecoveryKeyPage.description.label "To ensure your total privacy, all of your data is encrypted prior to being uploaded. The Recovery Key which is necessary to decrypt your data is not uploaded.">
+<!ENTITY recoveryKeyEntry.label        "Your Recovery Key">
+<!ENTITY recoveryKeyEntry.accesskey    "K">
 <!ENTITY syncGenerateNewKey.label  "Generate a new key">
-<!ENTITY syncKeyBackup.description "Your Sync Key is required to access &syncBrand.fullName.label; on other machines. Please create a backup copy. We cannot help you recover your Sync Key.">
+<!ENTITY recoveryKeyBackup.description "Your Recovery Key is required to access &syncBrand.fullName.label; on other machines. Please create a backup copy. We cannot help you recover your Recovery Key.">
 
 <!ENTITY button.syncKeyBackup.print.label     "Print…">
 <!ENTITY button.syncKeyBackup.print.accesskey "P">
 <!ENTITY button.syncKeyBackup.save.label      "Save…">
 <!ENTITY button.syncKeyBackup.save.accesskey  "S">
 
 <!-- New Account Page 3: Captcha -->
 <!ENTITY setup.captchaPage2.title.label     "Please Confirm You're Not a Robot">
@@ -60,22 +60,22 @@
 <!ENTITY addDevice.showMeHow.label          "Show me how.">
 <!ENTITY addDevice.dontHaveDevice.label     "I don't have the device with me">
 <!ENTITY addDevice.setup.description.label  "To activate, go to &syncBrand.shortName.label; Options on your other device and select &#x0022;Add a Device&#x0022;.">
 <!ENTITY addDevice.setup.enterCode.label    "Then, enter this code:">
 <!ENTITY addDevice.dialog.description.label "To activate your new device, go to &syncBrand.shortName.label; Options on the device and select &#x0022;Connect.&#x0022;">
 <!ENTITY addDevice.dialog.enterCode.label   "Enter the code that the device provides:">
 <!ENTITY addDevice.dialog.tryAgain.label    "Please try again.">
 <!ENTITY addDevice.dialog.successful.label  "The device has been successfully added. The initial synchronization can take several minutes and will finish in the background.">
-<!ENTITY addDevice.dialog.syncKey.label     "To activate your device you will need to enter your Sync Key. Please print or save this key and take it with you.">
+<!ENTITY addDevice.dialog.recoveryKey.label     "To activate your device you will need to enter your Recovery Key. Please print or save this key and take it with you.">
 <!ENTITY addDevice.dialog.connected.label   "Device Connected">
 
 <!-- Existing Account Page 2: Manual Login -->
 <!ENTITY setup.signInPage.title.label "Sign In">
-<!ENTITY existingSyncKey.description "You can get a copy of your Sync Key by going to &syncBrand.shortName.label; Options on your other device, and selecting  &#x0022;My Sync Key&#x0022; under &#x0022;Manage Account&#x0022;.">
+<!ENTITY existingRecoveryKey.description "You can get a copy of your Recovery Key by going to &syncBrand.shortName.label; Options on your other device, and selecting  &#x0022;My Recovery Key&#x0022; under &#x0022;Manage Account&#x0022;.">
 <!ENTITY verifying.label              "Verifying…">
 <!ENTITY resetPassword.label          "Reset Password">
 <!ENTITY resetSyncKey.label           "I have lost my other device.">
 
 <!-- Sync Options -->
 <!ENTITY setup.optionsPage.title      "Sync Options">
 <!ENTITY syncComputerName.label       "Computer Name:">
 <!ENTITY syncComputerName.accesskey   "c">
--- a/browser/locales/en-US/chrome/browser/syncSetup.properties
+++ b/browser/locales/en-US/chrome/browser/syncSetup.properties
@@ -24,18 +24,18 @@ bookmarksCount.label        = #1 bookmar
 # #1 is the number of days (was %S for a short while, use #1 instead, even if both work)
 historyDaysCount.label      = #1 day of history;#1 days of history
 # LOCALIZATION NOTE (passwordsCount.label):
 # Semi-colon list of plural forms. See:
 # http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #1 is the number of passwords (was %S for a short while, use #1 instead, even if both work)
 passwordsCount.label        = #1 password;#1 passwords
 
-save.synckey.title = Save Sync Key
-save.default.label = Firefox Sync Key.html
+save.recoverykey.title = Save Recovery Key
+save.recoverykey.defaultfilename = Firefox Recovery Key.html
 
 newAccount.action.label = Firefox Sync is now set up to automatically sync all of your browser data.
 newAccount.change.label = You can choose exactly what to sync by selecting Sync Options below.
 resetClient.change.label = Firefox Sync will now merge all this computer's browser data into your Sync account.
 wipeClient.change.label = Firefox Sync will now replace all of the browser data on this computer with the data in your Sync account.
 wipeRemote.change.label = Firefox Sync will now replace all of the browser data in your Sync account with the data on this computer.
 existingAccount.change.label = You can change this preference by selecting Sync Options below.
 
--- a/mobile/chrome/content/browser.xul
+++ b/mobile/chrome/content/browser.xul
@@ -567,17 +567,17 @@
           </hbox>
         </vbox>
         <vbox id="syncsetup-fallback" class="syncsetup-page" flex="1" hidden="true">
           <scrollbox class="prompt-message" orient="vertical" flex="1">
             <description class="syncsetup-desc syncsetup-center" flex="1">&sync.setup.manual;</description>
             <separator/>
             <textbox id="syncsetup-account" class="prompt-edit" placeholder="&sync.account;" oninput="WeaveGlue.canConnect();"/>
             <textbox id="syncsetup-password" class="prompt-edit" placeholder="&sync.password;" type="password" oninput="WeaveGlue.canConnect();"/>
-            <textbox id="syncsetup-synckey" class="prompt-edit" placeholder="&sync.syncKey;" oninput="WeaveGlue.canConnect();"/>
+            <textbox id="syncsetup-synckey" class="prompt-edit" placeholder="&sync.recoveryKey;" oninput="WeaveGlue.canConnect();"/>
             <separator class="thin"/>
             <button id="syncsetup-usecustomserver" type="checkbox" class="button-checkbox" pack="start" oncommand="WeaveGlue.toggleCustomServer();">
               <image class="button-image-icon"/>
               <description class="syncsetup-label prompt-checkbox-label" flex="1">&sync.customServer;</description>
             </button>
             <textbox id="syncsetup-customserver" class="prompt-edit" placeholder="&sync.serverURL;"/>
             <separator flex="1"/>
           </scrollbox>
--- a/mobile/locales/en-US/chrome/sync.dtd
+++ b/mobile/locales/en-US/chrome/sync.dtd
@@ -9,14 +9,14 @@
 <!ENTITY sync.syncNow               "Sync Now">
 
 <!ENTITY sync.setup.title           "Connect to Sync">
 <!ENTITY sync.setup.jpake           "From a Firefox Sync-connected computer, go to Sync options and select &#x0022;Add a device&#x0022;">
 <!ENTITY sync.fallback              "I'm not near my computer…">
 <!ENTITY sync.setup.manual          "Enter your Sync account information">
 <!ENTITY sync.account               "Account Name">
 <!ENTITY sync.password              "Password">
-<!ENTITY sync.syncKey               "Sync Key">
+<!ENTITY sync.recoveryKey           "Recovery Key">
 <!ENTITY sync.customServer          "Use custom server">
 <!ENTITY sync.serverURL             "Server URL">
 <!ENTITY sync.setup.connect         "Connect">
 <!ENTITY sync.setup.cancel          "Cancel">
 <!ENTITY sync.setup.tutorial        "Show me how">
--- a/services/sync/locales/en-US/errors.properties
+++ b/services/sync/locales/en-US/errors.properties
@@ -1,28 +1,22 @@
 error.login.reason.network      = Failed to connect to the server
-error.login.reason.synckey      = Wrong Sync Key
+error.login.reason.recoverykey  = Wrong Recovery Key
 error.login.reason.account      = Incorrect account name or password
 error.login.reason.no_username  = Missing account name
 error.login.reason.no_password2 = Missing password
-error.login.reason.no_synckey   = No saved Sync Key to use
+error.login.reason.no_recoverykey= No saved Recovery Key to use
 error.login.reason.server       = Server incorrectly configured
 
 error.sync.failed_partial     = One or more data types could not be synced
 
 invalid-captcha = Incorrect words, try again
 weak-password   = Use a stronger password
 
 # this is the fallback, if we hit an error we didn't bother to localize
 error.reason.unknown          = Unknown error
 
-change.synckey.sameAsSyncKey    = The new Sync Key cannot be the same as your Sync Key
-change.synckey.sameAsPassword   = The Sync Key cannot be the same as your password
-change.synckey.sameAsUsername   = The Sync Key cannot be the same as your user name
-change.synckey.sameAsEmail      = The Sync Key cannot be the same as your email address
-change.synckey.tooShort         = The Sync Key entered is too short
-
-change.password.pwSameAsSyncKey      = Password can't match your Sync Key
+change.password.pwSameAsRecoveryKey  = Password can't match your Recovery Key
 change.password.pwSameAsPassword     = Password can't match current password
 change.password.pwSameAsUsername     = Password can't match your user name
 change.password.pwSameAsEmail        = Password can't match your email address
 change.password.mismatch             = The passwords entered do not match
 change.password.tooShort             = The password entered is too short
--- a/services/sync/modules/async.js
+++ b/services/sync/modules/async.js
@@ -295,17 +295,17 @@ let Async = {
         return;
       }
       if (!this.results) {
         this.results = [];
       }
       let row;
       while ((row = results.getNextRow()) != null) {
         let item = {};
-        for each (name in this.names) {
+        for each (let name in this.names) {
           item[name] = row.getResultByName(name);
         }
         this.results.push(item);
       }
     },
     handleError: function handleError(error) {
       this.syncCb.throw(error);
     },
--- a/services/sync/modules/constants.js
+++ b/services/sync/modules/constants.js
@@ -142,20 +142,20 @@ MASTER_PASSWORD_LOCKED:                "
 // success states
 LOGIN_SUCCEEDED:                       "success.login",
 SYNC_SUCCEEDED:                        "success.sync",
 ENGINE_SUCCEEDED:                      "success.engine",
 
 // login failure status codes:
 LOGIN_FAILED_NO_USERNAME:              "error.login.reason.no_username",
 LOGIN_FAILED_NO_PASSWORD:              "error.login.reason.no_password2",
-LOGIN_FAILED_NO_PASSPHRASE:            "error.login.reason.no_synckey",
+LOGIN_FAILED_NO_PASSPHRASE:            "error.login.reason.no_recoverykey",
 LOGIN_FAILED_NETWORK_ERROR:            "error.login.reason.network",
 LOGIN_FAILED_SERVER_ERROR:             "error.login.reason.server",
-LOGIN_FAILED_INVALID_PASSPHRASE:       "error.login.reason.synckey",
+LOGIN_FAILED_INVALID_PASSPHRASE:       "error.login.reason.recoverykey",
 LOGIN_FAILED_LOGIN_REJECTED:           "error.login.reason.account",
 
 // sync failure status codes
 METARECORD_DOWNLOAD_FAIL:              "error.sync.reason.metarecord_download_fail",
 VERSION_OUT_OF_DATE:                   "error.sync.reason.version_out_of_date",
 DESKTOP_VERSION_OUT_OF_DATE:           "error.sync.reason.desktop_version_out_of_date",
 SETUP_FAILED_NO_PASSPHRASE:            "error.sync.reason.setup_failed_no_passphrase",
 CREDENTIALS_CHANGED:                   "error.sync.reason.credentials_changed",
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -617,26 +617,27 @@ SyncEngine.prototype = {
     // this._modified to the tracker.
     this.lastSyncLocal = Date.now();
     if (this.lastSync) {
       this._modified = this.getChangedIDs();
     } else {
       // Mark all items to be uploaded, but treat them as changed from long ago
       this._log.debug("First sync, uploading all items");
       this._modified = {};
-      for (let id in this._store.getAllIDs())
+      for (let id in this._store.getAllIDs()) {
         this._modified[id] = 0;
+      }
     }
     // Clear the tracker now. If the sync fails we'll add the ones we failed
     // to upload back.
     this._tracker.clearChangedIDs();
  
     // Array of just the IDs from this._modified. This is what we iterate over
     // so we can modify this._modified during the iteration.
-    this._modifiedIDs = [id for (id in this._modified)];
+    this._modifiedIDs = Object.keys(this._modified);
     this._log.info(this._modifiedIDs.length +
                    " outgoing items pre-reconciliation");
 
     // Keep track of what to delete at the end of sync
     this._delete = {};
   },
 
   // Process incoming records
@@ -647,17 +648,17 @@ SyncEngine.prototype = {
     let batchSize = Infinity;
     let newitems = new Collection(this.engineURL, this._recordObj);
     let isMobile = (Svc.Prefs.get("client.type") == "mobile");
 
     if (isMobile) {
       batchSize = MOBILE_BATCH_SIZE;
     }
     newitems.newer = this.lastSync;
-    newitems.full = true;
+    newitems.full  = true;
     newitems.limit = batchSize;
     
     // applied    => number of items that should be applied.
     // failed     => number of items that failed in this sync.
     // newFailed  => number of items that failed for the first time in this sync.
     // reconciled => number of items that were reconciled.
     let count = {applied: 0, failed: 0, newFailed: 0, reconciled: 0};
     let handled = [];
@@ -997,17 +998,17 @@ SyncEngine.prototype = {
           throw resp;
         }
 
         // Update server timestamp from the upload.
         let modified = resp.headers["x-weave-timestamp"];
         if (modified > this.lastSync)
           this.lastSync = modified;
 
-        let failed_ids = [id for (id in resp.obj.failed)];
+        let failed_ids = Object.keys(resp.obj.failed);
         if (failed_ids.length)
           this._log.debug("Records that will be uploaded again because "
                           + "the server couldn't store them: "
                           + failed_ids.join(", "));
 
         // Clear successfully uploaded objects.
         for each (let id in resp.obj.success) {
           delete this._modified[id];
@@ -1074,18 +1075,18 @@ SyncEngine.prototype = {
   _syncCleanup: function _syncCleanup() {
     if (!this._modified)
       return;
 
     // Mark failed WBOs as changed again so they are reuploaded next time.
     for (let [id, when] in Iterator(this._modified)) {
       this._tracker.addChangedID(id, when);
     }
-    delete this._modified;
-    delete this._modifiedIDs;
+    this._modified    = {};
+    this._modifiedIDs = [];
   },
 
   _sync: function SyncEngine__sync() {
     try {
       this._syncStartup();
       Observers.notify("weave:engine:sync:status", "process-incoming");
       this._processIncoming();
       Observers.notify("weave:engine:sync:status", "upload-outgoing");
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -108,40 +108,44 @@ PlacesItem.prototype = {
     }
     throw "Unknown places item object type: " + type;
   },
 
   __proto__: CryptoWrapper.prototype,
   _logName: "Sync.Record.PlacesItem",
 };
 
-Utils.deferGetSet(PlacesItem, "cleartext", ["hasDupe", "parentid", "parentName",
-                                            "type"]);
+Utils.deferGetSet(PlacesItem,
+                  "cleartext",
+                  ["hasDupe", "parentid", "parentName", "type"]);
 
 function Bookmark(collection, id, type) {
   PlacesItem.call(this, collection, id, type || "bookmark");
 }
 Bookmark.prototype = {
   __proto__: PlacesItem.prototype,
   _logName: "Sync.Record.Bookmark",
 };
 
-Utils.deferGetSet(Bookmark, "cleartext", ["title", "bmkUri", "description",
-  "loadInSidebar", "tags", "keyword"]);
+Utils.deferGetSet(Bookmark,
+                  "cleartext",
+                  ["title", "bmkUri", "description",
+                   "loadInSidebar", "tags", "keyword"]);
 
 function BookmarkQuery(collection, id) {
   Bookmark.call(this, collection, id, "query");
 }
 BookmarkQuery.prototype = {
   __proto__: Bookmark.prototype,
   _logName: "Sync.Record.BookmarkQuery",
 };
 
-Utils.deferGetSet(BookmarkQuery, "cleartext", ["folderName",
-                                               "queryId"]);
+Utils.deferGetSet(BookmarkQuery,
+                  "cleartext",
+                  ["folderName", "queryId"]);
 
 function BookmarkFolder(collection, id, type) {
   PlacesItem.call(this, collection, id, type || "folder");
 }
 BookmarkFolder.prototype = {
   __proto__: PlacesItem.prototype,
   _logName: "Sync.Record.Folder",
 };
@@ -165,24 +169,16 @@ function BookmarkSeparator(collection, i
 BookmarkSeparator.prototype = {
   __proto__: PlacesItem.prototype,
   _logName: "Sync.Record.Separator",
 };
 
 Utils.deferGetSet(BookmarkSeparator, "cleartext", "pos");
 
 
-function archiveBookmarks() {
-  // Some nightly builds of 3.7 don't have this function
-  try {
-    PlacesUtils.archiveBookmarksFile(null, true);
-  }
-  catch(ex) {}
-}
-
 let kSpecialIds = {
 
   // Special IDs. Note that mobile can attempt to create a record on
   // dereference; special accessors are provided to prevent recursion within
   // observers.
   guids: ["menu", "places", "tags", "toolbar", "unfiled", "mobile"],
 
   // Create the special mobile folder to store mobile bookmarks.
@@ -384,18 +380,19 @@ BookmarksEngine.prototype = {
     this._log.trace("No dupe found for key " + key + "/" + altKey + ".");
     return undefined;
   },
 
   _syncStartup: function _syncStart() {
     SyncEngine.prototype._syncStartup.call(this);
 
     // For first-syncs, make a backup for the user to restore
-    if (this.lastSync == 0)
-      archiveBookmarks();
+    if (this.lastSync == 0) {
+      PlacesUtils.archiveBookmarksFile(null, true);
+    }
 
     this.__defineGetter__("_guidMap", function() {
       // Create a mapping of folder titles and separator positions to GUID.
       // We do this lazily so that we don't do any work unless we reconcile
       // incoming items.
       let guidMap;
       try {
         guidMap = this._buildGUIDMap();
@@ -1169,17 +1166,17 @@ BookmarksStore.prototype = {
       return FOLDER_SORTINDEX;
 
     // For anything directly under the toolbar, give it a boost of more than an
     // unvisited bookmark
     let index = 0;
     if (record.parentid == "toolbar")
       index += 150;
 
-    // Add in the bookmark's frecency if we have something
+    // Add in the bookmark's frecency if we have something.
     if (record.bmkUri != null) {
       this._frecencyStm.params.url = record.bmkUri;
       let result = Async.querySpinningly(this._frecencyStm, this._frecencyCols);
       if (result.length)
         index += result[0].frecency;
     }
 
     return index;
@@ -1212,20 +1209,20 @@ BookmarksStore.prototype = {
         node.containerOpen = false;
       }
     }
 
     return items;
   },
 
   _tagURI: function BStore_tagURI(bmkURI, tags) {
-    // Filter out any null/undefined/empty tags
+    // Filter out any null/undefined/empty tags.
     tags = tags.filter(function(t) t);
 
-    // Temporarily tag a dummy uri to preserve tag ids when untagging
+    // Temporarily tag a dummy URI to preserve tag ids when untagging.
     let dummyURI = Utils.makeURI("about:weave#BStore_tagURI");
     PlacesUtils.tagging.tagURI(dummyURI, tags);
     PlacesUtils.tagging.untagURI(bmkURI, null);
     PlacesUtils.tagging.tagURI(bmkURI, tags);
     PlacesUtils.tagging.untagURI(dummyURI, null);
   },
 
   getAllIDs: function BStore_getAllIDs() {
@@ -1234,18 +1231,18 @@ BookmarksStore.prototype = {
     for each (let guid in kSpecialIds.guids) {
       if (guid != "places" && guid != "tags")
         this._getChildren(guid, items);
     }
     return items;
   },
 
   wipe: function BStore_wipe() {
-    // Save a backup before clearing out all bookmarks
-    archiveBookmarks();
+    // Save a backup before clearing out all bookmarks.
+    PlacesUtils.archiveBookmarksFile(null, true);
 
     for each (let guid in kSpecialIds.guids)
       if (guid != "places") {
         let id = kSpecialIds.specialIdForGUID(guid);
         if (id)
           PlacesUtils.bookmarks.removeFolderChildren(id);
       }
   }
@@ -1305,35 +1302,35 @@ BookmarksTracker.prototype = {
 
   QueryInterface: XPCOMUtils.generateQI([
     Ci.nsINavBookmarkObserver,
     Ci.nsINavBookmarkObserver_MOZILLA_1_9_1_ADDITIONS,
     Ci.nsISupportsWeakReference
   ]),
 
   /**
-   * Add a bookmark guid to be uploaded and bump up the sync score
+   * Add a bookmark GUID to be uploaded and bump up the sync score.
    *
    * @param itemGuid
-   *        Guid of the bookmark to upload
+   *        GUID of the bookmark to upload.
    */
   _add: function BMT__add(itemId, guid) {
     guid = kSpecialIds.specialGUIDForId(itemId) || guid;
     if (this.addChangedID(guid))
       this._upScore();
   },
 
-  /* Every add/remove/change will trigger a sync for MULTI_DEVICE */
+  /* Every add/remove/change will trigger a sync for MULTI_DEVICE. */
   _upScore: function BMT__upScore() {
     this.score += SCORE_INCREMENT_XLARGE;
   },
 
   /**
    * Determine if a change should be ignored: we're ignoring everything or the
-   * folder is for livemarks
+   * folder is for livemarks.
    *
    * @param itemId
    *        Item under consideration to ignore
    * @param folder (optional)
    *        Folder of the item being changed
    */
   _ignore: function BMT__ignore(itemId, folder, guid) {
     // Ignore unconditionally if the engine tells us to.
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -194,17 +194,18 @@ ClientEngine.prototype = {
    * and the value is a hash containing information about the command such as
    * number of arguments and description.
    */
   _commands: {
     resetAll:    { args: 0, desc: "Clear temporary local data for all engines" },
     resetEngine: { args: 1, desc: "Clear temporary local data for engine" },
     wipeAll:     { args: 0, desc: "Delete all client data for all engines" },
     wipeEngine:  { args: 1, desc: "Delete all client data for engine" },
-    logout:      { args: 0, desc: "Log out client" }
+    logout:      { args: 0, desc: "Log out client" },
+    displayURI:  { args: 2, desc: "Instruct a client to display a URI" }
   },
 
   /**
    * Remove any commands for the local client and mark it for upload.
    */
   clearCommands: function clearCommands() {
     delete this.localCommands;
     this._tracker.addChangedID(this.localID);
@@ -279,16 +280,19 @@ ClientEngine.prototype = {
             engines = null;
             // Fallthrough
           case "wipeEngine":
             Weave.Service.wipeClient(engines);
             break;
           case "logout":
             Weave.Service.logout();
             return false;
+          case "displayURI":
+            this._handleDisplayURI(args[0], args[1]);
+            break;
           default:
             this._log.debug("Received an unknown command: " + command);
             break;
         }
       }
 
       return true;
     })();
@@ -325,16 +329,62 @@ ClientEngine.prototype = {
 
     if (clientId) {
       this._sendCommandToClient(command, args, clientId);
     } else {
       for (let id in this._store._remoteClients) {
         this._sendCommandToClient(command, args, id);
       }
     }
+  },
+
+  /**
+   * Send a URI to another client for display.
+   *
+   * A side effect is the score is increased dramatically to incur an
+   * immediate sync.
+   *
+   * If an unknown client ID is specified, sendCommand() will throw an
+   * Error object.
+   *
+   * @param uri
+   *        URI (as a string) to send and display on the remote client
+   * @param clientId
+   *        ID of client to send the command to. If not defined, will be sent
+   *        to all remote clients.
+   */
+  sendURIToClientForDisplay: function sendURIToClientForDisplay(uri, clientId) {
+    this._log.info("Sending URI to client: " + uri + " -> " + clientId);
+    this.sendCommand("displayURI", [uri, this.syncID], clientId);
+
+    Clients._tracker.score += SCORE_INCREMENT_XLARGE;
+  },
+
+  /**
+   * Handle a single received 'displayURI' command.
+   *
+   * Interested parties should observe the "weave:engine:clients:display-uri"
+   * topic. The callback will receive an object as the subject parameter with
+   * the following keys:
+   *
+   *   uri       URI (string) that is requested for display
+   *   clientId  ID of client that sent the command
+   *
+   * The 'data' parameter to the callback will not be defined.
+   *
+   * @param uri
+   *        String URI that was received
+   * @param clientId
+   *        ID of client that sent URI
+   */
+  _handleDisplayURI: function _handleDisplayURI(uri, clientId) {
+    this._log.info("Received a URI for display: " + uri + " from " + clientId);
+
+    let subject = { uri: uri, client: clientId };
+    Svc.Obs.notify("weave:engine:clients:display-uri", subject);
   }
 };
 
 function ClientStore(name) {
   Store.call(this, name);
 }
 ClientStore.prototype = {
   __proto__: Store.prototype,
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -59,84 +59,124 @@ FormRec.prototype = {
   ttl: FORMS_TTL
 };
 
 Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]);
 
 
 let FormWrapper = {
   _log: Log4Moz.repository.getLogger("Sync.Engine.Forms"),
-    
-  getAllEntries: function getAllEntries() {
-    // Sort by (lastUsed - minLast) / (maxLast - minLast) * timesUsed / maxTimes
-    let query = Svc.Form.DBConnection.createAsyncStatement(
+
+  _getEntryCols: ["name", "value"],
+  _guidCols:     ["guid"],
+
+  _stmts: {},
+  _getStmt: function _getStmt(query) {
+    if (query in this._stmts) {
+      return this._stmts[query];
+    }
+
+    this._log.trace("Creating SQL statement: " + query);
+    let db = Svc.Form.DBConnection;
+    return this._stmts[query] = db.createAsyncStatement(query);
+  },
+
+  get _getAllEntriesStmt() {
+    const query =
       "SELECT fieldname name, value FROM moz_formhistory " +
       "ORDER BY 1.0 * (lastUsed - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) / " +
         "((SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed DESC LIMIT 1) - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) * " +
         "timesUsed / (SELECT timesUsed FROM moz_formhistory ORDER BY timesUsed DESC LIMIT 1) DESC " +
-      "LIMIT 500");
-    return Async.querySpinningly(query, ["name", "value"]);
+      "LIMIT 500";
+    return this._getStmt(query);
+  },
+
+  get _getEntryStmt() {
+    const query = "SELECT fieldname name, value FROM moz_formhistory " +
+                  "WHERE guid = :guid";
+    return this._getStmt(query);
+  },
+
+  get _getGUIDStmt() {
+    const query = "SELECT guid FROM moz_formhistory " +
+                  "WHERE fieldname = :name AND value = :value";
+    return this._getStmt(query);
+  },
+
+  get _setGUIDStmt() {
+    const query = "UPDATE moz_formhistory SET guid = :guid " +
+                  "WHERE fieldname = :name AND value = :value";
+    return this._getStmt(query);
+  },
+
+  get _hasGUIDStmt() {
+    const query = "SELECT guid FROM moz_formhistory WHERE guid = :guid LIMIT 1";
+    return this._getStmt(query);
+  },
+
+  get _replaceGUIDStmt() {
+    const query = "UPDATE moz_formhistory SET guid = :newGUID " +
+                  "WHERE guid = :oldGUID";
+    return this._getStmt(query);
+  },
+
+  getAllEntries: function getAllEntries() {
+    return Async.querySpinningly(this._getAllEntriesStmt, this._getEntryCols);
   },
 
   getEntry: function getEntry(guid) {
-    let query = Svc.Form.DBConnection.createAsyncStatement(
-      "SELECT fieldname name, value FROM moz_formhistory WHERE guid = :guid");
-    query.params.guid = guid;
-    return Async.querySpinningly(query, ["name", "value"])[0];
+    let stmt = this._getEntryStmt;
+    stmt.params.guid = guid;
+    return Async.querySpinningly(stmt, this._getEntryCols)[0];
   },
 
   getGUID: function getGUID(name, value) {
-    // Query for the provided entry
-    let getQuery = Svc.Form.DBConnection.createAsyncStatement(
-      "SELECT guid FROM moz_formhistory " +
-      "WHERE fieldname = :name AND value = :value");
-    getQuery.params.name = name;
-    getQuery.params.value = value;
+    // Query for the provided entry.
+    let getStmt = this._getGUIDStmt;
+    getStmt.params.name = name;
+    getStmt.params.value = value;
 
-    // Give the guid if we found one
-    let item = Async.querySpinningly(getQuery, ["guid"])[0];
-    
+    // Give the GUID if we found one.
+    let item = Async.querySpinningly(getStmt, this._guidCols)[0];
+
     if (!item) {
       // Shouldn't happen, but Bug 597400...
       // Might as well just return.
       this._log.warn("GUID query returned " + item + "; turn on Trace logging for details.");
       this._log.trace("getGUID(" + JSON.stringify(name) + ", " +
                       JSON.stringify(value) + ") => " + item);
       return null;
     }
-    
-    if (item.guid != null)
+
+    if (item.guid != null) {
       return item.guid;
+    }
 
-    // We need to create a guid for this entry
-    let setQuery = Svc.Form.DBConnection.createAsyncStatement(
-      "UPDATE moz_formhistory SET guid = :guid " +
-      "WHERE fieldname = :name AND value = :value");
+    // We need to create a GUID for this entry.
+    let setStmt = this._setGUIDStmt;
     let guid = Utils.makeGUID();
-    setQuery.params.guid = guid;
-    setQuery.params.name = name;
-    setQuery.params.value = value;
-    Async.querySpinningly(setQuery);
+    setStmt.params.guid = guid;
+    setStmt.params.name = name;
+    setStmt.params.value = value;
+    Async.querySpinningly(setStmt);
 
     return guid;
   },
 
   hasGUID: function hasGUID(guid) {
-    let query = Svc.Form.DBConnection.createAsyncStatement(
-      "SELECT guid FROM moz_formhistory WHERE guid = :guid LIMIT 1");
-    query.params.guid = guid;
-    return Async.querySpinningly(query, ["guid"]).length == 1;
+    let stmt = this._hasGUIDStmt;
+    stmt.params.guid = guid;
+    return Async.querySpinningly(stmt, this._guidCols).length == 1;
   },
 
   replaceGUID: function replaceGUID(oldGUID, newGUID) {
-    let query = Svc.Form.DBConnection.createAsyncStatement(
-      "UPDATE moz_formhistory SET guid = :newGUID WHERE guid = :oldGUID");
-    query.params.oldGUID = oldGUID;
-    query.params.newGUID = newGUID;
-    Async.querySpinningly(query);
+    let stmt = this._replaceGUIDStmt;
+    stmt.params.oldGUID = oldGUID;
+    stmt.params.newGUID = newGUID;
+    Async.querySpinningly(stmt);
   }
 
 };
 
 function FormEngine() {
   SyncEngine.call(this, "Forms");
 }
 FormEngine.prototype = {
@@ -144,18 +184,19 @@ FormEngine.prototype = {
   _storeObj: FormStore,
   _trackerObj: FormTracker,
   _recordObj: FormRec,
   applyIncomingBatchSize: FORMS_STORE_BATCH_SIZE,
 
   get prefName() "history",
 
   _findDupe: function _findDupe(item) {
-    if (Svc.Form.entryExists(item.name, item.value))
+    if (Svc.Form.entryExists(item.name, item.value)) {
       return FormWrapper.getGUID(item.name, item.value);
+    }
   }
 };
 
 function FormStore(name) {
   Store.call(this, name);
 }
 FormStore.prototype = {
   __proto__: Store.prototype,
@@ -168,18 +209,19 @@ FormStore.prototype = {
 
   applyIncoming: function applyIncoming(record) {
     Store.prototype.applyIncoming.call(this, record);
     this._sleep(0); // Yield back to main thread after synchronous operation.
   },
 
   getAllIDs: function FormStore_getAllIDs() {
     let guids = {};
-    for each (let {name, value} in FormWrapper.getAllEntries())
+    for each (let {name, value} in FormWrapper.getAllEntries()) {
       guids[FormWrapper.getGUID(name, value)] = true;
+    }
     return guids;
   },
 
   changeItemID: function FormStore_changeItemID(oldID, newID) {
     FormWrapper.replaceGUID(oldID, newID);
   },
 
   itemExists: function FormStore_itemExists(id) {
@@ -187,34 +229,35 @@ FormStore.prototype = {
   },
 
   createRecord: function createRecord(id, collection) {
     let record = new FormRec(collection, id);
     let entry = FormWrapper.getEntry(id);
     if (entry != null) {
       record.name = entry.name;
       record.value = entry.value;
+    } else {
+      record.deleted = true;
     }
-    else
-      record.deleted = true;
     return record;
   },
 
   create: function FormStore_create(record) {
     this._log.trace("Adding form record for " + record.name);
     Svc.Form.addEntry(record.name, record.value);
   },
 
   remove: function FormStore_remove(record) {
     this._log.trace("Removing form record: " + record.id);
 
     // Just skip remove requests for things already gone
     let entry = FormWrapper.getEntry(record.id);
-    if (entry == null)
+    if (entry == null) {
       return;
+    }
 
     Svc.Form.removeEntry(entry.name, entry.value);
   },
 
   update: function FormStore_update(record) {
     this._log.trace("Ignoring form record update request!");
   },
 
@@ -276,18 +319,19 @@ FormTracker.prototype = {
                              .toString();
           this.trackEntry(name, value);
         }
         break;
     }
   },
 
   notify: function FormTracker_notify(formElement, aWindow, actionURI) {
-    if (this.ignoreAll)
+    if (this.ignoreAll) {
       return;
+    }
 
     this._log.trace("Form submission notification for " + actionURI.spec);
 
     // XXX Bug 487541 Copy the logic from nsFormHistory::Notify to avoid
     // divergent logic, which can lead to security issues, until there's a
     // better way to get satchel's results like with a notification.
 
     // Determine if a dom node has the autocomplete attribute set to "off"
@@ -304,18 +348,19 @@ FormTracker.prototype = {
     /* Get number of elements in form, add points and changedIDs */
     let len = formElement.length;
     let elements = formElement.elements;
     for (let i = 0; i < len; i++) {
       let el = elements.item(i);
 
       // Grab the name for debugging, but check if empty when satchel would
       let name = el.name;
-      if (name === "")
+      if (name === "") {
         name = el.id;
+      }
 
       if (!(el instanceof Ci.nsIDOMHTMLInputElement)) {
         this._log.trace(name + " is not a DOMHTMLInputElement: " + el);
         continue;
       }
 
       if (el.type.search(/^text$/i) != 0) {
         this._log.trace(name + "'s type is not 'text': " + el.type);
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -41,17 +41,16 @@
 const EXPORTED_SYMBOLS = ['HistoryEngine', 'HistoryRec'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 const Cr = Components.results;
 
 const HISTORY_TTL = 5184000; // 60 days
-const TOPIC_UPDATEPLACES_COMPLETE = "places-updatePlaces-complete";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/log4moz.js");
@@ -249,30 +248,25 @@ HistoryStore.prototype = {
     }
     records.length = k; // truncate array
 
     // Nothing to do.
     if (!records.length) {
       return failed;
     }
 
-    let cb = Async.makeSyncCallback();
     let updatePlacesCallback = { 
       handleResult: function handleResult() {},
       handleError: function handleError(resultCode, placeInfo) {
         failed.push(placeInfo.guid);
-      }
+      },
+      handleCompletion: Async.makeSyncCallback()
     };
-    let onComplete = function onComplete(subject, topic, data) {
-      Svc.Obs.remove(TOPIC_UPDATEPLACES_COMPLETE, onComplete);
-      cb();
-    };
-    Svc.Obs.add(TOPIC_UPDATEPLACES_COMPLETE, onComplete);
     this._asyncHistory.updatePlaces(records, updatePlacesCallback);
-    Async.waitForSyncCallback(cb);
+    Async.waitForSyncCallback(updatePlacesCallback.handleCompletion);
     return failed;
   },
 
   /**
    * Converts a Sync history record to a mozIPlaceInfo.
    * 
    * Throws if an invalid record is encountered (invalid URI, etc.),
    * returns true if the record is to be applied, false otherwise
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -231,17 +231,17 @@ let SyncScheduler = {
       this.syncInterval = this.immediateInterval;
     } else {
       this._log.trace("Adjusting syncInterval to activeInterval.");
       this.syncInterval = this.activeInterval;
     }
   },
 
   calculateScore: function calculateScore() {
-    var engines = Engines.getEnabled();
+    let engines = [Clients].concat(Engines.getEnabled());
     for (let i = 0;i < engines.length;i++) {
       this._log.trace(engines[i].name + ": score: " + engines[i].score);
       this.globalScore += engines[i].score;
       engines[i]._tracker.resetScore();
     }
 
     this._log.trace("Global score updated: " + this.globalScore);
     this.checkSyncStatus();
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -1572,17 +1572,17 @@ WeaveSvc.prototype = {
       } else {
         // The engine was enabled remotely. Enable it locally.
         this._log.trace(engineName + " engine was enabled remotely.");
         engine.enabled = true;
       }
     }
 
     // Any remaining engines were either enabled locally or disabled remotely.
-    for each (engineName in enabled) {
+    for each (let engineName in enabled) {
       let engine = Engines.get(engineName);
       if (Svc.Prefs.get("engineStatusChanged." + engine.prefName, false)) {
         this._log.trace("The " + engineName + " engine was enabled locally.");
       } else {
         this._log.trace("The " + engineName + " engine was disabled remotely.");
         engine.enabled = false;
       }
     }
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -551,20 +551,16 @@ let Utils = {
     let iterations = Math.ceil(len/BLOCKSIZE);
     for (let i = 0; i < iterations; i++) {
       Tn = Utils.digestBytes(Tn + info + String.fromCharCode(i + 1), h);
       T += Tn;
     }
     return T.slice(0, len);
   },
 
-  byteArrayToString: function byteArrayToString(bytes) {
-    return [String.fromCharCode(byte) for each (byte in bytes)].join("");
-  },
-  
   /**
    * PBKDF2 implementation in Javascript.
    * 
    * The arguments to this function correspond to items in 
    * PKCS #5, v2.0 pp. 9-10 
    * 
    * P: the passphrase, an octet string:              e.g., "secret phrase"
    * S: the salt, an octet string:                    e.g., "DNXPzPpiwn"
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -199,16 +199,24 @@ function encryptPayload(cleartext) {
 }
 
 function generateNewKeys(collections) {
   let wbo = CollectionKeys.generateNewKeysWBO(collections);
   let modified = new_timestamp();
   CollectionKeys.setContents(wbo.cleartext, modified);
 }
 
+function do_check_empty(obj) {
+  do_check_attribute_count(obj, 0);
+}
+
+function do_check_attribute_count(obj, c) {
+  do_check_eq(c, Object.keys(obj).length);
+}
+
 function do_check_throws(aFunc, aResult, aStack)
 {
   if (!aStack) {
     try {
       // We might not have a 'Components' object.
       aStack = Components.stack.caller;
     } catch (e) {}
   }
--- a/services/sync/tests/unit/test_bookmark_tracker.js
+++ b/services/sync/tests/unit/test_bookmark_tracker.js
@@ -7,59 +7,59 @@ Cu.import("resource://gre/modules/Places
 Engines.register(BookmarksEngine);
 let engine = Engines.get("bookmarks");
 let store  = engine._store;
 store.wipe();
 
 function test_tracking() {
   _("Verify we've got an empty tracker to work with.");
   let tracker = engine._tracker;
-  do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+  do_check_empty(tracker.changedIDs);
 
   let folder = PlacesUtils.bookmarks.createFolder(
     PlacesUtils.bookmarks.bookmarksMenuFolder,
     "Test Folder", PlacesUtils.bookmarks.DEFAULT_INDEX);
   function createBmk() {
     return PlacesUtils.bookmarks.insertBookmark(
       folder, Utils.makeURI("http://getfirefox.com"),
       PlacesUtils.bookmarks.DEFAULT_INDEX, "Get Firefox!");
   }
 
   try {
     _("Create bookmark. Won't show because we haven't started tracking yet");
     createBmk();
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
     do_check_eq(tracker.score, 0);
 
     _("Tell the tracker to start tracking changes.");
     Svc.Obs.notify("weave:engine:start-tracking");
     createBmk();
     // We expect two changed items because the containing folder
     // changed as well (new child).
-    do_check_eq([id for (id in tracker.changedIDs)].length, 2);
+    do_check_attribute_count(tracker.changedIDs, 2);
     do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
 
     _("Notifying twice won't do any harm.");
     Svc.Obs.notify("weave:engine:start-tracking");
     createBmk();
-    do_check_eq([id for (id in tracker.changedIDs)].length, 3);
+    do_check_attribute_count(tracker.changedIDs, 3);
     do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 4);
 
     _("Let's stop tracking again.");
     tracker.clearChangedIDs();
     tracker.resetScore();
     Svc.Obs.notify("weave:engine:stop-tracking");
     createBmk();
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
     do_check_eq(tracker.score, 0);
 
     _("Notifying twice won't do any harm.");
     Svc.Obs.notify("weave:engine:stop-tracking");
     createBmk();
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
     do_check_eq(tracker.score, 0);
 
   } finally {
     _("Clean up.");
     store.wipe();
     tracker.clearChangedIDs();
     tracker.resetScore();
     Svc.Obs.notify("weave:engine:stop-tracking");
@@ -67,17 +67,17 @@ function test_tracking() {
 }
 
 function test_onItemChanged() {
   // Anno that's in ANNOS_TO_TRACK.
   const DESCRIPTION_ANNO = "bookmarkProperties/description";
 
   _("Verify we've got an empty tracker to work with.");
   let tracker = engine._tracker;
-  do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+  do_check_empty(tracker.changedIDs);
   do_check_eq(tracker.score, 0);
 
   try {
     Svc.Obs.notify("weave:engine:stop-tracking");
     let folder = PlacesUtils.bookmarks.createFolder(
       PlacesUtils.bookmarks.bookmarksMenuFolder, "Parent",
       PlacesUtils.bookmarks.DEFAULT_INDEX);
     _("Track changes to annos.");
@@ -102,17 +102,17 @@ function test_onItemChanged() {
     tracker.resetScore();
     Svc.Obs.notify("weave:engine:stop-tracking");
   }
 }
 
 function test_onItemMoved() {
   _("Verify we've got an empty tracker to work with.");
   let tracker = engine._tracker;
-  do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+  do_check_empty(tracker.changedIDs);
   do_check_eq(tracker.score, 0);
 
   try {
     let fx_id = PlacesUtils.bookmarks.insertBookmark(
       PlacesUtils.bookmarks.bookmarksMenuFolder,
       Utils.makeURI("http://getfirefox.com"),
       PlacesUtils.bookmarks.DEFAULT_INDEX,
       "Get Firefox!");
--- a/services/sync/tests/unit/test_clients_engine.js
+++ b/services/sync/tests/unit/test_clients_engine.js
@@ -444,13 +444,94 @@ add_test(function test_command_sync() {
 
   } finally {
     Svc.Prefs.resetBranch("");
     Records.clearCache();
     server.stop(run_next_test);
   }
 });
 
+add_test(function test_send_uri_to_client_for_display() {
+  _("Ensure sendURIToClientForDisplay() sends command properly.");
+
+  let tracker = Clients._tracker;
+  let store = Clients._store;
+
+  let remoteId = Utils.makeGUID();
+  let rec = new ClientsRec("clients", remoteId);
+  rec.name = "remote";
+  store.create(rec);
+  let remoteRecord = store.createRecord(remoteId, "clients");
+
+  tracker.clearChangedIDs();
+  let initialScore = tracker.score;
+
+  let uri = "http://www.mozilla.org/";
+  Clients.sendURIToClientForDisplay(uri, remoteId);
+
+  let newRecord = store._remoteClients[remoteId];
+
+  do_check_neq(newRecord, undefined);
+  do_check_eq(newRecord.commands.length, 1);
+
+  let command = newRecord.commands[0];
+  do_check_eq(command.command, "displayURI");
+  do_check_eq(command.args.length, 2);
+  do_check_eq(command.args[0], uri);
+
+  do_check_true(tracker.score > initialScore);
+  do_check_true(tracker.score - initialScore >= SCORE_INCREMENT_XLARGE);
+
+  _("Ensure unknown client IDs result in exception.");
+  let unknownId = Utils.makeGUID();
+  let error;
+
+  try {
+    Clients.sendURIToClientForDisplay(uri, unknownId);
+  } catch (ex) {
+    error = ex;
+  }
+
+  do_check_eq(error.message.indexOf("Unknown remote client ID: "), 0);
+
+  run_next_test();
+});
+
+add_test(function test_receive_display_uri() {
+  _("Ensure processing of received 'displayURI' commands works.");
+
+  // We don't set up WBOs and perform syncing because other tests verify
+  // the command API works as advertised. This saves us a little work.
+
+  let uri = "http://www.mozilla.org/";
+  let remoteId = Utils.makeGUID();
+
+  let command = {
+    command: "displayURI",
+    args: [uri, remoteId],
+  };
+
+  Clients.localCommands = [command];
+
+  // Received 'displayURI' command should result in the topic defined below
+  // being called.
+  let ev = "weave:engine:clients:display-uri";
+
+  let handler = function(subject, data) {
+    Svc.Obs.remove(ev, handler);
+
+    do_check_eq(subject.uri, uri);
+    do_check_eq(subject.client, remoteId);
+    do_check_eq(data, null);
+
+    run_next_test();
+  };
+
+  Svc.Obs.add(ev, handler);
+
+  do_check_true(Clients.processIncomingCommands());
+});
+
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.Engine.Clients").level = Log4Moz.Level.Trace;
   run_next_test();
 }
--- a/services/sync/tests/unit/test_forms_tracker.js
+++ b/services/sync/tests/unit/test_forms_tracker.js
@@ -1,44 +1,44 @@
 Cu.import("resource://services-sync/engines/forms.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/log4moz.js");
 
 function run_test() {
   _("Verify we've got an empty tracker to work with.");
   let tracker = new FormEngine()._tracker;
-  do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+  do_check_empty(tracker.changedIDs);
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   try {
     _("Create an entry. Won't show because we haven't started tracking yet");
     Svc.Form.addEntry("name", "John Doe");
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
 
     _("Tell the tracker to start tracking changes.");
     Svc.Obs.notify("weave:engine:start-tracking");
     Svc.Form.removeEntry("name", "John Doe");
     Svc.Form.addEntry("email", "john@doe.com");
-    do_check_eq([id for (id in tracker.changedIDs)].length, 2);
+    do_check_attribute_count(tracker.changedIDs, 2);
 
     _("Notifying twice won't do any harm.");
     Svc.Obs.notify("weave:engine:start-tracking");
     Svc.Form.addEntry("address", "Memory Lane");
-    do_check_eq([id for (id in tracker.changedIDs)].length, 3);
+    do_check_attribute_count(tracker.changedIDs, 3);
 
     _("Let's stop tracking again.");
     tracker.clearChangedIDs();
     Svc.Obs.notify("weave:engine:stop-tracking");
     Svc.Form.removeEntry("address", "Memory Lane");
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
 
     _("Notifying twice won't do any harm.");
     Svc.Obs.notify("weave:engine:stop-tracking");
     Svc.Form.removeEntry("email", "john@doe.com");
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
   
     _("Test error detection.");
     // This throws an exception without the fix for Bug 597400.
     tracker.trackEntry("foo", "bar");
     
   } finally {
     _("Clean up.");
     Svc.Form.removeAllEntries();
--- a/services/sync/tests/unit/test_history_store.js
+++ b/services/sync/tests/unit/test_history_store.js
@@ -73,24 +73,24 @@ let fxuri, fxguid, tburi, tbguid;
 
 function run_test() {
   initTestLogging("Trace");
   run_next_test();
 }
 
 add_test(function test_store() {
   _("Verify that we've got an empty store to work with.");
-  do_check_eq([id for (id in store.getAllIDs())].length, 0);
+  do_check_empty(store.getAllIDs());
 
   _("Let's create an entry in the database.");
   fxuri = Utils.makeURI("http://getfirefox.com/");
    PlacesUtils.history.addPageWithDetails(fxuri, "Get Firefox!", TIMESTAMP1);
 
   _("Verify that the entry exists.");
-  let ids = [id for (id in store.getAllIDs())];
+  let ids = Object.keys(store.getAllIDs());
   do_check_eq(ids.length, 1);
   fxguid = ids[0];
   do_check_true(store.itemExists(fxguid));
 
   _("If we query a non-existent record, it's marked as deleted.");
   let record = store.createRecord("non-existent");
   do_check_true(record.deleted);
 
@@ -122,17 +122,17 @@ add_test(function test_store() {
   ]);
 });
 
 add_test(function test_store_create() {
   _("Create a brand new record through the store.");
   tbguid = Utils.makeGUID();
   tburi = Utils.makeURI("http://getthunderbird.com");
   onNextTitleChanged(ensureThrows(function() {
-    do_check_eq([id for (id in store.getAllIDs())].length, 2);
+    do_check_attribute_count(store.getAllIDs(), 2);
     let queryres = queryHistoryVisits(tburi);
     do_check_eq(queryres.length, 1);
     do_check_eq(queryres[0].time, TIMESTAMP3);
     do_check_eq(queryres[0].title, "The bird is the word!");
     run_next_test();
   }));
   applyEnsureNoFailures([
     {id: tbguid,
@@ -149,31 +149,31 @@ add_test(function test_null_title() {
   let resuri = Utils.makeURI("unknown://title");
   applyEnsureNoFailures([
     {id: resguid,
      histUri: resuri.spec,
      title: null,
      visits: [{date: TIMESTAMP3,
                type: Ci.nsINavHistoryService.TRANSITION_TYPED}]}
   ]);
-  do_check_eq([id for (id in store.getAllIDs())].length, 3);
+  do_check_attribute_count(store.getAllIDs(), 3);
   let queryres = queryHistoryVisits(resuri);
   do_check_eq(queryres.length, 1);
   do_check_eq(queryres[0].time, TIMESTAMP3);
   run_next_test();
 });
 
 add_test(function test_invalid_records() {
   _("Make sure we handle invalid URLs in places databases gracefully.");
   let query = "INSERT INTO moz_places "
     + "(url, title, rev_host, visit_count, last_visit_date) "
     + "VALUES ('invalid-uri', 'Invalid URI', '.', 1, " + TIMESTAMP3 + ")";
   let stmt = PlacesUtils.history.DBConnection.createAsyncStatement(query);
   let result = Async.querySpinningly(stmt);
-  do_check_eq([id for (id in store.getAllIDs())].length, 4);
+  do_check_attribute_count(store.getAllIDs(), 4);
 
   _("Make sure we report records with invalid URIs.");
   let invalid_uri_guid = Utils.makeGUID();
   let failed = store.applyIncomingBatch([{
     id: invalid_uri_guid,
     histUri: ":::::::::::::::",
     title: "Doesn't have a valid URI",
     visits: [{date: TIMESTAMP3,
@@ -250,17 +250,17 @@ add_test(function test_remove() {
   applyEnsureNoFailures([{id: fxguid, deleted: true},
                          {id: Utils.makeGUID(), deleted: true}]);
   do_check_false(store.itemExists(fxguid));
   let queryres = queryHistoryVisits(fxuri);
   do_check_eq(queryres.length, 0);
 
   _("Make sure wipe works.");
   store.wipe();
-  do_check_eq([id for (id in store.getAllIDs())].length, 0);
+  do_check_empty(store.getAllIDs());
   queryres = queryHistoryVisits(fxuri);
   do_check_eq(queryres.length, 0);
   queryres = queryHistoryVisits(tburi);
   do_check_eq(queryres.length, 0);
   run_next_test();
 });
 
 add_test(function cleanup() {
--- a/services/sync/tests/unit/test_history_tracker.js
+++ b/services/sync/tests/unit/test_history_tracker.js
@@ -29,62 +29,62 @@ function addVisit() {
 
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_empty() {
   _("Verify we've got an empty tracker to work with.");
-  do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+  do_check_empty(tracker.changedIDs);
   do_check_eq(tracker.score, 0);
   run_next_test();
 });
 
 add_test(function test_not_tracking(next) {
   _("Create history item. Won't show because we haven't started tracking yet");
   addVisit();
   Utils.nextTick(function() {
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
     do_check_eq(tracker.score, 0);
     run_next_test();
   });
 });
 
 add_test(function test_start_tracking() {
   _("Tell the tracker to start tracking changes.");
   onScoreUpdated(function() {
-    do_check_eq([id for (id in tracker.changedIDs)].length, 1);
+    do_check_attribute_count(tracker.changedIDs, 1);
     do_check_eq(tracker.score, SCORE_INCREMENT_SMALL);
     run_next_test();
   });
   Svc.Obs.notify("weave:engine:start-tracking");
   addVisit();
 });
 
 add_test(function test_start_tracking_twice() {
   _("Notifying twice won't do any harm.");
   onScoreUpdated(function() {
-    do_check_eq([id for (id in tracker.changedIDs)].length, 2);
+    do_check_attribute_count(tracker.changedIDs, 2);
     do_check_eq(tracker.score, 2 * SCORE_INCREMENT_SMALL);
     run_next_test();
   });
   Svc.Obs.notify("weave:engine:start-tracking");
   addVisit();
 });
 
 add_test(function test_track_delete() {
   _("Deletions are tracked.");
   let uri = Utils.makeURI("http://getfirefox.com/0");
   let guid = engine._store.GUIDForUri(uri);
   do_check_false(guid in tracker.changedIDs);
 
   onScoreUpdated(function() {
     do_check_true(guid in tracker.changedIDs);
-    do_check_eq([id for (id in tracker.changedIDs)].length, 3);
+    do_check_attribute_count(tracker.changedIDs, 3);
     do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE + 2 * SCORE_INCREMENT_SMALL);
     run_next_test();
   });
   do_check_eq(tracker.score, 2 * SCORE_INCREMENT_SMALL);
   PlacesUtils.history.removePage(uri);
 });
 
 add_test(function test_dont_track_expiration() {
@@ -96,17 +96,17 @@ add_test(function test_dont_track_expira
 
   tracker.clearChangedIDs();
   do_check_false(guidToExpire in tracker.changedIDs);
   do_check_false(guidToRemove in tracker.changedIDs);
 
   onScoreUpdated(function() {
     do_check_false(guidToExpire in tracker.changedIDs);
     do_check_true(guidToRemove in tracker.changedIDs);
-    do_check_eq([id for (id in tracker.changedIDs)].length, 1);
+    do_check_attribute_count(tracker.changedIDs, 1);
     run_next_test();
   });
 
   // Observe expiration.
   Services.obs.addObserver(function onExpiration(aSubject, aTopic, aData) {
     Services.obs.removeObserver(onExpiration, aTopic);
     // Remove the remaining page to update its score.
     PlacesUtils.history.removePage(uriToRemove);
@@ -120,27 +120,27 @@ add_test(function test_dont_track_expira
 });
 
 add_test(function test_stop_tracking() {
   _("Let's stop tracking again.");
   tracker.clearChangedIDs();
   Svc.Obs.notify("weave:engine:stop-tracking");
   addVisit();
   Utils.nextTick(function() {
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
     run_next_test();
   });
 });
 
 add_test(function test_stop_tracking_twice() {
   _("Notifying twice won't do any harm.");
   Svc.Obs.notify("weave:engine:stop-tracking");
   addVisit();
   Utils.nextTick(function() {
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
     run_next_test();
   });
 });
 
 add_test(function cleanup() {
    _("Clean up.");
   PlacesUtils.history.removeAllPages();
   run_next_test();
--- a/services/sync/tests/unit/test_password_tracker.js
+++ b/services/sync/tests/unit/test_password_tracker.js
@@ -6,17 +6,17 @@ Engines.register(PasswordEngine);
 let engine = Engines.get("passwords");
 let store  = engine._store;
 
 function test_tracking() {
   let recordNum = 0;
 
   _("Verify we've got an empty tracker to work with.");
   let tracker = engine._tracker;
-  do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+  do_check_empty(tracker.changedIDs);
 
   function createPassword() {
     _("RECORD NUM: " + recordNum);
     let record = {id: "GUID" + recordNum,
                   hostname: "http://foo.bar.com",
                   formSubmitURL: "http://foo.bar.com/baz",
                   username: "john" + recordNum,
                   password: "smith",
@@ -25,58 +25,58 @@ function test_tracking() {
     recordNum++;
     let login = store._nsLoginInfoFromRecord(record);
     Services.logins.addLogin(login);
   }
 
   try {
     _("Create a password record. Won't show because we haven't started tracking yet");
     createPassword();
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
     do_check_eq(tracker.score, 0);
 
     _("Tell the tracker to start tracking changes.");
     Svc.Obs.notify("weave:engine:start-tracking");
     createPassword();
-    do_check_eq([id for (id in tracker.changedIDs)].length, 1);
+    do_check_attribute_count(tracker.changedIDs, 1);
     do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
 
     _("Notifying twice won't do any harm.");
     Svc.Obs.notify("weave:engine:start-tracking");
     createPassword();
-    do_check_eq([id for (id in tracker.changedIDs)].length, 2);
+    do_check_attribute_count(tracker.changedIDs, 2);
     do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE * 2);
 
     _("Let's stop tracking again.");
     tracker.clearChangedIDs();
     tracker.resetScore();
     Svc.Obs.notify("weave:engine:stop-tracking");
     createPassword();
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
     do_check_eq(tracker.score, 0);
 
     _("Notifying twice won't do any harm.");
     Svc.Obs.notify("weave:engine:stop-tracking");
     createPassword();
-    do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+    do_check_empty(tracker.changedIDs);
     do_check_eq(tracker.score, 0);
 
   } finally {
     _("Clean up.");
     store.wipe();
     tracker.clearChangedIDs();
     tracker.resetScore();
     Svc.Obs.notify("weave:engine:stop-tracking");
   }
 }
 
 function test_onWipe() {
   _("Verify we've got an empty tracker to work with.");
   let tracker = engine._tracker;
-  do_check_eq([id for (id in tracker.changedIDs)].length, 0);
+  do_check_empty(tracker.changedIDs);
   do_check_eq(tracker.score, 0);
 
   try {
     _("A store wipe should increment the score");
     Svc.Obs.notify("weave:engine:start-tracking");
     store.wipe();
     
     do_check_eq(tracker.score, SCORE_INCREMENT_XLARGE);
--- a/services/sync/tests/unit/test_prefs_store.js
+++ b/services/sync/tests/unit/test_prefs_store.js
@@ -31,17 +31,17 @@ function run_test() {
     prefs.set("testing.string", "ohai");
     prefs.set("testing.bool", true);
     prefs.set("testing.dont.change", "Please don't change me.");
     prefs.set("testing.turned.off", "I won't get synced.");
     prefs.set("testing.not.turned.on", "I won't get synced either!");
 
     _("The GUID corresponds to XUL App ID.");
     let allIDs = store.getAllIDs();
-    let ids = [id for (id in allIDs)];
+    let ids = Object.keys(allIDs);
     do_check_eq(ids.length, 1);
     do_check_eq(ids[0], PREFS_GUID);
     do_check_true(allIDs[PREFS_GUID], true);
 
     do_check_true(store.itemExists(PREFS_GUID));
     do_check_false(store.itemExists("random-gibberish"));
 
     _("Unknown prefs record is created as deleted.");
--- a/services/sync/tests/unit/test_prefs_tracker.js
+++ b/services/sync/tests/unit/test_prefs_tracker.js
@@ -15,25 +15,25 @@ function run_test() {
     do_check_false(tracker.modified);
 
     tracker.modified = true;
     do_check_eq(Svc.Prefs.get("engine.prefs.modified"), true);
     do_check_true(tracker.modified);
 
     _("Engine's getChangedID() just returns the one GUID we have.");
     let changedIDs = engine.getChangedIDs();
-    let ids = [id for (id in changedIDs)];
+    let ids = Object.keys(changedIDs);
     do_check_eq(ids.length, 1);
     do_check_eq(ids[0], Utils.encodeBase64url(Services.appinfo.ID));
 
     Svc.Prefs.set("engine.prefs.modified", false);
     do_check_false(tracker.modified);
 
     _("No modified state, so no changed IDs.");
-    do_check_eq([id for (id in engine.getChangedIDs())].length, 0);
+    do_check_empty(engine.getChangedIDs());
 
     _("Initial score is 0");
     do_check_eq(tracker.score, 0);
 
     _("Test fixtures.");
     Svc.Prefs.set("prefs.sync.testing.int", true);
 
     _("Test fixtures haven't upped the tracker score yet because it hasn't started tracking yet.");
--- a/services/sync/tests/unit/test_score_triggers.js
+++ b/services/sync/tests/unit/test_score_triggers.js
@@ -1,12 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/policies.js");
 
 Svc.DefaultPrefs.set("registerEngines", "");
 Cu.import("resource://services-sync/service.js");
 
 Engines.register(RotaryEngine);
 let engine = Engines.get("rotary");
@@ -90,8 +91,30 @@ add_test(function test_sync_triggered() 
     Svc.Obs.remove("weave:service:sync:finish", onSyncFinish);
     _("Sync completed!");
     server.stop(run_next_test);
   });
 
   tracker.score += SCORE_INCREMENT_XLARGE;
 });
 
+add_test(function test_clients_engine_sync_triggered() {
+  _("Ensure that client engine score changes trigger a sync.");
+
+  // The clients engine is not registered like other engines. Therefore,
+  // it needs special treatment throughout the code. Here, we verify the
+  // global score tracker gives it that treatment. See bug 676042 for more.
+
+  let server = sync_httpd_setup();
+  setUp();
+  Service.login();
+
+  const TOPIC = "weave:service:sync:finish";
+  Svc.Obs.add(TOPIC, function onSyncFinish() {
+    Svc.Obs.remove(TOPIC, onSyncFinish);
+    _("Sync due to clients engine change completed.");
+    server.stop(run_next_test);
+  });
+
+  SyncScheduler.syncThreshold = MULTI_DEVICE_THRESHOLD;
+  Clients._tracker.score += SCORE_INCREMENT_XLARGE;
+});
+
--- a/services/sync/tests/unit/test_syncengine_sync.js
+++ b/services/sync/tests/unit/test_syncengine_sync.js
@@ -397,17 +397,17 @@ add_test(function test_processIncoming_m
   meta_global.payload.engines = {rotary: {version: engine.version,
                                          syncID: engine.syncID}};
 
   try {
 
     _("On a mobile client, we get new records from the server in batches of 50.");
     engine._syncStartup();
     engine._processIncoming();
-    do_check_eq([id for (id in engine._store.items)].length, 234);
+    do_check_attribute_count(engine._store.items, 234);
     do_check_true('record-no-0' in engine._store.items);
     do_check_true('record-no-49' in engine._store.items);
     do_check_true('record-no-50' in engine._store.items);
     do_check_true('record-no-233' in engine._store.items);
 
     // Verify that the right number of GET requests with the right
     // kind of parameters were made.
     do_check_eq(collection.get_log.length,
@@ -468,28 +468,28 @@ add_test(function test_processIncoming_s
   let server = sync_httpd_setup({
       "/1.1/foo/storage/rotary": collection.handler()
   });
 
   try {
 
     // Confirm initial environment
     do_check_eq(engine.lastSync, 0);
-    do_check_eq([id for (id in engine._store.items)].length, 0);
+    do_check_empty(engine._store.items);
 
     let error;
     try {
       engine.sync();
     } catch (ex) {
       error = ex;
     }
     do_check_true(!!error);
 
     // Only the first two batches have been applied.
-    do_check_eq([id for (id in engine._store.items)].length,
+    do_check_eq(Object.keys(engine._store.items).length,
                 MOBILE_BATCH_SIZE * 2);
 
     // The third batch is stuck in toFetch. lastSync has been moved forward to
     // the last successful item's timestamp.
     do_check_eq(engine.toFetch.length, MOBILE_BATCH_SIZE);
     do_check_eq(engine.lastSync, collection.wbos["record-no-99"].modified);
 
   } finally {
@@ -597,24 +597,23 @@ add_test(function test_processIncoming_a
                                          syncID: engine.syncID}};
   let server = sync_httpd_setup({
       "/1.1/foo/storage/rotary": collection.handler()
   });
 
   try {
 
     // Confirm initial environment
-    do_check_eq([id for (id in engine._store.items)].length, 0);
+    do_check_empty(engine._store.items);
 
     engine._syncStartup();
     engine._processIncoming();
 
     // Records have been applied and the expected failures have failed.
-    do_check_eq([id for (id in engine._store.items)].length,
-                APPLY_BATCH_SIZE - 1 - 2);
+    do_check_attribute_count(engine._store.items, APPLY_BATCH_SIZE - 1 - 2);
     do_check_eq(engine.toFetch.length, 0);
     do_check_eq(engine.previousFailed.length, 2);
     do_check_eq(engine.previousFailed[0], "record-no-0");
     do_check_eq(engine.previousFailed[1], "record-no-8");
 
   } finally {
     cleanAndGo(server);
   }
@@ -653,25 +652,24 @@ add_test(function test_processIncoming_a
                                          syncID: engine.syncID}};
   let server = sync_httpd_setup({
       "/1.1/foo/storage/rotary": collection.handler()
   });
 
   try {
 
     // Confirm initial environment
-    do_check_eq([id for (id in engine._store.items)].length, 0);
+    do_check_empty(engine._store.items);
 
     engine._syncStartup();
     engine._processIncoming();
 
     // Records have been applied in 3 batches.
     do_check_eq(batchCalls, 3);
-    do_check_eq([id for (id in engine._store.items)].length,
-                APPLY_BATCH_SIZE * 3);
+    do_check_attribute_count(engine._store.items, APPLY_BATCH_SIZE * 3);
 
   } finally {
     cleanAndGo(server);
   }
 });
 
 
 add_test(function test_processIncoming_notify_count() {
@@ -707,49 +705,49 @@ add_test(function test_processIncoming_n
       "/1.1/foo/storage/rotary": collection.handler()
   });
 
   try {
     // Confirm initial environment.
     do_check_eq(engine.lastSync, 0);
     do_check_eq(engine.toFetch.length, 0);
     do_check_eq(engine.previousFailed.length, 0);
-    do_check_eq([id for (id in engine._store.items)].length, 0);
+    do_check_empty(engine._store.items);
 
     let called = 0;
     let counts;
     function onApplied(count) {
       _("Called with " + JSON.stringify(counts));
       counts = count;
       called++;
     }
     Svc.Obs.add("weave:engine:sync:applied", onApplied);
 
     // Do sync.
     engine._syncStartup();
     engine._processIncoming();
     
     // Confirm failures.
-    do_check_eq([id for (id in engine._store.items)].length, 12);
+    do_check_attribute_count(engine._store.items, 12);
     do_check_eq(engine.previousFailed.length, 3);
     do_check_eq(engine.previousFailed[0], "record-no-0");
     do_check_eq(engine.previousFailed[1], "record-no-5");
     do_check_eq(engine.previousFailed[2], "record-no-10");
 
     // There are newly failed records and they are reported.
     do_check_eq(called, 1);
     do_check_eq(counts.failed, 3);
     do_check_eq(counts.applied, 15);
     do_check_eq(counts.newFailed, 3);
 
     // Sync again, 1 of the failed items are the same, the rest didn't fail.
     engine._processIncoming();
     
     // Confirming removed failures.
-    do_check_eq([id for (id in engine._store.items)].length, 14);
+    do_check_attribute_count(engine._store.items, 14);
     do_check_eq(engine.previousFailed.length, 1);
     do_check_eq(engine.previousFailed[0], "record-no-0");
 
     do_check_eq(called, 2);
     do_check_eq(counts.failed, 1);
     do_check_eq(counts.applied, 3);
     do_check_eq(counts.newFailed, 0);
 
@@ -794,45 +792,45 @@ add_test(function test_processIncoming_p
       "/1.1/foo/storage/rotary": collection.handler()
   });
 
   try {
     // Confirm initial environment.
     do_check_eq(engine.lastSync, 0);
     do_check_eq(engine.toFetch.length, 0);
     do_check_eq(engine.previousFailed.length, 0);
-    do_check_eq([id for (id in engine._store.items)].length, 0);
+    do_check_empty(engine._store.items);
 
     // Initial failed items in previousFailed to be reset.
     let previousFailed = [Utils.makeGUID(), Utils.makeGUID(), Utils.makeGUID()];
     engine.previousFailed = previousFailed;
     do_check_eq(engine.previousFailed, previousFailed);
 
     // Do sync.
     engine._syncStartup();
     engine._processIncoming();
 
     // Expected result: 4 sync batches with 2 failures each => 8 failures
-    do_check_eq([id for (id in engine._store.items)].length, 6);
+    do_check_attribute_count(engine._store.items, 6);
     do_check_eq(engine.previousFailed.length, 8);
     do_check_eq(engine.previousFailed[0], "record-no-0");
     do_check_eq(engine.previousFailed[1], "record-no-1");
     do_check_eq(engine.previousFailed[2], "record-no-4");
     do_check_eq(engine.previousFailed[3], "record-no-5");
     do_check_eq(engine.previousFailed[4], "record-no-8");
     do_check_eq(engine.previousFailed[5], "record-no-9");
     do_check_eq(engine.previousFailed[6], "record-no-12");
     do_check_eq(engine.previousFailed[7], "record-no-13");
 
     // Sync again with the same failed items (records 0, 1, 8, 9).
     engine._processIncoming();
 
     // A second sync with the same failed items should not add the same items again.
     // Items that did not fail a second time should no longer be in previousFailed.
-    do_check_eq([id for (id in engine._store.items)].length, 10);
+    do_check_attribute_count(engine._store.items, 10);
     do_check_eq(engine.previousFailed.length, 4);
     do_check_eq(engine.previousFailed[0], "record-no-0");
     do_check_eq(engine.previousFailed[1], "record-no-1");
     do_check_eq(engine.previousFailed[2], "record-no-8");
     do_check_eq(engine.previousFailed[3], "record-no-9");
 
     // Refetched items that didn't fail the second time are in engine._store.items.
     do_check_eq(engine._store.items['record-no-4'], "Record No. 4");
@@ -849,17 +847,17 @@ add_test(function test_processIncoming_f
   _("Ensure that failed records from _reconcile and applyIncomingBatch are refetched.");
   let syncTesting = new SyncTestingInfrastructure();
   Svc.Prefs.set("clusterURL", "http://localhost:8080/");
   Svc.Prefs.set("username", "foo");
 
   // Let's create three and a bit batches worth of server side records.
   let collection = new ServerCollection();
   const NUMBER_OF_RECORDS = MOBILE_BATCH_SIZE * 3 + 5;
-  for (var i = 0; i < NUMBER_OF_RECORDS; i++) {
+  for (let i = 0; i < NUMBER_OF_RECORDS; i++) {
     let id = 'record-no-' + i;
     let payload = encryptPayload({id: id, denomination: "Record No. " + id});
     let wbo = new ServerWBO(id, payload);
     wbo.modified = Date.now()/1000 + 60 * (i - MOBILE_BATCH_SIZE * 3);
     collection.wbos[id] = wbo;
   }
 
   // Engine that batches but likes to throw on a couple of records,
@@ -911,32 +909,32 @@ add_test(function test_processIncoming_f
   });
 
   try {
 
     // Confirm initial environment
     do_check_eq(engine.lastSync, 0);
     do_check_eq(engine.toFetch.length, 0);
     do_check_eq(engine.previousFailed.length, 0);
-    do_check_eq([id for (id in engine._store.items)].length, 0);
+    do_check_empty(engine._store.items);
 
     let observerSubject;
     let observerData;
     Svc.Obs.add("weave:engine:sync:applied", function onApplied(subject, data) {
       Svc.Obs.remove("weave:engine:sync:applied", onApplied);
       observerSubject = subject;
       observerData = data;
     });
 
     engine._syncStartup();
     engine._processIncoming();
 
     // Ensure that all records but the bogus 4 have been applied.
-    do_check_eq([id for (id in engine._store.items)].length,
-                NUMBER_OF_RECORDS - BOGUS_RECORDS.length);
+    do_check_attribute_count(engine._store.items,
+                             NUMBER_OF_RECORDS - BOGUS_RECORDS.length);
 
     // Ensure that the bogus records will be fetched again on the next sync.
     do_check_eq(engine.previousFailed.length, BOGUS_RECORDS.length);
     engine.previousFailed.sort();
     BOGUS_RECORDS.sort();
     for (let i = 0; i < engine.previousFailed.length; i++) {
       do_check_eq(engine.previousFailed[i], BOGUS_RECORDS[i]);
     }
@@ -1525,17 +1523,17 @@ add_test(function test_syncapplied_obser
 
   try {
     SyncScheduler.hasIncomingItems = false;
 
     // Do sync.
     engine._syncStartup();
     engine._processIncoming();
 
-    do_check_eq([id for (id in engine._store.items)].length, 10);
+    do_check_attribute_count(engine._store.items, 10);
 
     do_check_eq(numApplyCalls, 1);
     do_check_eq(engine_name, "rotary");
     do_check_eq(count.applied, 10);
 
     do_check_true(SyncScheduler.hasIncomingItems);
   } finally {
     cleanAndGo(server);
--- a/services/sync/tests/unit/test_tab_tracker.js
+++ b/services/sync/tests/unit/test_tab_tracker.js
@@ -42,17 +42,17 @@ function fakeSvcSession() {
 }
 
 function run_test() {
   let engine = new TabEngine();
 
   _("We assume that tabs have changed at startup.");
   let tracker = engine._tracker;
   do_check_true(tracker.modified);
-  do_check_true(Utils.deepEquals([id for (id in engine.getChangedIDs())],
+  do_check_true(Utils.deepEquals(Object.keys(engine.getChangedIDs()),
                                  [Clients.localID]));
 
   let logs;
 
   _("Test listeners are registered on windows");
   logs = fakeSvcWinMediator();
   Svc.Obs.notify("weave:engine:start-tracking");
   do_check_eq(logs.length, 2);
@@ -86,26 +86,26 @@ function run_test() {
   for each (let evttype in ["TabOpen", "TabClose", "TabSelect"]) {
     // Pretend we just synced.
     tracker.clearChangedIDs();
     do_check_false(tracker.modified);
 
     // Send a fake tab event
     tracker.onTab({type: evttype , originalTarget: evttype});
     do_check_true(tracker.modified);
-    do_check_true(Utils.deepEquals([id for (id in engine.getChangedIDs())],
+    do_check_true(Utils.deepEquals(Object.keys(engine.getChangedIDs()),
                                    [Clients.localID]));
     do_check_eq(logs.length, idx+1);
     do_check_eq(logs[idx].target, evttype);
     do_check_eq(logs[idx].prop, "weaveLastUsed");
     do_check_true(typeof logs[idx].value == "number");
     idx++;
   }
 
   // Pretend we just synced.
   tracker.clearChangedIDs();
   do_check_false(tracker.modified);
 
   tracker.onTab({type: "pageshow", originalTarget: "pageshow"});
-  do_check_true(Utils.deepEquals([id for (id in engine.getChangedIDs())],
+  do_check_true(Utils.deepEquals(Object.keys(engine.getChangedIDs()),
                                  [Clients.localID]));
   do_check_eq(logs.length, idx); // test that setTabValue isn't called
 }
--- a/services/sync/tps/install.rdf
+++ b/services/sync/tps/install.rdf
@@ -5,17 +5,17 @@
     <em:id>tps@mozilla.org</em:id>
     <em:version>0.2</em:version>
 
     <em:targetApplication>
       <!-- Firefox -->
       <Description>
         <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
         <em:minVersion>3.5.0</em:minVersion>
-        <em:maxVersion>8.0.*</em:maxVersion>
+        <em:maxVersion>10.0.*</em:maxVersion>
       </Description>
     </em:targetApplication>
 
     <!-- front-end metadata -->
     <em:name>TPS</em:name>
     <em:description>Sync test extension</em:description>
     <em:creator>Jonathan Griffin</em:creator>
     <em:homepageURL>http://www.mozilla.org/</em:homepageURL>
--- a/testing/tps/setup.py
+++ b/testing/tps/setup.py
@@ -38,17 +38,17 @@
 import sys
 from setuptools import setup, find_packages
 
 version = '0.2.40'
 
 deps = ['pulsebuildmonitor >= 0.2', 'MozillaPulse == .4', 
         'mozinfo == 0.3.1', 'mozprofile == 0.1a',
         'mozprocess == 0.1a', 'mozrunner == 3.0a', 'mozregression == 0.3',
-        'mozautolog >= 0.2.0']
+        'mozautolog >= 0.2.1']
 
 # we only support python 2.6+ right now
 assert sys.version_info[0] == 2
 assert sys.version_info[1] >= 6
 
 setup(name='tps',
       version=version,
       description='run automated multi-profile sync tests',
--- a/toolkit/components/places/Helpers.cpp
+++ b/toolkit/components/places/Helpers.cpp
@@ -372,24 +372,16 @@ GetHiddenState(bool aIsRedirect,
          aIsRedirect;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// PlacesEvent
 
 PlacesEvent::PlacesEvent(const char* aTopic)
 : mTopic(aTopic)
-, mDoubleEnqueue(false)
-{
-}
-
-PlacesEvent::PlacesEvent(const char* aTopic,
-                         bool aDoubleEnqueue)
-: mTopic(aTopic)
-, mDoubleEnqueue(aDoubleEnqueue)
 {
 }
 
 NS_IMETHODIMP
 PlacesEvent::Run()
 {
   Notify();
   return NS_OK;
@@ -400,26 +392,20 @@ PlacesEvent::Complete()
 {
   Notify();
   return NS_OK;
 }
 
 void
 PlacesEvent::Notify()
 {
-  if (mDoubleEnqueue) {
-    mDoubleEnqueue = false;
-    (void)NS_DispatchToMainThread(this);
-  }
-  else {
-    NS_ASSERTION(NS_IsMainThread(), "Must only be used on the main thread!");
-    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
-    if (obs) {
-      (void)obs->NotifyObservers(nsnull, mTopic, nsnull);
-    }
+  NS_ASSERTION(NS_IsMainThread(), "Must only be used on the main thread!");
+  nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+  if (obs) {
+    (void)obs->NotifyObservers(nsnull, mTopic, nsnull);
   }
 }
 
 NS_IMPL_THREADSAFE_ISUPPORTS2(
   PlacesEvent
 , mozIStorageCompletionCallback
 , nsIRunnable
 )
--- a/toolkit/components/places/Helpers.h
+++ b/toolkit/components/places/Helpers.h
@@ -293,20 +293,18 @@ class PlacesEvent : public nsRunnable
                   , public mozIStorageCompletionCallback
 {
 public:
   NS_DECL_ISUPPORTS
   NS_DECL_NSIRUNNABLE
   NS_DECL_MOZISTORAGECOMPLETIONCALLBACK
 
   PlacesEvent(const char* aTopic);
-  PlacesEvent(const char* aTopic, bool aDoubleEnqueue);
 protected:
   void Notify();
 
   const char* const mTopic;
-  bool mDoubleEnqueue;
 };
 
 } // namespace places
 } // namespace mozilla
 
 #endif // mozilla_places_Helpers_h_
--- a/toolkit/components/places/History.cpp
+++ b/toolkit/components/places/History.cpp
@@ -58,19 +58,16 @@
 #include "nsIXPConnect.h"
 #include "mozilla/unused.h"
 #include "mozilla/Util.h"
 #include "nsContentUtils.h"
 
 // Initial size for the cache holding visited status observers.
 #define VISIT_OBSERVERS_INITIAL_CACHE_SIZE 128
 
-// Topic used to notify that work in mozIAsyncHistory::updatePlaces is done.
-#define TOPIC_UPDATEPLACES_COMPLETE "places-updatePlaces-complete"
-
 using namespace mozilla::dom;
 using mozilla::unused;
 
 namespace mozilla {
 namespace places {
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Global Defines
@@ -526,24 +523,24 @@ public:
   }
 private:
   const nsCString mSpec;
   const nsString mTitle;
   const nsCString mGUID;
 };
 
 /**
- * Notifies a callback object about completion.
+ * Notifies a callback object when a visit has been handled.
  */
-class NotifyCompletion : public nsRunnable
+class NotifyVisitInfoCallback : public nsRunnable
 {
 public:
-  NotifyCompletion(mozIVisitInfoCallback* aCallback,
-                   const VisitData& aPlace,
-                   nsresult aResult)
+  NotifyVisitInfoCallback(mozIVisitInfoCallback* aCallback,
+                          const VisitData& aPlace,
+                          nsresult aResult)
   : mCallback(aCallback)
   , mPlace(aPlace)
   , mResult(aResult)
   {
     NS_PRECONDITION(aCallback, "Must pass a non-null callback!");
   }
 
   NS_IMETHOD Run()
@@ -586,16 +583,54 @@ private:
    * this object (and therefore cannot hold a strong reference to it).
    */
   mozIVisitInfoCallback* mCallback;
   VisitData mPlace;
   const nsresult mResult;
 };
 
 /**
+ * Notifies a callback object when the operation is complete.
+ */
+class NotifyCompletion : public nsRunnable
+{
+public:
+  NotifyCompletion(mozIVisitInfoCallback* aCallback)
+  : mCallback(aCallback)
+  {
+    NS_PRECONDITION(aCallback, "Must pass a non-null callback!");
+  }
+
+  NS_IMETHOD Run()
+  {
+    if (NS_IsMainThread()) {
+      (void)mCallback->HandleCompletion();
+    }
+    else {
+      (void)NS_DispatchToMainThread(this);
+
+      // Also dispatch an event to release the reference to the callback after
+      // we have run.
+      nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+      (void)NS_ProxyRelease(mainThread, mCallback, PR_TRUE);
+    }
+    return NS_OK;
+  }
+
+private:
+  /**
+   * Callers MUST hold a strong reference to this because we may be created
+   * off of the main thread, and therefore cannot call AddRef on this object
+   * (and therefore cannot hold a strong reference to it). If invoked from a
+   * background thread, NotifyCompletion will release the reference to this.
+   */
+  mozIVisitInfoCallback* mCallback;
+};
+
+/**
  * Checks to see if we can add aURI to history, and dispatches an error to
  * aCallback (if provided) if we cannot.
  *
  * @param aURI
  *        The URI to check.
  * @param [optional] aGUID
  *        The guid of the URI to check.  This is passed back to the callback.
  * @param [optional] aCallback
@@ -613,29 +648,29 @@ CanAddURI(nsIURI* aURI,
   PRBool canAdd;
   nsresult rv = navHistory->CanAddURI(aURI, &canAdd);
   if (NS_SUCCEEDED(rv) && canAdd) {
     return true;
   };
 
   // We cannot add the URI.  Notify the callback, if we were given one.
   if (aCallback) {
-    // NotifyCompletion does not hold a strong reference to the callback, so we
+    // NotifyVisitInfoCallback does not hold a strong reference to the callback, so we
     // have to manage it by AddRefing now and then releasing it after the event
     // has run.
     NS_ADDREF(aCallback);
 
     VisitData place(aURI);
     place.guid = aGUID;
     nsCOMPtr<nsIRunnable> event =
-      new NotifyCompletion(aCallback, place, NS_ERROR_INVALID_ARG);
+      new NotifyVisitInfoCallback(aCallback, place, NS_ERROR_INVALID_ARG);
     (void)NS_DispatchToMainThread(event);
 
     // Also dispatch an event to release our reference to the callback after
-    // NotifyCompletion has run.
+    // NotifyVisitInfoCallback has run.
     nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
     (void)NS_ProxyRelease(mainThread, aCallback, PR_TRUE);
   }
 
   return false;
 }
 
 /**
@@ -692,17 +727,17 @@ public:
       bool known = (lastPlace && lastPlace->IsSamePlaceAs(place)) ||
                    mHistory->FetchPageInfo(place);
 
       FetchReferrerInfo(referrer, place);
 
       nsresult rv = DoDatabaseInserts(known, place, referrer);
       if (mCallback) {
         nsCOMPtr<nsIRunnable> event =
-          new NotifyCompletion(mCallback, place, rv);
+          new NotifyVisitInfoCallback(mCallback, place, rv);
         nsresult rv2 = NS_DispatchToMainThread(event);
         NS_ENSURE_SUCCESS(rv2, rv2);
       }
       NS_ENSURE_SUCCESS(rv, rv);
 
       nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(place, referrer);
       rv = NS_DispatchToMainThread(event);
       NS_ENSURE_SUCCESS(rv, rv);
@@ -1225,26 +1260,26 @@ StoreAndNotifyEmbedVisit(VisitData& aPla
   nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
   if (!navHistory || !uri) {
     return;
   }
 
   navHistory->registerEmbedVisit(uri, aPlace.visitTime);
 
   if (aCallback) {
-    // NotifyCompletion does not hold a strong reference to the callback, so we
-    // have to manage it by AddRefing now and then releasing it after the event
-    // has run.
+    // NotifyVisitInfoCallback does not hold a strong reference to the callback,
+    // so we have to manage it by AddRefing now and then releasing it after the
+    // event has run.
     NS_ADDREF(aCallback);
     nsCOMPtr<nsIRunnable> event =
-      new NotifyCompletion(aCallback, aPlace, NS_OK);
+      new NotifyVisitInfoCallback(aCallback, aPlace, NS_OK);
     (void)NS_DispatchToMainThread(event);
 
     // Also dispatch an event to release our reference to the callback after
-    // NotifyCompletion has run.
+    // NotifyVisitInfoCallback has run.
     nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
     (void)NS_ProxyRelease(mainThread, aCallback, PR_TRUE);
   }
 
   VisitData noReferrer;
   nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(aPlace, noReferrer);
   (void)NS_DispatchToMainThread(event);
 }
@@ -1970,24 +2005,32 @@ History::UpdatePlaces(const jsval& aPlac
   // It is possible that all of the visits we were passed were dissallowed by
   // CanAddURI, which isn't an error.  If we have no visits to add, however,
   // we should not call InsertVisitedURIs::Start.
   if (visitData.Length()) {
     nsresult rv = InsertVisitedURIs::Start(dbConn, visitData, aCallback);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  // Be sure to notify that all of our operations are complete.  This is
-  // double enqueued to make sure that all database notifications and all embed
-  // or canAddURI notifications have finished.
-  nsCOMPtr<nsIEventTarget> backgroundThread = do_GetInterface(dbConn);
-  NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED);
-  nsRefPtr<PlacesEvent> completeEvent =
-    new PlacesEvent(TOPIC_UPDATEPLACES_COMPLETE, true);
-  (void)backgroundThread->Dispatch(completeEvent, 0);
+  // Be sure to notify that all of our operations are complete.  This
+  // is dispatched to the background thread first and redirected to the
+  // main thread from there to make sure that all database notifications
+  // and all embed or canAddURI notifications have finished.
+  if (aCallback) {
+    // NotifyCompletion does not hold a strong reference to the callback,
+    // so we have to manage it by AddRefing now. NotifyCompletion will
+    // release it for us once it has dispatched the callback to the main
+    // thread.
+    NS_ADDREF(aCallback);
+
+    nsCOMPtr<nsIEventTarget> backgroundThread = do_GetInterface(dbConn);
+    NS_ENSURE_TRUE(backgroundThread, NS_ERROR_UNEXPECTED);
+    nsCOMPtr<nsIRunnable> event = new NotifyCompletion(aCallback);
+    (void)backgroundThread->Dispatch(event, NS_DISPATCH_NORMAL);
+  }
 
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIObserver
 
 NS_IMETHODIMP
--- a/toolkit/components/places/mozIAsyncHistory.idl
+++ b/toolkit/components/places/mozIAsyncHistory.idl
@@ -117,17 +117,17 @@ interface mozIPlaceInfo : nsISupports
    */
   [implicit_jscontext]
   readonly attribute jsval visits;
 };
 
 /**
  * @status EXPERIMENTAL
  */
-[scriptable, uuid(eb0b406f-8f57-4f2b-b0da-8883684b138a)]
+[scriptable, uuid(1f266877-2859-418b-a11b-ec3ae4f4f93d)]
 interface mozIVisitInfoCallback : nsISupports
 {
   /**
    * Called when the given mozIPlaceInfo object could not be processed.
    *
    * @param aResultCode
    *        nsresult indicating the failure reason.
    * @param aPlaceInfo
@@ -142,16 +142,22 @@ interface mozIVisitInfoCallback : nsISup
    * a given visit, only one callback will be given (i.e. title change and 
    * add visit).
    *
    * @param aPlaceInfo
    *        The information that was being entered into the database.
    */
   void handleResult(in mozIPlaceInfo aPlaceInfo);
 
+  /**
+   * Called when the mozIAsyncHistory::updatePlaces has finished processing
+   * all mozIPlaceInfo records.
+   */
+  void handleCompletion();
+
 };
 
 /**
  * @status EXPERIMENTAL
  */
 [scriptable, uuid(f79ca67c-7e57-4511-a400-ea31001c762f)]
 interface mozIAsyncHistory : nsISupports
 {
--- a/toolkit/components/places/tests/unit/test_async_history_api.js
+++ b/toolkit/components/places/tests/unit/test_async_history_api.js
@@ -12,17 +12,16 @@ XPCOMUtils.defineLazyServiceGetter(this,
                                    "@mozilla.org/browser/history;1",
                                    "mozIAsyncHistory");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gGlobalHistory",
                                    "@mozilla.org/browser/nav-history-service;1",
                                    "nsIGlobalHistory2");
 
 const TEST_DOMAIN = "http://mozilla.org/";
-const TOPIC_UPDATEPLACES_COMPLETE = "places-updatePlaces-complete";
 const URI_VISIT_SAVED = "uri-visit-saved";
 const RECENT_EVENT_THRESHOLD = 15 * 60 * 1000000;
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Helpers
 
 /**
  * Object that represents a mozIVisitInfo object.
@@ -737,24 +736,24 @@ function test_place_id_ignored()
       },
       handleError: function handleError(aResultCode) {
         do_throw("Unexpected error: " + aResultCode);
       }
     });
   }));
 }
 
-function test_observer_topic_dispatched_when_complete()
+function test_handleCompletion_called_when_complete()
 {
   // We test a normal visit, and embeded visit, and a uri that would fail
   // the canAddURI test to make sure that the notification happens after *all*
   // of them have had a callback.
   let places = [
     { uri: NetUtil.newURI(TEST_DOMAIN +
-                          "test_observer_topic_dispatched_when_complete"),
+                          "test_handleCompletion_called_when_complete"),
       visits: [
         new VisitInfo(),
         new VisitInfo(TRANSITION_EMBED),
       ],
     },
     { uri: NetUtil.newURI("data:,Hello%2C%20World!"),
       visits: [
         new VisitInfo(),
@@ -772,30 +771,23 @@ function test_observer_topic_dispatched_
   gHistory.updatePlaces(places, {
     handleResult: function handleResult(aPlaceInfo) {
       let checker = PlacesUtils.history.canAddURI(aPlaceInfo.uri) ?
         do_check_true : do_check_false;
       callbackCountSuccess++;
     },
     handleError: function handleError(aResultCode, aPlaceInfo) {
       callbackCountFailure++;
-    }
-  });
-
-  let observer = {
-    observe: function(aSubject, aTopic, aData)
-    {
-      do_check_eq(aTopic, TOPIC_UPDATEPLACES_COMPLETE);
+    },
+    handleCompletion: function handleCompletion() {
       do_check_eq(callbackCountSuccess, EXPECTED_COUNT_SUCCESS);
       do_check_eq(callbackCountFailure, EXPECTED_COUNT_FAILURE);
-      Services.obs.removeObserver(observer, TOPIC_UPDATEPLACES_COMPLETE);
       waitForAsyncUpdates(run_next_test);
     },
-  };
-  Services.obs.addObserver(observer, TOPIC_UPDATEPLACES_COMPLETE, false);
+  });
 }
 
 function test_add_visit()
 {
   const VISIT_TIME = Date.now() * 1000;
   let place = {
     uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit"),
     title: "test_add_visit title",
@@ -1295,25 +1287,18 @@ function test_callbacks_not_supplied()
     catch (e if e.result === Cr.NS_ERROR_FAILURE) {
       // NetUtil.newURI() can throw if e.g. our app knows about imap://
       // but the account is not set up and so the URL is invalid for us.
       // Note this in the log but ignore as it's not the subject of this test.
       do_log_info("Could not construct URI for '" + url + "'; ignoring");
     }
   });
   
-  gHistory.updatePlaces(places, {} );
-  let observer = {
-    observe: function(aSubject, aTopic, aData)
-    {
-      Services.obs.removeObserver(observer, TOPIC_UPDATEPLACES_COMPLETE);
-      waitForAsyncUpdates(run_next_test);
-    },
-  };
-  Services.obs.addObserver(observer, TOPIC_UPDATEPLACES_COMPLETE, false);
+  gHistory.updatePlaces(places, {});
+  waitForAsyncUpdates(run_next_test);
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Test Runner
 
 [
   test_interface_exists,
   test_invalid_uri_throws,
@@ -1328,17 +1313,17 @@ function test_callbacks_not_supplied()
   test_non_addable_uri_errors,
   test_duplicate_guid_errors,
   test_invalid_referrerURI_ignored,
   test_nonnsIURI_referrerURI_ignored,
   test_invalid_sessionId_ignored,
   test_unstored_sessionId_ignored,
   test_old_referrer_ignored,
   test_place_id_ignored,
-  test_observer_topic_dispatched_when_complete,
+  test_handleCompletion_called_when_complete,
   test_add_visit,
   test_properties_saved,
   test_guid_saved,
   test_referrer_saved,
   test_sessionId_saved,
   test_guid_change_saved,
   test_title_change_saved,
   test_no_title_does_not_clear_title,