Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 04 Jun 2015 09:24:15 -0400
changeset 247139 5b4c240e1a36ed27db67f21dc35c1ca2e8934d0c
parent 247118 f21fac50a8cca34cf708d6acffca39e1d4d4e6d8 (current diff)
parent 247138 4b69a62d1905679020c20fc3f06645140532eb3e (diff)
child 247143 dbc89e025b5f37be29eb360f6338a667ac4d6f26
child 247193 d421de06735de2701094b38db71280badd584ed4
child 247214 b538d26f89743b9a0ce5d1a3d268a24b2468fffc
push id28854
push userryanvm@gmail.com
push dateThu, 04 Jun 2015 13:24:20 +0000
treeherdermozilla-central@5b4c240e1a36 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c. a=merge
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -173,19 +173,17 @@ pref("app.update.mode", 1);
 pref("app.update.silent", false);
 
 // If set to true, the hamburger button will show badges for update events.
 #ifndef RELEASE_BUILD
 pref("app.update.badge", true);
 #else
 pref("app.update.badge", false);
 #endif
-// Give the user x seconds to reboot before showing a badge on the hamburger
-// button. default=4 days
-pref("app.update.badgeWaitTime", 345600);
+// app.update.badgeWaitTime is in branding section
 
 // If set to true, the Update Service will apply updates in the background
 // when it finishes downloading them.
 pref("app.update.staging.enabled", true);
 
 // Update service URL:
 pref("app.update.url", "https://aus4.mozilla.org/update/3/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml");
 // app.update.url.manual is in branding section
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -26,16 +26,17 @@ let gFxAccounts = {
   get topics() {
     // Do all this dance to lazy-load FxAccountsCommon.
     delete this.topics;
     return this.topics = [
       "weave:service:ready",
       "weave:service:sync:start",
       "weave:service:login:error",
       "weave:service:setup-complete",
+      "weave:ui:login:error",
       "fxa-migration:state-changed",
       this.FxAccountsCommon.ONVERIFIED_NOTIFICATION,
       this.FxAccountsCommon.ONLOGOUT_NOTIFICATION,
       "weave:notification:removed",
     ];
   },
 
   get button() {
--- a/browser/base/content/browser-syncui.js
+++ b/browser/base/content/browser-syncui.js
@@ -222,16 +222,17 @@ let gSyncUI = {
     this.clearError(title);
   },
 
   onSetupComplete: function SUI_onSetupComplete() {
     this.onLoginFinish();
   },
 
   onLoginError: function SUI_onLoginError() {
+    this.log.debug("onLoginError: login=${login}, sync=${sync}", Weave.Status);
     // Note: This is used for *both* Sync and ReadingList login errors.
     // if login fails, any other notifications are essentially moot
     Weave.Notifications.removeAll();
 
     // if we haven't set up the client, don't show errors
     if (this._needsSetup()) {
       this.updateUI();
       return;
@@ -457,16 +458,24 @@ let gSyncUI = {
     // Clear out sync failures on a successful sync
     this.clearError(title);
   },
 
   // Return true if the reading-list is in a "prolonged" error state. That
   // engine doesn't impose what that means, so calculate it here. For
   // consistency, we just use the sync prefs.
   isProlongedReadingListError() {
+    // If the readinglist scheduler is disabled we don't treat it as prolonged.
+    let enabled = false;
+    try {
+      enabled = Services.prefs.getBoolPref("readinglist.scheduler.enabled");
+    } catch (_) {}
+    if (!enabled) {
+      return false;
+    }
     let lastSync, threshold, prolonged;
     try {
       lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
       threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") * 1000);
       prolonged = lastSync <= threshold;
     } catch (ex) {
       // no pref, assume not prolonged.
       prolonged = false;
@@ -515,17 +524,17 @@ let gSyncUI = {
     let notification =
       new Weave.Notification(title, description, null, priority, buttons);
     Weave.Notifications.replaceTitle(notification);
 
     this.updateUI();
   },
 
   onSyncError: function SUI_onSyncError() {
-    this.log.debug("onSyncError");
+    this.log.debug("onSyncError: login=${login}, sync=${sync}", Weave.Status);
     let title = this._stringBundle.GetStringFromName("error.sync.title");
 
     if (Weave.Status.login != Weave.LOGIN_SUCCEEDED) {
       this.onLoginError();
       return;
     }
 
     let description;
--- a/browser/base/content/test/general/browser_syncui.js
+++ b/browser/base/content/test/general/browser_syncui.js
@@ -32,16 +32,33 @@ add_task(function* prepare() {
   // mock out the "_needsSetup()" function so we don't short-circuit.
   let oldNeedsSetup = window.gSyncUI._needsSetup;
   window.gSyncUI._needsSetup = () => false;
   registerCleanupFunction(() => {
     window.gSyncUI._needsSetup = oldNeedsSetup;
   });
 });
 
+add_task(function* testNotProlongedRLErrorWhenDisabled() {
+  // Here we arrange for the (dead?) readinglist scheduler to have a last-synced
+  // date of long ago and the RL scheduler is disabled.
+  // gSyncUI.isProlongedReadingListError() should return false.
+  // Pretend the reading-list is in the "prolonged error" state.
+  let longAgo = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000); // 100 days ago.
+  Services.prefs.setCharPref("readinglist.scheduler.lastSync", longAgo.toString());
+
+  // It's prolonged while it's enabled.
+  Services.prefs.setBoolPref("readinglist.scheduler.enabled", true);
+  Assert.equal(gSyncUI.isProlongedReadingListError(), true);
+
+  // But false when disabled.
+  Services.prefs.setBoolPref("readinglist.scheduler.enabled", false);
+  Assert.equal(gSyncUI.isProlongedReadingListError(), false);
+});
+
 add_task(function* testProlongedSyncError() {
   let promiseNotificationAdded = promiseObserver("weave:notification:added");
   Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
 
   // Pretend we are in the "prolonged error" state.
   Weave.Status.sync = Weave.PROLONGED_SYNC_FAILURE;
   Weave.Status.login = Weave.LOGIN_SUCCEEDED;
   Services.obs.notifyObservers(null, "weave:ui:sync:error", null);
@@ -55,16 +72,17 @@ add_task(function* testProlongedSyncErro
   let promiseNotificationRemoved = promiseObserver("weave:notification:removed");
   Weave.Status.sync = Weave.STATUS_OK;
   Services.obs.notifyObservers(null, "weave:ui:sync:finish", null);
   yield promiseNotificationRemoved;
   Assert.equal(Notifications.notifications.length, 0, "no notifications left");
 });
 
 add_task(function* testProlongedRLError() {
+  Services.prefs.setBoolPref("readinglist.scheduler.enabled", true);
   let promiseNotificationAdded = promiseObserver("weave:notification:added");
   Assert.equal(Notifications.notifications.length, 0, "start with no notifications");
 
   // Pretend the reading-list is in the "prolonged error" state.
   let longAgo = new Date(Date.now() - 100 * 24 * 60 * 60 * 1000); // 100 days ago.
   Services.prefs.setCharPref("readinglist.scheduler.lastSync", longAgo.toString());
   getInternalScheduler().state = ReadingListScheduler.STATE_ERROR_OTHER;
   Services.obs.notifyObservers(null, "readinglist:sync:start", null);
--- a/browser/branding/aurora/pref/firefox-branding.js
+++ b/browser/branding/aurora/pref/firefox-branding.js
@@ -21,11 +21,15 @@ pref("app.update.url.manual", "https://w
 // supplied in the "An update is available" page of the update wizard. 
 pref("app.update.url.details", "https://www.mozilla.org/firefox/aurora/");
 
 // The number of days a binary is permitted to be old
 // without checking for an update.  This assumes that
 // app.update.checkInstallTime is true.
 pref("app.update.checkInstallTime.days", 2);
 
+// Give the user x seconds to reboot before showing a badge on the hamburger
+// button. default=4 days
+pref("app.update.badgeWaitTime", 345600);
+
 // Number of usages of the web console or scratchpad.
 // If this is less than 5, then pasting code into the web console or scratchpad is disabled
-pref("devtools.selfxss.count", 5);
\ No newline at end of file
+pref("devtools.selfxss.count", 5);
--- a/browser/branding/nightly/pref/firefox-branding.js
+++ b/browser/branding/nightly/pref/firefox-branding.js
@@ -19,11 +19,15 @@ pref("app.update.url.manual", "https://n
 // supplied in the "An update is available" page of the update wizard. 
 pref("app.update.url.details", "https://nightly.mozilla.org");
 
 // The number of days a binary is permitted to be old
 // without checking for an update.  This assumes that
 // app.update.checkInstallTime is true.
 pref("app.update.checkInstallTime.days", 2);
 
+// Give the user x seconds to reboot before showing a badge on the hamburger
+// button. default=immediately
+pref("app.update.badgeWaitTime", 0);
+
 // Number of usages of the web console or scratchpad.
 // If this is less than 5, then pasting code into the web console or scratchpad is disabled
-pref("devtools.selfxss.count", 5);
\ No newline at end of file
+pref("devtools.selfxss.count", 5);
--- a/browser/branding/official/pref/firefox-branding.js
+++ b/browser/branding/official/pref/firefox-branding.js
@@ -18,11 +18,15 @@ pref("app.update.url.manual", "https://w
 // supplied in the "An update is available" page of the update wizard. 
 pref("app.update.url.details", "https://www.mozilla.org/%LOCALE%/firefox/notes");
 
 // The number of days a binary is permitted to be old
 // without checking for an update.  This assumes that
 // app.update.checkInstallTime is true.
 pref("app.update.checkInstallTime.days", 63);
 
+// Give the user x seconds to reboot before showing a badge on the hamburger
+// button. default=immediately
+pref("app.update.badgeWaitTime", 0);
+
 // Number of usages of the web console or scratchpad.
 // If this is less than 5, then pasting code into the web console or scratchpad is disabled
-pref("devtools.selfxss.count", 0);
\ No newline at end of file
+pref("devtools.selfxss.count", 0);
--- a/browser/branding/unofficial/pref/firefox-branding.js
+++ b/browser/branding/unofficial/pref/firefox-branding.js
@@ -18,11 +18,15 @@ pref("app.update.url.manual", "https://n
 // supplied in the "An update is available" page of the update wizard. 
 pref("app.update.url.details", "https://nightly.mozilla.org");
 
 // The number of days a binary is permitted to be old
 // without checking for an update.  This assumes that
 // app.update.checkInstallTime is true.
 pref("app.update.checkInstallTime.days", 2);
 
+// Give the user x seconds to reboot before showing a badge on the hamburger
+// button. default=immediately
+pref("app.update.badgeWaitTime", 0);
+
 // Number of usages of the web console or scratchpad.
 // If this is less than 5, then pasting code into the web console or scratchpad is disabled
-pref("devtools.selfxss.count", 0);
\ No newline at end of file
+pref("devtools.selfxss.count", 0);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -2623,19 +2623,19 @@ let DefaultBrowserCheck = {
       this._notification.close();
     }
   },
 
   setAsDefault: function() {
     let claimAllTypes = true;
 #ifdef XP_WIN
     try {
-      // In Windows 8, the UI for selecting default protocol is much
+      // In Windows 8+, the UI for selecting default protocol is much
       // nicer than the UI for setting file type associations. So we
-      // only show the protocol association screen on Windows 8.
+      // only show the protocol association screen on Windows 8+.
       // Windows 8 is version 6.2.
       let version = Services.sysinfo.getProperty("version");
       claimAllTypes = (parseFloat(version) < 6.2);
     } catch (ex) { }
 #endif
     try {
       ShellService.setDefaultBrowser(claimAllTypes, false);
     } catch (ex) {
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -1466,19 +1466,16 @@ XPCOMUtils.defineLazyGetter(PlacesUIUtil
       new PlacesSetItemAnnotationTransaction(aItemId, aAnnotationObject),
 
     setPageAnnotation: function(aURI, aAnnotationObject)
       new PlacesSetPageAnnotationTransaction(aURI, aAnnotationObject),
 
     editBookmarkKeyword: function(aItemId, aNewKeyword)
       new PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword),
 
-    editBookmarkPostData: function(aItemId, aPostData)
-      new PlacesEditBookmarkPostDataTransaction(aItemId, aPostData),
-
     editLivemarkSiteURI: function(aLivemarkId, aSiteURI)
       new PlacesEditLivemarkSiteURITransaction(aLivemarkId, aSiteURI),
 
     editLivemarkFeedURI: function(aLivemarkId, aFeedURI)
       new PlacesEditLivemarkFeedURITransaction(aLivemarkId, aFeedURI),
 
     editItemDateAdded: function(aItemId, aNewDateAdded)
       new PlacesEditItemDateAddedTransaction(aItemId, aNewDateAdded),
--- a/browser/components/places/content/bookmarkProperties.js
+++ b/browser/components/places/content/bookmarkProperties.js
@@ -278,17 +278,18 @@ var BookmarkPropertiesPanel = {
         gEditItemOverlay.initPanel({ node: this._node
                                    , hiddenRows: this._hiddenRows });
         acceptButton.disabled = gEditItemOverlay.readOnly;
         break;
       case ACTION_ADD:
         this._node = yield this._promiseNewItem();
         // Edit the new item
         gEditItemOverlay.initPanel({ node: this._node
-                                   , hiddenRows: this._hiddenRows });
+                                   , hiddenRows: this._hiddenRows
+                                   , postData: this._postData });
 
         // Empty location field if the uri is about:blank, this way inserting a new
         // url will be easier for the user, Accept button will be automatically
         // disabled by the input listener until the user fills the field.
         let locationField = this._element("locationField");
         if (locationField.value == "about:blank")
           locationField.value = "";
 
@@ -520,32 +521,28 @@ var BookmarkPropertiesPanel = {
 
     if (this._loadInSidebar) {
       let annoObj = { name   : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO,
                       value  : true };
       let setLoadTxn = new PlacesSetItemAnnotationTransaction(-1, annoObj);
       childTransactions.push(setLoadTxn);
     }
 
-    if (this._postData) {
-      let postDataTxn = new PlacesEditBookmarkPostDataTransaction(-1, this._postData);
-      childTransactions.push(postDataTxn);
-    }
-
     //XXX TODO: this should be in a transaction!
     if (this._charSet && !PrivateBrowsingUtils.isWindowPrivate(window))
       PlacesUtils.setCharsetForURI(this._uri, this._charSet);
 
     let createTxn = new PlacesCreateBookmarkTransaction(this._uri,
                                                         aContainer,
                                                         aIndex,
                                                         this._title,
                                                         this._keyword,
                                                         annotations,
-                                                        childTransactions);
+                                                        childTransactions,
+                                                        this._postData);
 
     return new PlacesAggregatedTransaction(this._getDialogTitle(),
                                            [createTxn]);
   },
 
   /**
    * Returns a childItems-transactions array representing the URIList with
    * which the dialog has been opened.
--- a/browser/components/places/content/editBookmarkOverlay.js
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -37,29 +37,30 @@ let gEditItemOverlay = {
     let isURI = node && PlacesUtils.nodeIsURI(node);
     let uri = isURI ? NetUtil.newURI(node.uri) : null;
     let title = node ? node.title : null;
     let isBookmark = isItem && isURI;
     let bulkTagging = !node;
     let uris = bulkTagging ? aInitInfo.uris : null;
     let visibleRows = new Set();
     let isParentReadOnly = false;
+    let postData = aInitInfo.postData;
     if (node && "parent" in node) {
       let parent = node.parent;
       if (parent) {
         isParentReadOnly = !PlacesUtils.nodeIsFolder(parent) ||
                             PlacesUIUtils.isContentsReadOnly(parent);
       }
     }
 
     return this._paneInfo = { itemId, itemGuid, isItem,
                               isURI, uri, title,
                               isBookmark, isFolderShortcut, isParentReadOnly,
                               bulkTagging, uris,
-                              visibleRows };
+                              visibleRows, postData };
   },
 
   get initialized() {
     return this._paneInfo != null;
   },
 
   // Backwards-compatibility getters
   get itemId() {
@@ -591,17 +592,17 @@ let gEditItemOverlay = {
 
   onKeywordFieldChange() {
     if (this.readOnly || !this._paneInfo.isBookmark)
       return;
 
     let itemId = this._paneInfo.itemId;
     let newKeyword = this._keywordField.value;
     if (!PlacesUIUtils.useAsyncTransactions) {
-      let txn = new PlacesEditBookmarkKeywordTransaction(itemId, newKeyword);
+      let txn = new PlacesEditBookmarkKeywordTransaction(itemId, newKeyword, this._paneInfo.postData);
       PlacesUtils.transactionManager.doTransaction(txn);
       return;
     }
     let guid = this._paneInfo.itemGuid;
     PlacesTransactions.EditKeyword({ guid, keyword: newKeyword })
                       .transact().catch(Components.utils.reportError);
   },
 
--- a/browser/components/places/tests/browser/browser.ini
+++ b/browser/components/places/tests/browser/browser.ini
@@ -5,47 +5,45 @@
 [DEFAULT]
 skip-if = buildapp == "mulet"
 support-files =
   head.js
   framedPage.html
   frameLeft.html
   frameRight.html
   sidebarpanels_click_test_page.html
+  keyword_form.html
 
 [browser_0_library_left_pane_migration.js]
-[browser_library_left_pane_fixnames.js]
-[browser_425884.js]
-[browser_475045.js]
+[browser_410196_paste_into_tags.js]
+[browser_416459_cut.js]
 [browser_423515.js]
-[browser_410196_paste_into_tags.js]
-[browser_sort_in_library.js]
-[browser_library_open_leak.js]
-[browser_library_panel_leak.js]
-[browser_library_search.js]
-[browser_history_sidebar_search.js]
-skip-if = e10s && (os == 'linux' || os == 'mac') # Bug 1116457
+[browser_425884.js]
+[browser_435851_copy_query.js]
+[browser_475045.js]
+[browser_555547.js]
+[browser_bookmarkProperties_addKeywordForThisSearch.js]
 [browser_bookmarksProperties.js]
-
-[browser_forgetthissite_single.js]
-[browser_library_commands.js]
 [browser_drag_bookmarks_on_toolbar.js]
 skip-if = e10s # Bug ?????? - test fails - "Number of dragged items should be the same. - Got 0, expected 1"
+[browser_forgetthissite_single.js]
+[browser_history_sidebar_search.js]
+skip-if = e10s && (os == 'linux' || os == 'mac') # Bug 1116457
+[browser_library_batch_delete.js]
+[browser_library_commands.js]
+[browser_library_downloads.js]
+[browser_library_infoBox.js]
+[browser_library_left_pane_fixnames.js]
+[browser_library_left_pane_select_hierarchy.js]
 [browser_library_middleclick.js]
+[browser_library_open_leak.js]
+[browser_library_openFlatContainer.js]
+[browser_library_panel_leak.js]
+[browser_library_search.js]
 [browser_library_views_liveupdate.js]
-[browser_views_liveupdate.js]
-
-[browser_sidebarpanels_click.js]
-# temporarily disabled for breaking the treeview - bug 658744
-skip-if = true
-
-[browser_library_infoBox.js]
 [browser_markPageAsFollowedLink.js]
 skip-if = e10s # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly (test does EventUtils.sendMouseEvent...)
+[browser_sidebarpanels_click.js]
+skip-if = true # temporarily disabled for breaking the treeview - bug 658744
+[browser_sort_in_library.js]
 [browser_toolbar_migration.js]
-[browser_library_batch_delete.js]
-[browser_555547.js]
-[browser_416459_cut.js]
-[browser_library_downloads.js]
-[browser_library_left_pane_select_hierarchy.js]
-[browser_435851_copy_query.js]
 [browser_toolbarbutton_menu_context.js]
-[browser_library_openFlatContainer.js]
+[browser_views_liveupdate.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js
@@ -0,0 +1,57 @@
+"use strict"
+
+const TEST_URL = "http://mochi.test:8888/browser/browser/components/places/tests/browser/keyword_form.html";
+
+add_task(function* () {
+  yield BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: TEST_URL,
+  }, function* (browser) {
+    // We must wait for the context menu code to build metadata.
+    yield openContextMenuForContentSelector(browser, 'form > input[name="search"]');
+
+    yield withBookmarksDialog(function*() {
+      AddKeywordForSearchField();
+    }, function* (dialogWin) {
+      let acceptBtn = dialogWin.document.documentElement.getButton("accept");
+      ok(acceptBtn.disabled, "Accept button is disabled");
+
+      let promiseKeywordNotification = promiseBookmarksNotification(
+        "onItemChanged", (itemId, prop, isAnno, val) => prop == "keyword" && val =="kw");
+
+      fillBookmarkTextField("editBMPanel_keywordField", "kw", dialogWin);
+
+      ok(!acceptBtn.disabled, "Accept button is enabled");
+
+      // The dialog is instant apply.
+      yield promiseKeywordNotification;
+
+      // After the notification, the keywords cache will update asynchronously.
+      info("Check the keyword entry has been created");
+      let entry;
+      yield waitForCondition(function* () {
+        entry = yield PlacesUtils.keywords.fetch("kw");
+        return !!entry;
+      },"Unable to find the expected keyword");
+      is(entry.keyword, "kw", "keyword is correct");
+      is(entry.url.href, TEST_URL, "URL is correct");
+      is(entry.postData, "accenti%3D%E0%E8%EC%F2%F9&search%3D%25s", "POST data is correct");
+
+      info("Check the charset has been saved");
+      let charset = yield PlacesUtils.getCharsetForURI(NetUtil.newURI(TEST_URL));
+      is(charset, "windows-1252", "charset is correct");
+
+      // Now check getShortcutOrURI.
+      let data = yield getShortcutOrURIAndPostData("kw test");
+      is(getPostDataString(data.postData), "accenti=\u00E0\u00E8\u00EC\u00F2\u00F9&search=test", "getShortcutOrURI POST data is correct");
+      is(data.url, TEST_URL, "getShortcutOrURI URL is correct");
+    });
+  });
+});
+
+function getPostDataString(stream) {
+  let sis = Cc["@mozilla.org/scriptableinputstream;1"]
+              .createInstance(Ci.nsIScriptableInputStream);
+  sis.init(stream);
+  return sis.read(stream.available()).split("\n").pop();
+}
--- a/browser/components/places/tests/browser/head.js
+++ b/browser/components/places/tests/browser/head.js
@@ -157,28 +157,31 @@ function promiseIsURIVisited(aURI) {
   PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
     deferred.resolve(aIsVisited);
   });
 
   return deferred.promise;
 }
 
 function promiseBookmarksNotification(notification, conditionFn) {
-  info(`Waiting for ${notification}`);
+  info(`promiseBookmarksNotification: waiting for ${notification}`);
   return new Promise((resolve, reject) => {
     let proxifiedObserver = new Proxy({}, {
       get: (target, name) => {
         if (name == "QueryInterface")
           return XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver ]);
+        info(`promiseBookmarksNotification: got ${name} notification`);
         if (name == notification)
           return () => {
             if (conditionFn.apply(this, arguments)) {
               clearTimeout(timeout);
               PlacesUtils.bookmarks.removeObserver(proxifiedObserver, false);
               executeSoon(resolve);
+            } else {
+              info(`promiseBookmarksNotification: skip cause condition doesn't apply to ${JSON.stringify(arguments)}`);
             }
           }
         return () => {};
       }
     });
     PlacesUtils.bookmarks.addObserver(proxifiedObserver, false);
     let timeout = setTimeout(() => {
       PlacesUtils.bookmarks.removeObserver(proxifiedObserver, false);
@@ -273,8 +276,136 @@ function promiseSetToolbarVisibility(aTo
 function isToolbarVisible(aToolbar) {
   let hidingAttribute = aToolbar.getAttribute("type") == "menubar"
                         ? "autohide"
                         : "collapsed";
   let hidingValue = aToolbar.getAttribute(hidingAttribute).toLowerCase();
   // Check for both collapsed="true" and collapsed="collapsed"
   return hidingValue !== "true" && hidingValue !== hidingAttribute;
 }
+
+/**
+ * Executes a task after opening the bookmarks dialog, then cancels the dialog.
+ *
+ * @param openFn
+ *        generator function causing the dialog to open
+ * @param task
+ *        the task to execute once the dialog is open
+ */
+let withBookmarksDialog = Task.async(function* (openFn, taskFn) {
+  let dialogPromise = new Promise(resolve => {
+    Services.ww.registerNotification(function winObserver(subject, topic, data) {
+      if (topic != "domwindowopened")
+        return;
+      let win = subject.QueryInterface(Ci.nsIDOMWindow);
+      win.addEventListener("load", function load() {
+        win.removeEventListener("load", load);
+        ok(win.location.href.startsWith("chrome://browser/content/places/bookmarkProperties"),
+           "The bookmark properties dialog is ready");
+        Services.ww.unregisterNotification(winObserver);
+        // This is needed for the overlay.
+        waitForFocus(() => {
+          resolve(win);
+        }, win);
+      });
+    });
+  });
+
+  info("withBookmarksDialog: opening the dialog");
+  yield openFn();
+  info("withBookmarksDialog: waiting for the dialog");
+  let dialogWin = yield dialogPromise;
+
+  // Ensure overlay is loaded
+  ok(dialogWin.gEditItemOverlay.initialized, "EditItemOverlay is initialized");
+
+  info("withBookmarksDialog: executing the task");
+  try {
+    yield taskFn(dialogWin);
+  } finally {
+    info("withBookmarksDialog: canceling the dialog");
+    dialogWin.document.documentElement.cancelDialog();
+  }
+});
+
+/**
+ * Opens the contextual menu on the element pointed by the given selector.
+ *
+ * @param selector
+ *        Valid selector syntax
+ * @return the target DOM node.
+ */
+let openContextMenuForContentSelector = Task.async(function* (browser, selector) {
+  info("wait for the context menu");
+  let contextPromise = BrowserTestUtils.waitForEvent(document.getElementById("contentAreaContextMenu"),
+                                                     "popupshown");
+  yield ContentTask.spawn(browser, { selector }, function* (args) {
+    let doc = content.document;
+    let elt = doc.querySelector(args.selector)
+    dump(`openContextMenuForContentSelector: found ${elt}\n`);
+
+    /* Open context menu so chrome can access the element */
+    const domWindowUtils =
+      content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+             .getInterface(Components.interfaces.nsIDOMWindowUtils);
+    let rect = elt.getBoundingClientRect();
+    let left = rect.left + rect.width / 2;
+    let top = rect.top + rect.height / 2;
+    domWindowUtils.sendMouseEvent("contextmenu", left, top, 2,
+                                  1, 0, false, 0, 0, true);
+  });
+  yield contextPromise;
+
+  return gContextMenuContentData.popupNode;
+});
+
+/**
+ * Waits for a specified condition to happen.
+ *
+ * @param conditionFn
+ *        a Function or a generator function, returning a boolean for whether
+ *        the condition is fulfilled.
+ * @param errorMsg
+ *        Error message to use if the condition has not been satisfied after a
+ *        meaningful amount of tries.
+ */
+let waitForCondition = Task.async(function* (conditionFn, errorMsg) {
+  for (let tries = 0; tries < 100; ++tries) {
+    if ((yield conditionFn()))
+      return;
+    yield new Promise(resolve => {
+      if (!waitForCondition._timers) {
+        waitForCondition._timers = new Set();
+        registerCleanupFunction(() => {
+          is(waitForCondition._timers.size, 0, "All the wait timers have been removed");
+          delete waitForCondition._timers;
+        });
+      }
+      let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+      waitForCondition._timers.add(timer);
+      timer.init(() => {
+        waitForCondition._timers.delete(timer);
+        resolve();
+      }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+    });
+  }
+  ok(false, errorMsg);
+});
+
+/**
+ * Fills a bookmarks dialog text field ensuring to cause expected edit events.
+ *
+ * @param id
+ *        id of the text field
+ * @param text
+ *        text to fill in
+ * @param win
+ *        dialog window
+ */
+function fillBookmarkTextField(id, text, win) {
+  let elt = win.document.getElementById(id);
+  elt.focus();
+  elt.select();
+  for (let c of text.split("")) {
+    EventUtils.synthesizeKey(c, {}, win);
+  }
+  elt.blur();
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/places/tests/browser/keyword_form.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+
+<html lang="en">
+<head>
+  <meta http-equiv="Content-Type" content="text/html;charset=windows-1252">
+</head>
+<body>
+  <form method="POST" action="keyword_form.html">
+    <input type="hidden" name="accenti" value="אטלעש">
+    <input type="text" name="search">
+  </form>
+</body>
+</html>
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -682,43 +682,67 @@ var gMainPane = {
   /*
    * Preferences:
    *
    * browser.shell.checkDefault
    * - true if a default-browser check (and prompt to make it so if necessary)
    *   occurs at startup, false otherwise
    */
 
+   /**
+    * Firefox can attempt to set itself as the default application
+    * for all related filetypes or just for HTML. Some platforms
+    * such as Windows have terrible UIs for all filetypes. In those
+    * platforms, Firefox only attempts to associate itself with HTML.
+    */
+   shouldClaimAllTypes: function()
+   {
+    let claimAllTypes = true;
+    try {
+      if (AppConstants.platform == "win") {
+        // In Windows 8+, the UI for selecting default protocol is much
+        // nicer than the UI for setting file type associations. So we
+        // only show the protocol association screen on Windows 8+.
+        // Windows 8 is version 6.2.
+        let version = Services.sysinfo.getProperty("version");
+        claimAllTypes = (parseFloat(version) < 6.2);
+      }
+    } catch (ex) {}
+    return claimAllTypes;
+   },
+
   /**
    * Show button for setting browser as default browser or information that
    * browser is already the default browser.
    */
   updateSetDefaultBrowser: function()
   {
     let shellSvc = getShellService();
     let defaultBrowserBox = document.getElementById("defaultBrowserBox");
     if (!shellSvc) {
       defaultBrowserBox.hidden = true;
       return;
     }
     let setDefaultPane = document.getElementById("setDefaultPane");
-    let selectedIndex = shellSvc.isDefaultBrowser(false, true) ? 1 : 0;
+    let claimAllTypes = gMainPane.shouldClaimAllTypes();
+    let selectedIndex = shellSvc.isDefaultBrowser(false, claimAllTypes) ? 1 : 0;
     setDefaultPane.selectedIndex = selectedIndex;
   },
 
   /**
    * Set browser as the operating system default browser.
    */
   setDefaultBrowser: function()
   {
     let shellSvc = getShellService();
     if (!shellSvc)
       return;
     try {
-      shellSvc.setDefaultBrowser(true, false);
+      let claimAllTypes = gMainPane.shouldClaimAllTypes();
+      shellSvc.setDefaultBrowser(claimAllTypes, false);
     } catch (ex) {
       Cu.reportError(ex);
       return;
     }
     let selectedIndex =
       shellSvc.isDefaultBrowser(false, true) ? 1 : 0;
     document.getElementById("setDefaultPane").selectedIndex = selectedIndex;
   }
--- a/browser/components/shell/nsWindowsShellService.cpp
+++ b/browser/components/shell/nsWindowsShellService.cpp
@@ -34,16 +34,19 @@
 #include "windows.h"
 #include "shellapi.h"
 
 #ifdef _WIN32_WINNT
 #undef _WIN32_WINNT
 #endif
 #define _WIN32_WINNT 0x0600
 #define INITGUID
+#undef NTDDI_VERSION
+#define NTDDI_VERSION NTDDI_WIN8
+// Needed for access to IApplicationActivationManager
 #include <shlobj.h>
 
 #include <mbstring.h>
 #include <shlwapi.h>
 
 #ifndef MAX_BUF
 #define MAX_BUF 4096
 #endif
@@ -658,16 +661,38 @@ nsWindowsShellService::LaunchControlPane
   }
   CloseHandle(pi.hProcess);
   CloseHandle(pi.hThread);
 
   return NS_OK;
 }
 
 nsresult
+nsWindowsShellService::LaunchModernSettingsDialogDefaultApps()
+{
+  IApplicationActivationManager* pActivator;
+  HRESULT hr = CoCreateInstance(CLSID_ApplicationActivationManager,
+                                nullptr,
+                                CLSCTX_INPROC,
+                                IID_IApplicationActivationManager,
+                                (void**)&pActivator);
+
+  if (SUCCEEDED(hr)) {
+    DWORD pid;
+    hr = pActivator->ActivateApplication(
+           L"windows.immersivecontrolpanel_cw5n1h2txyewy"
+           L"!microsoft.windows.immersivecontrolpanel",
+           L"page=SettingsPageAppsDefaults", AO_NONE, &pid);
+    pActivator->Release();
+    return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
+  }
+  return NS_OK;
+}
+
+nsresult
 nsWindowsShellService::LaunchHTTPHandlerPane()
 {
   OPENASINFO info;
   info.pcszFile = L"http";
   info.pcszClass = nullptr;
   info.oaifInFlags = OAIF_FORCE_REGISTRATION | 
                      OAIF_URL_PROTOCOL |
                      OAIF_REGISTER_EXT;
@@ -692,19 +717,27 @@ nsWindowsShellService::SetDefaultBrowser
     if (aClaimAllTypes) {
       rv = LaunchControlPanelDefaultPrograms();
       // The above call should never really fail, but just in case
       // fall back to showing the HTTP association screen only.
       if (NS_FAILED(rv)) {
         rv = LaunchHTTPHandlerPane();
       }
     } else {
-      rv = LaunchHTTPHandlerPane();
-      // The above calls hould never really fail, but just in case
-      // fallb ack to showing control panel for all defaults
+      // Windows 10 blocks attempts to load the HTTP Handler
+      // association dialog, so the modern Settings dialog
+      // is opened with the Default Apps view loaded.
+      if (IsWin10OrLater()) {
+        rv = LaunchModernSettingsDialogDefaultApps();
+      } else {
+        rv = LaunchHTTPHandlerPane();
+      }
+
+      // The above call should never really fail, but just in case
+      // fall back to showing control panel for all defaults
       if (NS_FAILED(rv)) {
         rv = LaunchControlPanelDefaultPrograms();
       }
     }
   }
 
   nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
   if (prefs) {
--- a/browser/components/shell/nsWindowsShellService.h
+++ b/browser/components/shell/nsWindowsShellService.h
@@ -23,15 +23,16 @@ public:
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSISHELLSERVICE
   NS_DECL_NSIWINDOWSSHELLSERVICE
 
 protected:
   bool IsDefaultBrowserVista(bool aCheckAllTypes, bool* aIsDefaultBrowser);
   nsresult LaunchControlPanelDefaultPrograms();
+  nsresult LaunchModernSettingsDialogDefaultApps();
   nsresult LaunchHTTPHandlerPane();
 
 private:
   bool      mCheckedThisSession;
 };
 
 #endif // nswindowsshellservice_h____
--- a/build/mobile/remoteautomation.py
+++ b/build/mobile/remoteautomation.py
@@ -19,16 +19,20 @@ import mozcrash
 # signatures for logcat messages that we don't care about much
 fennecLogcatFilters = [ "The character encoding of the HTML document was not declared",
                         "Use of Mutation Events is deprecated. Use MutationObserver instead.",
                         "Unexpected value from nativeGetEnabledTags: 0" ]
 
 class RemoteAutomation(Automation):
     _devicemanager = None
 
+    # Part of a hack for Robocop: "am COMMAND" is handled specially if COMMAND
+    # is in this set. See usages below.
+    _specialAmCommands = ('instrument', 'start')
+
     def __init__(self, deviceManager, appName = '', remoteLog = None,
                  processArgs=None):
         self._devicemanager = deviceManager
         self._appName = appName
         self._remoteProfile = None
         self._remoteLog = remoteLog
         self._processArgs = processArgs or {};
 
@@ -232,17 +236,17 @@ class RemoteAutomation(Automation):
 
     def buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs):
         # If remote profile is specified, use that instead
         if (self._remoteProfile):
             profileDir = self._remoteProfile
 
         # Hack for robocop, if app & testURL == None and extraArgs contains the rest of the stuff, lets
         # assume extraArgs is all we need
-        if app == "am" and extraArgs[0] == "instrument":
+        if app == "am" and extraArgs[0] in RemoteAutomation._specialAmCommands:
             return app, extraArgs
 
         cmd, args = Automation.buildCommandLine(self, app, debuggerInfo, profileDir, testURL, extraArgs)
         # Remove -foreground if it exists, if it doesn't this just returns
         try:
             args.remove('-foreground')
         except:
             pass
@@ -270,17 +274,17 @@ class RemoteAutomation(Automation):
             self.messageLogger = messageLogger
 
             if (self.proc is None):
                 if cmd[0] == 'am':
                     self.proc = stdout
                 else:
                     raise Exception("unable to launch process")
             self.procName = cmd[0].split('/')[-1]
-            if cmd[0] == 'am' and cmd[1] == "instrument":
+            if cmd[0] == 'am' and cmd[1] in RemoteAutomation._specialAmCommands:
                 self.procName = app
                 print "Robocop process name: "+self.procName
 
             # Setting timeout at 1 hour since on a remote device this takes much longer.
             # Temporarily increased to 75 minutes because no more chunks can be created.
             self.timeout = 4500
             # The benefit of the following sleep is unclear; it was formerly 15 seconds
             time.sleep(1)
--- a/build/mobile/robocop/AndroidManifest.xml.in
+++ b/build/mobile/robocop/AndroidManifest.xml.in
@@ -3,17 +3,21 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="org.mozilla.roboexample.test"
 #ifdef MOZ_ANDROID_SHARED_ID
     android:sharedUserId="@MOZ_ANDROID_SHARED_ID@"
 #endif
     android:versionCode="1"
     android:versionName="1.0" >
 
-    <uses-sdk android:minSdkVersion="8" />
+    <uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
+#ifdef MOZ_ANDROID_MAX_SDK_VERSION
+              android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
+#endif
+              android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
 
     <instrumentation
         android:name="org.mozilla.gecko.FennecInstrumentationTestRunner"
         android:targetPackage="@ANDROID_PACKAGE_NAME@" />
 
     <application
         android:label="@string/app_name" >
         <uses-library android:name="android.test.runner" />
@@ -38,11 +42,19 @@
                 <action android:name="android.intent.action.SEND" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="text/*" />
                 <data android:mimeType="image/*" />
             </intent-filter>
 
         </activity>
 
+        <activity android:name="org.mozilla.gecko.LaunchFennecWithConfigurationActivity"
+                  android:label="Robocop Fennec">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
     </application>
 
 </manifest>
new file mode 100644
--- /dev/null
+++ b/build/mobile/robocop/LaunchFennecWithConfigurationActivity.java
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko;
+
+import java.util.Map;
+
+import org.mozilla.gecko.tests.BaseRobocopTest;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+/**
+ * An Activity that extracts Robocop settings from robotium.config, launches
+ * Fennec with the Robocop testing parameters, and finishes itself.
+ * <p>
+ * This is intended to be used by local testers using |mach robocop --serve|.
+ */
+public class LaunchFennecWithConfigurationActivity extends Activity {
+    @Override
+    public void onCreate(Bundle arguments) {
+        super.onCreate(arguments);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        final String configFile = FennecNativeDriver.getFile(BaseRobocopTest.DEFAULT_ROOT_PATH + "/robotium.config");
+        final Map<String, String> config = FennecNativeDriver.convertTextToTable(configFile);
+        final Intent intent = BaseRobocopTest.createActivityIntent(config);
+
+        intent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
+
+        this.finish();
+        this.startActivity(intent);
+    }
+}
--- a/build/mobile/robocop/Makefile.in
+++ b/build/mobile/robocop/Makefile.in
@@ -19,16 +19,17 @@ ANDROID_ASSETS_DIR := $(TESTPATH)/assets
   Driver.java \
   Element.java \
   FennecInstrumentationTestRunner.java \
   FennecNativeActions.java \
   FennecMochitestAssert.java \
   FennecTalosAssert.java \
   FennecNativeDriver.java \
   FennecNativeElement.java \
+  LaunchFennecWithConfigurationActivity.java \
   RoboCopException.java \
   RobocopShare1.java \
   RobocopShare2.java \
   RobocopUtils.java \
   PaintedSurface.java \
   StructuredLogger.java \
   $(NULL)
 
--- a/mobile/android/base/GeckoInputConnection.java
+++ b/mobile/android/base/GeckoInputConnection.java
@@ -12,17 +12,16 @@ import java.util.concurrent.SynchronousQ
 
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.gfx.InputConnectionHandler;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.ThreadUtils.AssertBehavior;
 
-import android.R;
 import android.content.Context;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.SystemClock;
 import android.text.Editable;
 import android.text.InputType;
 import android.text.Selection;
 import android.text.SpannableString;
@@ -286,37 +285,37 @@ class GeckoInputConnection
         Editable editable = getEditable();
         if (editable == null) {
             return false;
         }
         int selStart = Selection.getSelectionStart(editable);
         int selEnd = Selection.getSelectionEnd(editable);
 
         switch (id) {
-            case R.id.selectAll:
+            case android.R.id.selectAll:
                 setSelection(0, editable.length());
                 break;
-            case R.id.cut:
+            case android.R.id.cut:
                 // If selection is empty, we'll select everything
                 if (selStart == selEnd) {
                     // Fill the clipboard
                     Clipboard.setText(editable);
                     editable.clear();
                 } else {
                     Clipboard.setText(
                             editable.toString().substring(
                                 Math.min(selStart, selEnd),
                                 Math.max(selStart, selEnd)));
                     editable.delete(selStart, selEnd);
                 }
                 break;
-            case R.id.paste:
+            case android.R.id.paste:
                 commitText(Clipboard.getText(), 1);
                 break;
-            case R.id.copy:
+            case android.R.id.copy:
                 // Copy the current selection or the empty string if nothing is selected.
                 String copiedText = selStart == selEnd ? "" :
                                     editable.toString().substring(
                                         Math.min(selStart, selEnd),
                                         Math.max(selStart, selEnd));
                 Clipboard.setText(copiedText);
                 break;
         }
--- a/mobile/android/base/toolbar/ToolbarDisplayLayout.java
+++ b/mobile/android/base/toolbar/ToolbarDisplayLayout.java
@@ -224,16 +224,17 @@ public class ToolbarDisplayLayout extend
         final int lockAnimDuration = 300;
         mLockFadeIn.setDuration(lockAnimDuration);
         mTitleSlideLeft.setDuration(lockAnimDuration);
         mTitleSlideRight.setDuration(lockAnimDuration);
     }
 
     @Override
     public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
         mIsAttached = false;
     }
 
     @Override
     public void onAnimationStart(Animation animation) {
         if (animation.equals(mLockFadeIn)) {
             if (mSiteSecurityVisible)
                 mSiteSecurity.setVisibility(View.VISIBLE);
new file mode 100644
--- /dev/null
+++ b/mobile/android/gradle/base/lint.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<lint>
+    <!-- Enable relevant checks disabled by default -->
+    <issue id="NegativeMargin" severity="warning" />
+</lint>
--- a/mobile/android/mach_commands.py
+++ b/mobile/android/mach_commands.py
@@ -125,16 +125,17 @@ class MachCommands(MachCommandBase):
         srcdir('app/src/robocop_harness/org/mozilla/gecko', 'build/mobile/robocop')
         srcdir('app/src/robocop/org/mozilla/gecko/tests', 'mobile/android/tests/browser/robocop')
         srcdir('app/src/background/org/mozilla/gecko', 'mobile/android/tests/background/junit3/src')
         srcdir('app/src/browser/org/mozilla/gecko', 'mobile/android/tests/browser/junit3/src')
         # Test libraries.
         srcdir('app/libs', 'build/mobile/robocop')
 
         srcdir('base/build.gradle', 'mobile/android/gradle/base/build.gradle')
+        srcdir('base/lint.xml', 'mobile/android/gradle/base/lint.xml')
         srcdir('base/src/main/AndroidManifest.xml', 'mobile/android/gradle/base/AndroidManifest.xml')
         srcdir('base/src/main/java/org/mozilla/gecko', 'mobile/android/base')
         srcdir('base/src/main/java/org/mozilla/mozstumbler', 'mobile/android/stumbler/java/org/mozilla/mozstumbler')
         srcdir('base/src/main/java/org/mozilla/search', 'mobile/android/search/java/org/mozilla/search')
         srcdir('base/src/main/res', 'mobile/android/base/resources')
         srcdir('base/src/crashreporter/res', 'mobile/android/base/crashreporter/res')
 
         manifest_path = os.path.join(self.topobjdir, 'mobile', 'android', 'gradle.manifest')
--- a/mobile/android/tests/browser/robocop/BaseRobocopTest.java
+++ b/mobile/android/tests/browser/robocop/BaseRobocopTest.java
@@ -1,49 +1,57 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tests;
 
-import java.util.Map;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.PowerManager;
+import android.test.ActivityInstrumentationTestCase2;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.jayway.android.robotium.solo.Solo;
 
 import org.apache.http.HttpResponse;
 import org.apache.http.client.HttpClient;
 import org.apache.http.client.methods.HttpGet;
 import org.apache.http.impl.client.DefaultHttpClient;
 import org.mozilla.gecko.Actions;
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.Assert;
+import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.Driver;
 import org.mozilla.gecko.FennecInstrumentationTestRunner;
 import org.mozilla.gecko.FennecMochitestAssert;
 import org.mozilla.gecko.FennecNativeActions;
 import org.mozilla.gecko.FennecNativeDriver;
 import org.mozilla.gecko.FennecTalosAssert;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.PowerManager;
-import android.test.ActivityInstrumentationTestCase2;
-import android.util.Log;
-
-import com.jayway.android.robotium.solo.Solo;
+import java.util.Map;
 
 @SuppressWarnings("unchecked")
 public abstract class BaseRobocopTest extends ActivityInstrumentationTestCase2<Activity> {
+    public static final String LOGTAG = "BaseTest";
+
     public enum Type {
         MOCHITEST,
         TALOS
     }
 
-    private static final String DEFAULT_ROOT_PATH = "/mnt/sdcard/tests";
+    public static final String DEFAULT_ROOT_PATH = "/mnt/sdcard/tests";
+
+    // How long to wait for a Robocop:Quit message to actually kill Fennec.
+    private static final int ROBOCOP_QUIT_WAIT_MS = 180000;
 
     /**
      * The Java Class instance that launches the browser.
      * <p>
      * This should always agree with {@link AppConstants#MOZ_ANDROID_BROWSER_INTENT_CLASS}.
      */
     public static final Class<? extends Activity> BROWSER_INTENT_CLASS;
 
@@ -71,18 +79,16 @@ public abstract class BaseRobocopTest ex
     protected Solo mSolo;
     protected Driver mDriver;
     protected Actions mActions;
 
     protected String mProfile;
 
     protected StringHelper mStringHelper;
 
-    protected abstract Intent createActivityIntent();
-
     /**
      * The browser is started at the beginning of this test. A single test is a
      * class inheriting from <code>BaseRobocopTest</code> that contains test
      * methods.
      * <p>
      * If a test should not start the browser at the beginning of a test,
      * specify a different activity class to the one-argument constructor. To do
      * as little as possible, specify <code>Activity.class</code>.
@@ -107,16 +113,40 @@ public abstract class BaseRobocopTest ex
      * <p>
      * By default tests are mochitests, but a test can override this method in
      * order to change its type. Most Robocop tests are mochitests.
      */
     protected Type getTestType() {
         return Type.MOCHITEST;
     }
 
+    // Member function to allow specialization.
+    protected Intent createActivityIntent() {
+        return BaseRobocopTest.createActivityIntent(mConfig);
+    }
+
+    // Static function to allow re-use.
+    public static Intent createActivityIntent(Map<String, String> config) {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.putExtra("args", "-no-remote -profile " + config.get("profile"));
+        // Don't show the first run experience.
+        intent.putExtra(BrowserApp.EXTRA_SKIP_STARTPANE, true);
+
+        final String envString = config.get("envvars");
+        if (!TextUtils.isEmpty(envString)) {
+            final String[] envStrings = envString.split(",");
+
+            for (int iter = 0; iter < envStrings.length; iter++) {
+                intent.putExtra("env" + iter, envStrings[iter]);
+            }
+        }
+
+        return intent;
+    }
+
     @Override
     protected void setUp() throws Exception {
         // Disable the updater.
         UpdateServiceHelper.setEnabled(false);
 
         // Load config file from root path (set up by Python script).
         mRootPath = FennecInstrumentationTestRunner.getFennecArguments().getString("deviceroot");
         if (mRootPath == null) {
@@ -147,17 +177,53 @@ public abstract class BaseRobocopTest ex
         Activity tempActivity = getActivity();
 
         StringHelper.initialize(tempActivity.getResources());
         mStringHelper = StringHelper.get();
 
         mSolo = new Solo(getInstrumentation(), tempActivity);
         mDriver = new FennecNativeDriver(tempActivity, mSolo, mRootPath);
         mActions = new FennecNativeActions(tempActivity, mSolo, getInstrumentation(), mAsserter);
+    }
 
+    @Override
+    public void tearDown() throws Exception {
+        try {
+            mAsserter.endTest();
+
+            // By default, we don't quit Fennec on finish, and we don't finish
+            // all opened activities. Not quiting Fennec entirely is intended to
+            // make life better for local testers, who might want to alter a
+            // test that is under development rather than Fennec itself. Not
+            // finishing activities is intended to allow local testers to
+            // manually inspect an activity's state after a test
+            // run. runtestsremote.py sets this to "1".  Testers running via an
+            // IDE will not have this set at all.
+            final String quitAndFinish = FennecInstrumentationTestRunner.getFennecArguments()
+                    .getString("quit_and_finish"); // null means not specified.
+            if ("1".equals(quitAndFinish)) {
+                // Request the browser force quit and wait for it to take effect.
+                Log.i(LOGTAG, "Requesting force quit.");
+                GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null));
+                mSolo.sleep(ROBOCOP_QUIT_WAIT_MS);
+
+                // If still running, finish activities as recommended by Robotium.
+                Log.i(LOGTAG, "Finishing all opened activities.");
+                mSolo.finishOpenedActivities();
+            } else {
+                // This has the effect of keeping the activity-under-test
+                // around; if we don't set it to null, it is killed, either by
+                // finishOpenedActivities above or super.tearDown below.
+                Log.i(LOGTAG, "Not requesting force quit and trying to keep started activity alive.");
+                setActivity(null);
+            }
+        } catch (Throwable e) {
+            e.printStackTrace();
+        }
+        super.tearDown();
     }
 
     /**
      * Function to early abort if we can't reach the given HTTP server. Provides local testers
      * with diagnostic information. Not currently available for TALOS tests, which are rarely run
      * locally in any case.
      */
     public void throwIfHttpGetFails() {
--- a/mobile/android/tests/browser/robocop/BaseTest.java
+++ b/mobile/android/tests/browser/robocop/BaseTest.java
@@ -139,48 +139,16 @@ abstract class BaseTest extends BaseRobo
                 mAsserter.dumpLog("Exception caught during test!", t);
                 mAsserter.ok(false, "Exception caught", t.toString());
             }
             // re-throw to continue bail-out
             throw t;
         }
     }
 
-    @Override
-    public void tearDown() throws Exception {
-        try {
-            mAsserter.endTest();
-            // request a force quit of the browser and wait for it to take effect
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null));
-            mSolo.sleep(120000);
-            // if still running, finish activities as recommended by Robotium
-            mSolo.finishOpenedActivities();
-        } catch (Throwable e) {
-            e.printStackTrace();
-        }
-        super.tearDown();
-    }
-
-    @Override
-    protected Intent createActivityIntent() {
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
-        intent.putExtra("args", "-no-remote -profile " + mProfile);
-
-        final String envString = mConfig.get("envvars");
-        if (!TextUtils.isEmpty(envString)) {
-            final String[] envStrings = envString.split(",");
-
-            for (int iter = 0; iter < envStrings.length; iter++) {
-                intent.putExtra("env" + iter, envStrings[iter]);
-            }
-        }
-
-        return intent;
-    }
-
     public void assertMatches(String value, String regex, String name) {
         if (value == null) {
             mAsserter.ok(false, name, "Expected /" + regex + "/, got null");
             return;
         }
         mAsserter.ok(value.matches(regex), name, "Expected /" + regex +"/, got \"" + value + "\"");
     }
 
--- a/mobile/android/tests/browser/robocop/UITest.java
+++ b/mobile/android/tests/browser/robocop/UITest.java
@@ -54,32 +54,16 @@ abstract class UITest extends BaseRoboco
         initHelpers();
 
         // Ensure Robocop tests have access to network, and are run with Display powered on.
         throwIfHttpGetFails();
         throwIfScreenNotOn();
     }
 
     @Override
-    public void tearDown() throws Exception {
-        try {
-            mAsserter.endTest();
-            // request a force quit of the browser and wait for it to take effect
-            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Robocop:Quit", null));
-            mSolo.sleep(120000);
-            // if still running, finish activities as recommended by Robotium
-            mSolo.finishOpenedActivities();
-        } catch (Throwable e) {
-            e.printStackTrace();
-        }
-
-        super.tearDown();
-    }
-
-    @Override
     protected void runTest() throws Throwable {
         try {
             super.runTest();
         } catch (Throwable t) {
             // save screenshot -- written to /mnt/sdcard/Robotium-Screenshots
             // as <filename>.jpg
             mSolo.takeScreenshot("robocop-screenshot");
             if (mAsserter != null) {
@@ -177,36 +161,16 @@ abstract class UITest extends BaseRoboco
     public String getAbsoluteIpUrl(final String url) {
         return getAbsoluteUrl(mBaseIpUrl, url);
     }
 
     private String getAbsoluteUrl(final String baseUrl, final String url) {
         return baseUrl + "/" + url.replaceAll("(^/)", "");
     }
 
-    @Override
-    protected Intent createActivityIntent() {
-        final Intent intent = new Intent(Intent.ACTION_MAIN);
-
-        // Don't show the first run experience.
-        intent.putExtra(BrowserApp.EXTRA_SKIP_STARTPANE, true);
-        intent.putExtra("args", "-no-remote -profile " + mProfile);
-
-        final String envString = mConfig.get("envvars");
-        if (!TextUtils.isEmpty(envString)) {
-            final String[] envStrings = envString.split(",");
-
-            for (int iter = 0; iter < envStrings.length; iter++) {
-                intent.putExtra("env" + iter, envStrings[iter]);
-            }
-        }
-
-        return intent;
-    }
-
     /**
      * Throws an Exception. Called from overridden JUnit methods to ensure JUnit assertions
      * are not accidentally used over AssertionHelper assertions (the latter of which contains
      * additional logging facilities for use in our test harnesses).
      */
     private static void junit() {
         throw new UnsupportedOperationException(JUNIT_FAILURE_MSG);
     }
--- a/testing/mochitest/mach_commands.py
+++ b/testing/mochitest/mach_commands.py
@@ -575,17 +575,23 @@ class RobocopCommands(MachCommandBase):
     @Command('robocop', category='testing',
              conditions=[conditions.is_android],
              description='Run a Robocop test.',
              parser=setup_argument_parser)
     @CommandArgument('test_paths', nargs='*', metavar='TEST', default=None,
                      help='Test to run. Can be a single Robocop test file (like "testLoad.java") '
                           ' or a directory of tests '
                           '(to run recursively). If omitted, the entire Robocop suite is run.')
-    def run_robocop(self, test_paths, **kwargs):
+    @CommandArgument('--serve', default=False, action='store_true',
+        help='Run no tests but start the mochi.test web server and launch '
+             'Fennec with a test profile.')
+    def run_robocop(self, test_paths, serve=False, **kwargs):
+        if serve:
+            kwargs['autorun'] = False
+
         if not kwargs.get('robocopIni'):
             kwargs['robocopIni'] = os.path.join(self.topobjdir, '_tests', 'testing',
                                                 'mochitest', 'robocop.ini')
 
         if not kwargs.get('robocopApk'):
             kwargs['robocopApk'] = os.path.join(self.topobjdir, 'build', 'mobile',
                                                 'robocop', 'robocop-debug.apk')
 
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -538,16 +538,21 @@ def run_test_harness(options):
         options.extraPrefs.append('layout.css.devPixelsPerPx=1.0')
         options.extraPrefs.append('browser.chrome.dynamictoolbar=false')
         options.extraPrefs.append('browser.snippets.enabled=false')
         options.extraPrefs.append('browser.casting.enabled=true')
 
         if (options.dm_trans == 'adb' and options.robocopApk):
             dm._checkCmd(["install", "-r", options.robocopApk])
 
+        if not options.autorun:
+            # Force a single loop iteration. The iteration will start Fennec and
+            # the httpd server, but not actually run a test.
+            options.testPath = robocop_tests[0]['name']
+
         retVal = None
         # Filtering tests
         active_tests = []
         for test in robocop_tests:
             if options.testPath and options.testPath != test['name']:
                 continue
 
             if 'disabled' in test:
@@ -565,30 +570,46 @@ def run_test_harness(options):
             # each cycle
             if mochitest.localProfile:
                 options.profilePath = mochitest.localProfile
                 os.system("rm -Rf %s" % options.profilePath)
                 options.profilePath = None
                 mochitest.localProfile = options.profilePath
 
             options.app = "am"
-            options.browserArgs = [
-                "instrument",
-                "-w",
-                "-e",
-                "deviceroot",
-                deviceRoot,
-                "-e",
-                "class"]
-            options.browserArgs.append(
-                "org.mozilla.gecko.tests.%s" %
-                test['name'].split('.java')[0])
-            options.browserArgs.append(
-                "org.mozilla.roboexample.test/org.mozilla.gecko.FennecInstrumentationTestRunner")
             mochitest.nsprLogName = "nspr-%s.log" % test['name']
+            if options.autorun:
+                # This launches a test (using "am instrument") and instructs
+                # Fennec to /quit/ the browser (using Robocop:Quit) and to
+                # /finish/ all opened activities.
+                options.browserArgs = [
+                    "instrument",
+                    "-w",
+                    "-e", "quit_and_finish", "1",
+                    "-e", "deviceroot", deviceRoot,
+                    "-e",
+                    "class"]
+                options.browserArgs.append(
+                    "org.mozilla.gecko.tests.%s" %
+                    test['name'].split('.java')[0])
+                options.browserArgs.append(
+                    "org.mozilla.roboexample.test/org.mozilla.gecko.FennecInstrumentationTestRunner")
+            else:
+                # This does not launch a test at all. It launches an activity
+                # that starts Fennec and then waits indefinitely, since cat
+                # never returns.
+                options.browserArgs = ["start",
+                                       "-n", "org.mozilla.roboexample.test/org.mozilla.gecko.LaunchFennecWithConfigurationActivity",
+                                       "&&", "cat"]
+                dm.default_timeout = sys.maxint # Forever.
+
+                mochitest.log.info("")
+                mochitest.log.info("Serving mochi.test Robocop root at http://%s:%s/tests/robocop/" %
+                    (options.remoteWebServer, options.httpPort))
+                mochitest.log.info("")
 
             # If the test is for checking the import from bookmarks then make
             # sure there is data to import
             if test['name'] == "testImportFromAndroid":
 
                 # Get the OS so we can run the insert in the apropriate
                 # database and following the correct table schema
                 osInfo = dm.getInfo("os")
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -2753,46 +2753,53 @@ PlacesCreateFolderTransaction.prototype 
  *        array of annotations to set for the new bookmark
  * @param [optional] aChildTransactions
  *        child transactions to commit after creating the bookmark. Prefer
  *        using any of the arguments above if possible. In general, a child
  *        transations should be used only if the change it does has to be
  *        reverted manually when removing the bookmark item.
  *        a child transaction must support setting its bookmark-item
  *        identifier via an "id" js setter.
+ * @param [optional] aPostData
+ *        keyword's POST data, if available.
  *
  * @return nsITransaction object
  */
 this.PlacesCreateBookmarkTransaction =
  function PlacesCreateBookmarkTransaction(aURI, aParentId, aIndex, aTitle,
                                           aKeyword, aAnnotations,
-                                          aChildTransactions)
+                                          aChildTransactions, aPostData)
 {
   this.item = new TransactionItemCache();
   this.item.uri = aURI;
   this.item.parentId = aParentId;
   this.item.index = aIndex;
   this.item.title = aTitle;
   this.item.keyword = aKeyword;
+  this.item.postData = aPostData;
   this.item.annotations = aAnnotations;
   this.childTransactions = aChildTransactions;
 }
 
 PlacesCreateBookmarkTransaction.prototype = {
   __proto__: BaseTransaction.prototype,
 
   doTransaction: function CITXN_doTransaction()
   {
     this.item.id = PlacesUtils.bookmarks.insertBookmark(this.item.parentId,
                                                         this.item.uri,
                                                         this.item.index,
                                                         this.item.title);
     if (this.item.keyword) {
       PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id,
                                                   this.item.keyword);
+      if (this.item.postData) {
+        PlacesUtils.setPostDataForBookmark(this.item.id,
+                                           this.item.postData);
+      }
     }
     if (this.item.annotations && this.item.annotations.length > 0)
       PlacesUtils.setAnnotationsForItem(this.item.id, this.item.annotations);
  
     if (this.childTransactions && this.childTransactions.length > 0) {
       // Set the new item id into child transactions.
       for (let i = 0; i < this.childTransactions.length; ++i) {
         this.childTransactions[i].item.id = this.item.id;
@@ -3069,16 +3076,18 @@ this.PlacesRemoveItemTransaction =
     // Remove this folder itself.
     let txn = PlacesUtils.bookmarks.getRemoveFolderTransaction(this.item.id);
     this.childTransactions.push(txn);
   }
   else if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_BOOKMARK) {
     this.item.uri = PlacesUtils.bookmarks.getBookmarkURI(this.item.id);
     this.item.keyword =
       PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
+    if (this.item.keyword)
+      this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id);
   }
 
   if (this.item.itemType != Ci.nsINavBookmarksService.TYPE_SEPARATOR)
     this.item.title = PlacesUtils.bookmarks.getItemTitle(this.item.id);
 
   this.item.parentId = PlacesUtils.bookmarks.getFolderIdForItem(this.item.id);
   this.item.annotations = PlacesUtils.getAnnotationsForItem(this.item.id);
   this.item.dateAdded = PlacesUtils.bookmarks.getItemDateAdded(this.item.id);
@@ -3120,16 +3129,19 @@ PlacesRemoveItemTransaction.prototype = 
                                                           this.item.uri,
                                                           this.item.index,
                                                           this.item.title);
       if (this.item.tags && this.item.tags.length > 0)
         PlacesUtils.tagging.tagURI(this.item.uri, this.item.tags);
       if (this.item.keyword) {
         PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id,
                                                     this.item.keyword);
+        if (this.item.postData) {
+          PlacesUtils.bookmarks.setPostDataForBookmark(this.item.id);
+        }
       }
     }
     else if (this.item.itemType == Ci.nsINavBookmarksService.TYPE_FOLDER) {
       let txn = new PlacesAggregatedTransaction("Remove item childTxn",
                                                 this.childTransactions);
       txn.undoTransaction();
     }
     else { // TYPE_SEPARATOR
@@ -3368,40 +3380,52 @@ PlacesSetPageAnnotationTransaction.proto
 
 /**
  * Transaction for editing a bookmark's keyword.
  *
  * @param aItemId
  *        id of the bookmark to edit
  * @param aNewKeyword
  *        new keyword for the bookmark
+ * @param aNewPostData [optional]
+ *        new keyword's POST data, if available
  *
  * @return nsITransaction object
  */
 this.PlacesEditBookmarkKeywordTransaction =
- function PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword)
+ function PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword, aNewPostData)
 {
   this.item = new TransactionItemCache();
   this.item.id = aItemId;
   this.new = new TransactionItemCache();
   this.new.keyword = aNewKeyword;
+  this.new.postData = aNewPostData
 }
 
 PlacesEditBookmarkKeywordTransaction.prototype = {
   __proto__: BaseTransaction.prototype,
 
   doTransaction: function EBKTXN_doTransaction()
   {
+    // Store the current values.
     this.item.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
+    if (this.item.keyword)
+      this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id);
+
+    // Update the keyword.
     PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.new.keyword);
+    if (this.new.keyword && this.new.postData)
+      PlacesUtils.setPostDataForBookmark(this.item.id, this.new.postData);
   },
 
   undoTransaction: function EBKTXN_undoTransaction()
   {
     PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.item.keyword);
+    if (this.item.postData)
+      PlacesUtils.setPostDataForBookmark(this.item.id, this.item.postData);
   }
 };
 
 
 /**
  * Transaction for editing the post data associated with a bookmark.
  *
  * @param aItemId
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -153,17 +153,22 @@ const SQL_ADAPTIVE_QUERY =
                             :matchBehavior, :searchBehavior)
    ORDER BY rank DESC, h.frecency DESC`;
 
 
 function hostQuery(conditions = "") {
   let query =
     `/* do not warn (bug NA): not worth to index on (typed, frecency) */
      SELECT :query_type, host || '/', IFNULL(prefix, '') || host || '/',
-            NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency
+            ( SELECT f.url FROM moz_favicons f
+              JOIN moz_places h ON h.favicon_id = f.id
+              WHERE rev_host = get_unreversed_host(host || '.') || '.'
+                 OR rev_host = get_unreversed_host(host || '.') || '.www.'
+            ) AS favicon_url,
+            NULL, NULL, NULL, NULL, NULL, NULL, NULL, frecency
      FROM moz_hosts
      WHERE host BETWEEN :searchString AND :searchString || X'FFFF'
      AND frecency <> 0
      ${conditions}
      ORDER BY frecency DESC
      LIMIT 1`;
   return query;
 }
@@ -171,18 +176,22 @@ function hostQuery(conditions = "") {
 const SQL_HOST_QUERY = hostQuery();
 
 const SQL_TYPED_HOST_QUERY = hostQuery("AND typed = 1");
 
 function bookmarkedHostQuery(conditions = "") {
   let query =
     `/* do not warn (bug NA): not worth to index on (typed, frecency) */
      SELECT :query_type, host || '/', IFNULL(prefix, '') || host || '/',
-            NULL, (
-              SELECT foreign_count > 0 FROM moz_places
+            ( SELECT f.url FROM moz_favicons f
+              JOIN moz_places h ON h.favicon_id = f.id
+              WHERE rev_host = get_unreversed_host(host || '.') || '.'
+                 OR rev_host = get_unreversed_host(host || '.') || '.www.'
+            ) AS favicon_url,
+            ( SELECT foreign_count > 0 FROM moz_places
               WHERE rev_host = get_unreversed_host(host || '.') || '.'
                  OR rev_host = get_unreversed_host(host || '.') || '.www.'
             ) AS bookmarked, NULL, NULL, NULL, NULL, NULL, NULL, frecency
      FROM moz_hosts
      WHERE host BETWEEN :searchString AND :searchString || X'FFFF'
      AND bookmarked
      AND frecency <> 0
      ${conditions}
@@ -193,19 +202,21 @@ function bookmarkedHostQuery(conditions 
 
 const SQL_BOOKMARKED_HOST_QUERY = bookmarkedHostQuery();
 
 const SQL_BOOKMARKED_TYPED_HOST_QUERY = bookmarkedHostQuery("AND typed = 1");
 
 function urlQuery(conditions = "") {
   let query =
     `/* do not warn (bug no): cannot use an index */
-     SELECT :query_type, h.url, NULL,
-            NULL, foreign_count > 0 AS bookmarked, NULL, NULL, NULL, NULL, NULL, NULL, h.frecency
+     SELECT :query_type, h.url, NULL, f.url AS favicon_url,
+            foreign_count > 0 AS bookmarked,
+            NULL, NULL, NULL, NULL, NULL, NULL, h.frecency
      FROM moz_places h
+     LEFT JOIN moz_favicons f ON h.favicon_id = f.id
      WHERE h.frecency <> 0
      ${conditions}
      AND AUTOCOMPLETE_MATCH(:searchString, h.url,
      h.title, '',
      h.visit_count, h.typed, bookmarked, 0,
      :matchBehavior, :searchBehavior)
      ORDER BY h.frecency DESC, h.id DESC
      LIMIT 1`;
@@ -1171,49 +1182,43 @@ Search.prototype = {
     }
   },
 
   _processHostRow: function (row) {
     let match = {};
     let trimmedHost = row.getResultByIndex(QUERYINDEX_URL);
     let untrimmedHost = row.getResultByIndex(QUERYINDEX_TITLE);
     let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+    let faviconUrl = row.getResultByIndex(QUERYINDEX_ICONURL);
 
     // If the untrimmed value doesn't preserve the user's input just
     // ignore it and complete to the found host.
     if (untrimmedHost &&
         !untrimmedHost.toLowerCase().includes(this._trimmedOriginalSearchString.toLowerCase())) {
       untrimmedHost = null;
     }
 
     match.value = this._strippedPrefix + trimmedHost;
     // Remove the trailing slash.
     match.comment = stripHttpAndTrim(trimmedHost);
     match.finalCompleteValue = untrimmedHost;
-
-    try {
-      let iconURI = NetUtil.newURI(untrimmedHost);
-      iconURI.path = "/favicon.ico";
-      match.icon = PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
-    } catch (e) {
-      // This can fail, which is ok.
-    }
-
+    match.icon = faviconUrl;
     // Although this has a frecency, this query is executed before any other
     // queries that would result in frecency matches.
     match.frecency = frecency;
     match.style = "autofill";
     return match;
   },
 
   _processUrlRow: function (row) {
     let match = {};
     let value = row.getResultByIndex(QUERYINDEX_URL);
     let url = fixupSearchText(value);
     let frecency = row.getResultByIndex(QUERYINDEX_FRECENCY);
+    let faviconUrl = row.getResultByIndex(QUERYINDEX_ICONURL);
 
     let prefix = value.slice(0, value.length - stripPrefix(value).length);
 
     // We must complete the URL up to the next separator (which is /, ? or #).
     let separatorIndex = url.slice(this._searchString.length)
                             .search(/[\/\?\#]/);
     if (separatorIndex != -1) {
       separatorIndex += this._searchString.length;
@@ -1229,16 +1234,17 @@ Search.prototype = {
     if (untrimmedURL &&
         !untrimmedURL.toLowerCase().includes(this._trimmedOriginalSearchString.toLowerCase())) {
       untrimmedURL = null;
      }
 
     match.value = this._strippedPrefix + url;
     match.comment = url;
     match.finalCompleteValue = untrimmedURL;
+    match.icon = faviconUrl;
     // Although this has a frecency, this query is executed before any other
     // queries that would result in frecency matches.
     match.frecency = frecency;
     match.style = "autofill";
     return match;
   },
 
   _processRow: function (row) {
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -94,39 +94,43 @@ AutoCompleteInput.prototype = {
   onTextEntered: function() false,
   onTextReverted: function() false,
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput])
 }
 
 // A helper for check_autocomplete to check a specific match against data from
 // the controller.
-function _check_autocomplete_matches(match, controllerInfo) {
+function _check_autocomplete_matches(match, result) {
   let { uri, title, tags, searchEngine, style } = match;
-  let { controllerValue, controllerComment, controllerStyle } = controllerInfo;
   if (tags)
     title += " \u2013 " + tags.sort().join(", ");
   if (searchEngine)
     title += TITLE_SEARCH_ENGINE_SEPARATOR + searchEngine;
   if (style)
     style = style.sort();
   else
     style = ["favicon"];
 
   do_print("Checking against expected '" + uri.spec + "', '" + title + "'...");
   // Got a match on both uri and title?
-  if (stripPrefix(uri.spec) != stripPrefix(controllerValue) || title != controllerComment) {
+  if (stripPrefix(uri.spec) != stripPrefix(result.value) || title != result.comment) {
     return false;
   }
-  let actualStyle = controllerStyle.split(/\s+/).sort();
+
+  let actualStyle = result.style.split(/\s+/).sort();
   if (style)
     Assert.equal(actualStyle.toString(), style.toString(), "Match should have expected style");
   if (uri.spec.startsWith("moz-action:")) {
     Assert.ok(actualStyle.indexOf("action") != -1, "moz-action results should always have 'action' in their style");
   }
+
+  if (match.icon)
+    Assert.equal(result.image, match.icon, "Match should have expected image");
+
   return true;
 }
 
 function* check_autocomplete(test) {
   // At this point frecency could still be updating due to latest pages
   // updates.
   // This is not a problem in real life, but autocomplete tests should
   // return reliable resultsets, thus we have to wait.
@@ -179,47 +183,51 @@ function* check_autocomplete(test) {
   if (test.matches) {
     // Do not modify the test original matches.
     let matches = test.matches.slice();
 
     let firstIndexToCheck = 0;
     if (test.searchParam && test.searchParam == "enable-actions") {
       firstIndexToCheck = 1;
       do_print("Checking first match is first autocomplete entry")
-      let controllerValue = controller.getValueAt(0);
-      let controllerComment = controller.getCommentAt(0);
-      let controllerStyle = controller.getStyleAt(0);
-      do_print("First match is '" + controllerValue + "', '" + controllerComment + "");
-      let controllerInfo = { controllerValue, controllerComment, controllerStyle };
-      Assert.ok(_check_autocomplete_matches(matches[0], controllerInfo), "first item is correct");
+      let result = {
+        value: controller.getValueAt(0),
+        comment: controller.getCommentAt(0),
+        style: controller.getStyleAt(0),
+        image: controller.getImageAt(0),
+      }
+      do_print(`First match is "${result.value}", " ${result.comment}"`);
+      Assert.ok(_check_autocomplete_matches(matches[0], result), "first item is correct");
       do_print("Checking rest of the matches");
     }
 
     for (let i = firstIndexToCheck; i < controller.matchCount; i++) {
-      let controllerValue = controller.getValueAt(i);
-      let controllerComment = controller.getCommentAt(i);
-      let controllerStyle = controller.getStyleAt(i);
-      let controllerInfo = { controllerValue, controllerComment, controllerStyle };
-      do_print("Looking for '" + controllerValue + "', '" + controllerComment + "' in expected results...");
+      let result = {
+        value: controller.getValueAt(i),
+        comment: controller.getCommentAt(i),
+        style: controller.getStyleAt(i),
+        image: controller.getImageAt(i),
+      }
+      do_print(`Looking for "${result.value}", "${result.comment}" in expected results...`);
       let j;
       for (j = firstIndexToCheck; j < matches.length; j++) {
         // Skip processed expected results
         if (matches[j] == undefined)
           continue;
-        if (_check_autocomplete_matches(matches[j], controllerInfo)) {
+        if (_check_autocomplete_matches(matches[j], result)) {
           do_print("Got a match at index " + j + "!");
           // Make it undefined so we don't process it again
           matches[j] = undefined;
           break;
         }
       }
 
       // We didn't hit the break, so we must have not found it
       if (j == matches.length)
-        do_throw("Didn't find the current result ('" + controllerValue + "', '" + controllerComment + "') in matches");
+        do_throw(`Didn't find the current result ("${result.value}", "${result.comment}") in matches`);
     }
 
     Assert.equal(controller.matchCount, matches.length,
                  "Got as many results as expected");
 
     // If we expect results, make sure we got matches.
     do_check_eq(controller.searchStatus, matches.length ?
                 Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH :
@@ -368,16 +376,28 @@ function makeVisitMatch(input, url, extr
 function makeSwitchToTabMatch(url, extra = {}) {
   return {
     uri: makeActionURI("switchtab", {url}),
     title: extra.title || url,
     style: [ "action", "switchtab" ],
   }
 }
 
+function setFaviconForHref(href, iconHref) {
+  return new Promise(resolve => {
+    PlacesUtils.favicons.setAndFetchFaviconForPage(
+      NetUtil.newURI(href),
+      NetUtil.newURI(iconHref),
+      true,
+      PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+      resolve
+    );
+  });
+}
+
 // Ensure we have a default search engine and the keyword.enabled preference
 // set.
 add_task(function ensure_search_engine() {
   // keyword.enabled is necessary for the tests to see keyword searches.
   Services.prefs.setBoolPref("keyword.enabled", true);
 
   // Remove any existing engines before adding ours.
   for (let engine of Services.search.getEngines()) {
--- a/toolkit/components/places/tests/unifiedcomplete/test_autoFill_default_behavior.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_autoFill_default_behavior.js
@@ -17,33 +17,37 @@ add_task(function* test_default_behavior
     { uri: uri1, title: "typed", transition: TRANSITION_TYPED },
     { uri: uri2, title: "visited" },
     { uri: uri4, title: "tpbk", transition: TRANSITION_TYPED },
   ]);
   yield addBookmark( { uri: uri3, title: "bookmarked" } );
   yield addBookmark( { uri: uri4, title: "tpbk" } );
   yield addBookmark( { uri: uri5, title: "title", tags: ["foo"] } );
 
+  yield setFaviconForHref(uri1.spec, "chrome://global/skin/icons/information-16.png");
+  yield setFaviconForHref(uri3.spec, "chrome://global/skin/icons/error-16.png");
+
   // RESTRICT TO HISTORY.
   Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
   Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false);
   Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
 
   do_print("Restrict history, common visit, should not autoFill");
   yield check_autocomplete({
     search: "vi",
     matches: [ { uri: uri2, title: "visited" } ],
     autofilled: "vi",
     completed: "vi"
   });
 
   do_print("Restrict history, typed visit, should autoFill");
   yield check_autocomplete({
     search: "ty",
-    matches: [ { uri: uri1, title: "typed", style: [ "autofill" ] } ],
+    matches: [ { uri: uri1, title: "typed", style: [ "autofill" ],
+                 icon: "chrome://global/skin/icons/information-16.png" } ],
     autofilled: "typed/",
     completed: "typed/"
   });
 
   // Don't autoFill this one cause it's not typed.
   do_print("Restrict history, bookmark, should not autoFill");
   yield check_autocomplete({
     search: "bo",
@@ -64,17 +68,18 @@ add_task(function* test_default_behavior
   Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
 
   // We are not restricting on typed, so we autoFill the bookmark even if we
   // are restricted to history.  We accept that cause not doing that
   // would be a perf hit and the privacy implications are very weak.
   do_print("Restrict history, bookmark, autoFill.typed = false, should autoFill");
   yield check_autocomplete({
     search: "bo",
-    matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ], style: [ "autofill" ] } ],
+    matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ], style: [ "autofill" ],
+                 icon: "chrome://global/skin/icons/error-16.png" } ],
     autofilled: "bookmarked/",
     completed: "bookmarked/"
   });
 
   do_print("Restrict history, common visit, autoFill.typed = false, should autoFill");
   yield check_autocomplete({
     search: "vi",
     matches: [ { uri: uri2, title: "visited", style: [ "autofill" ] } ],
@@ -93,17 +98,18 @@ add_task(function* test_default_behavior
     matches: [ ],
     autofilled: "vi",
     completed: "vi"
   });
 
   do_print("Restrict typed, typed visit, autofill.typed = false, should autoFill");
   yield check_autocomplete({
     search: "ty",
-    matches: [ { uri: uri1, title: "typed", style: [ "autofill" ] } ],
+    matches: [ { uri: uri1, title: "typed", style: [ "autofill" ],
+                 icon: "chrome://global/skin/icons/information-16.png"} ],
     autofilled: "typed/",
     completed: "typed/"
   });
 
   do_print("Restrict typed, bookmark, autofill.typed = false, should not autoFill");
   yield check_autocomplete({
     search: "bo",
     matches: [ ],
@@ -139,17 +145,18 @@ add_task(function* test_default_behavior
     autofilled: "ty",
     completed: "ty"
   });
 
   // Don't autoFill this one cause it's not typed.
   do_print("Restrict bookmarks, bookmark, should not autoFill");
   yield check_autocomplete({
     search: "bo",
-    matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ] } ],
+    matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ],
+                 icon: "chrome://global/skin/icons/error-16.png"} ],
     autofilled: "bo",
     completed: "bo"
   });
 
   // Note we don't show this one cause it's not typed.
   do_print("Restrict bookmarks, typed bookmark, should autoFill");
   yield check_autocomplete({
     search: "tp",
@@ -158,17 +165,18 @@ add_task(function* test_default_behavior
     completed: "tpbk/"
   });
 
   Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
 
   do_print("Restrict bookmarks, bookmark, autofill.typed = false, should autoFill");
   yield check_autocomplete({
     search: "bo",
-    matches: [ { uri: uri3, title: "bookmarked", style: [ "autofill" ] } ],
+    matches: [ { uri: uri3, title: "bookmarked", style: [ "autofill" ],
+                 icon: "chrome://global/skin/icons/error-16.png" } ],
     autofilled: "bookmarked/",
     completed: "bookmarked/"
   });
 
   // Don't autofill because it's a title.
   do_print("Restrict bookmarks, title, autofill.typed = false, should not autoFill");
   yield check_autocomplete({
     search: "# ta",
@@ -198,16 +206,19 @@ add_task(function* test_default_behavior
   yield PlacesTestUtils.addVisits([
     { uri: uri1, title: "typed", transition: TRANSITION_TYPED },
     { uri: uri2, title: "visited" },
     { uri: uri4, title: "tpbk", transition: TRANSITION_TYPED },
   ]);
   yield addBookmark( { uri: uri3, title: "bookmarked" } );
   yield addBookmark( { uri: uri4, title: "tpbk" } );
 
+  yield setFaviconForHref(uri1.spec, "chrome://global/skin/icons/information-16.png");
+  yield setFaviconForHref(uri3.spec, "chrome://global/skin/icons/error-16.png");
+
   // RESTRICT TO HISTORY.
   Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
   Services.prefs.setBoolPref("browser.urlbar.suggest.history.onlyTyped", false);
   Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
   Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", true);
   Services.prefs.setBoolPref("browser.urlbar.autoFill.searchEngines", false);
 
   do_print("URL: Restrict history, common visit, should not autoFill");
@@ -216,17 +227,18 @@ add_task(function* test_default_behavior
     matches: [ { uri: uri2, title: "visited" } ],
     autofilled: "visited/v",
     completed: "visited/v"
   });
 
   do_print("URL: Restrict history, typed visit, should autoFill");
   yield check_autocomplete({
     search: "typed/t",
-    matches: [ { uri: uri1, title: "typed/ty/", style: [ "autofill" ] } ],
+    matches: [ { uri: uri1, title: "typed/ty/", style: [ "autofill" ],
+                 icon: "chrome://global/skin/icons/information-16.png"} ],
     autofilled: "typed/ty/",
     completed: "http://typed/ty/"
   });
 
   // Don't autoFill this one cause it's not typed.
   do_print("URL: Restrict history, bookmark, should not autoFill");
   yield check_autocomplete({
     search: "bookmarked/b",
@@ -263,17 +275,18 @@ add_task(function* test_default_behavior
     autofilled: "typed/t",
     completed: "typed/t"
   });
 
   // Don't autoFill this one cause it's not typed.
   do_print("URL: Restrict bookmarks, bookmark, should not autoFill");
   yield check_autocomplete({
     search: "bookmarked/b",
-    matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ] } ],
+    matches: [ { uri: uri3, title: "bookmarked", style: [ "bookmark" ],
+                 icon: "chrome://global/skin/icons/error-16.png" } ],
     autofilled: "bookmarked/b",
     completed: "bookmarked/b"
   });
 
   // Note we don't show this one cause it's not typed.
   do_print("URL: Restrict bookmarks, typed bookmark, should autoFill");
   yield check_autocomplete({
     search: "tpbk/t",
@@ -282,15 +295,16 @@ add_task(function* test_default_behavior
     completed: "http://tpbk/tp/"
   });
 
   Services.prefs.setBoolPref("browser.urlbar.autoFill.typed", false);
 
   do_print("URL: Restrict bookmarks, bookmark, autofill.typed = false, should autoFill");
   yield check_autocomplete({
     search: "bookmarked/b",
-    matches: [ { uri: uri3, title: "bookmarked/bo/", style: [ "autofill" ] } ],
+    matches: [ { uri: uri3, title: "bookmarked/bo/", style: [ "autofill" ],
+                 icon: "chrome://global/skin/icons/error-16.png" } ],
     autofilled: "bookmarked/bo/",
     completed: "http://bookmarked/bo/"
   });
 
   yield cleanup();
 });
--- a/toolkit/components/places/tests/unit/test_placesTxn.js
+++ b/toolkit/components/places/tests/unit/test_placesTxn.js
@@ -535,27 +535,29 @@ add_task(function* test_edit_description
 });
 
 add_task(function* test_edit_keyword() {
   const KEYWORD = "keyword-test_edit_keyword";
 
   let testURI = NetUtil.newURI("http://test_edit_keyword.com");
   let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit keyword");
 
-  let txn = new PlacesEditBookmarkKeywordTransaction(testBkmId, KEYWORD);
+  let txn = new PlacesEditBookmarkKeywordTransaction(testBkmId, KEYWORD, "postData");
 
   txn.doTransaction();
   do_check_eq(observer._itemChangedId, testBkmId);
   do_check_eq(observer._itemChangedProperty, "keyword");
   do_check_eq(observer._itemChangedValue, KEYWORD);
+  do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), "postData");
 
   txn.undoTransaction();
   do_check_eq(observer._itemChangedId, testBkmId);
   do_check_eq(observer._itemChangedProperty, "keyword");
   do_check_eq(observer._itemChangedValue, "");
+  do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), null);
 });
 
 add_task(function* test_LoadInSidebar_transaction() {
   let testURI = NetUtil.newURI("http://test_LoadInSidebar_transaction.com");
   let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test LoadInSidebar transaction");
 
   const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
   let anno = { name: LOAD_IN_SIDEBAR_ANNO,
@@ -695,44 +697,16 @@ add_task(function* test_sort_folder_by_n
   do_check_eq(0, bmsvc.getItemIndex(b3));
 
   txn.undoTransaction();
   do_check_eq(0, bmsvc.getItemIndex(b1));
   do_check_eq(1, bmsvc.getItemIndex(b2));
   do_check_eq(2, bmsvc.getItemIndex(b3));
 });
 
-add_task(function* test_edit_postData() {
-  let postData = "post-test_edit_postData";
-  let testURI = NetUtil.newURI("http://test_edit_postData.com");
-
-  let testBkm = yield PlacesUtils.bookmarks.insert({
-    parentGuid: PlacesUtils.bookmarks.menuGuid,
-    url: "http://test_edit_postData.com",
-    title: "Test edit Post Data"
-  });
-
-  yield PlacesUtils.keywords.insert({
-    keyword: "kw",
-    url: "http://test_edit_postData.com"
-  });
-
-  let testBkmId = yield PlacesUtils.promiseItemId(testBkm.guid);
-  let txn = new PlacesEditBookmarkPostDataTransaction(testBkmId, postData);
-
-  txn.doTransaction();
-  yield promiseKeyword("kw", testURI.spec, postData);
-
-  txn.undoTransaction();
-  entry = yield PlacesUtils.keywords.fetch("kw");
-  Assert.equal(entry.url.href, testURI.spec);
-  // We don't allow anymore to set a null post data.
-  //Assert.equal(null, post_data);
-});
-
 add_task(function* test_tagURI_untagURI() {
   const TAG_1 = "tag-test_tagURI_untagURI-bar";
   const TAG_2 = "tag-test_tagURI_untagURI-foo";
   let tagURI = NetUtil.newURI("http://test_tagURI_untagURI.com");
 
   // Test tagURI
   let tagTxn = new PlacesTagURITransaction(tagURI, [TAG_1, TAG_2]);
 
--- a/toolkit/mozapps/update/tests/chrome/chrome.ini
+++ b/toolkit/mozapps/update/tests/chrome/chrome.ini
@@ -13,18 +13,18 @@ support-files =
 ; order and not in the order specified in the manifest.
 [test_0011_check_basic.xul]
 [test_0012_check_basic_license.xul]
 [test_0013_check_incompat_basic.xul]
 [test_0014_check_incompat_basic_license.xul]
 [test_0015_check_incompat_basic_addons.xul]
 [test_0016_check_incompat_basic_license_addons.xul]
 [test_0017_check_staging_basic.xul]
-skip-if = os != 'win'
-reason = Bug 918029 and bug 1164560 - timeout caused by copying too many files.
+skip-if = asan
+reason = Bug 1168003
 [test_0021_check_billboard.xul]
 [test_0022_check_billboard_license.xul]
 [test_0023_check_incompat_billboard.xul]
 [test_0024_check_incompat_billboard_license.xul]
 [test_0025_check_incompat_billboard_addons.xul]
 [test_0026_check_incompat_billboard_license_addons.xul]
 [test_0031_available_basic.xul]
 [test_0032_available_basic_license.xul]
@@ -55,21 +55,25 @@ reason = Bug 918029 and bug 1164560 - ti
 [test_0083_error_patchApplyFailure_partial_complete.xul]
 [test_0084_error_patchApplyFailure_verify_failed.xul]
 [test_0091_installed.xul]
 [test_0092_finishedBackground.xul]
 [test_0093_restartNotification.xul]
 [test_0094_restartNotification_remote.xul]
 [test_0095_restartNotification_remoteInvalidNumber.xul]
 [test_0096_restartNotification_stagedBackground.xul]
+skip-if = asan
+reason = Bug 1168003
 [test_0097_restartNotification_stagedServiceBackground.xul]
+skip-if = os != 'win'
+reason = only Windows has the maintenance service.
 [test_0101_background_restartNotification.xul]
 [test_0102_background_restartNotification_staging.xul]
-skip-if = os == 'linux'
-reason = Bug 918029 - timeout caused by copying too many files.
+skip-if = asan
+reason = Bug 1168003
 [test_0103_background_restartNotification_stagingService.xul]
 skip-if = os != 'win'
 reason = only Windows has the maintenance service.
 [test_0104_background_restartNotification_NoIncompatAddons.xul]
 [test_0105_background_restartNotification_VersionCompatAddons.xul]
 [test_0111_neverButton_basic.xul]
 [test_0112_neverButton_billboard.xul]
 [test_0113_showNeverForVersionRemovedWithPref.xul]
--- a/toolkit/mozapps/update/tests/chrome/test_0017_check_staging_basic.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_0017_check_staging_basic.xul
@@ -26,16 +26,18 @@ const TESTS = [ {
   buttonClick: "next"
 }, {
   pageid: PAGEID_DOWNLOADING
 }, {
   pageid: PAGEID_FINISHED,
   buttonClick: "extra1"
 } ];
 
+gUseTestUpdater = true;
+
 function runTest() {
   debugDump("entering");
 
   Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true);
 
   let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams();
   setUpdateURLOverride(url);
 
--- a/toolkit/mozapps/update/tests/chrome/test_0096_restartNotification_stagedBackground.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_0096_restartNotification_stagedBackground.xul
@@ -19,16 +19,18 @@
 <script type="application/javascript">
 <![CDATA[
 
 const TESTS = [ {
   pageid: PAGEID_FINISHED_BKGRD,
   buttonClick: "extra1"
 } ];
 
+gUseTestUpdater = true;
+
 function runTest() {
   debugDump("entering");
 
   let patches = getLocalPatchString("complete", null, null, null, null, null,
                                     STATE_APPLIED);
   let updates = getLocalUpdateString(patches, null, null, null,
                                      Services.appinfo.version,
                                      Services.appinfo.platformVersion);
--- a/toolkit/mozapps/update/tests/chrome/test_0097_restartNotification_stagedServiceBackground.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_0097_restartNotification_stagedServiceBackground.xul
@@ -19,16 +19,18 @@
 <script type="application/javascript">
 <![CDATA[
 
 const TESTS = [ {
   pageid: PAGEID_FINISHED_BKGRD,
   buttonClick: "extra1"
 } ];
 
+gUseTestUpdater = true;
+
 function runTest() {
   debugDump("entering");
 
   let patches = getLocalPatchString("complete", null, null, null, null, null,
                                     STATE_APPLIED_SVC);
   let updates = getLocalUpdateString(patches, null, null, null,
                                      Services.appinfo.version,
                                      Services.appinfo.platformVersion);
--- a/toolkit/mozapps/update/tests/chrome/test_0102_background_restartNotification_staging.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_0102_background_restartNotification_staging.xul
@@ -19,16 +19,18 @@
 <script type="application/javascript">
 <![CDATA[
 
 const TESTS = [ {
   pageid: PAGEID_FINISHED_BKGRD,
   buttonClick: "extra1"
 } ];
 
+gUseTestUpdater = true;
+
 function runTest() {
   debugDump("entering");
 
   Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true);
   Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1);
 
   let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams();
   setUpdateURLOverride(url);
--- a/toolkit/mozapps/update/tests/chrome/test_0103_background_restartNotification_stagingService.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_0103_background_restartNotification_stagingService.xul
@@ -19,16 +19,18 @@
 <script type="application/javascript">
 <![CDATA[
 
 const TESTS = [ {
   pageid: PAGEID_FINISHED_BKGRD,
   buttonClick: "extra1"
 } ];
 
+gUseTestUpdater = true;
+
 function runTest() {
   debugDump("entering");
 
   Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true);
   Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, true);
   Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1);
 
   let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams();
--- a/toolkit/mozapps/update/tests/chrome/test_9999_cleanup.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_9999_cleanup.xul
@@ -76,17 +76,39 @@ function runTest() {
   try {
     removeDirRecursive(addonPrepDir);
   }
   catch (e) {
     logTestInfo("Unable to remove directory. Path: " + addonPrepDir.path +
                 ", Exception: " + e);
   }
 
-  resetAddons(finishTest);
+  resetAddons(cleanupRestoreUpdaterBackup);
+}
+
+/**
+ * After all tests finish this will repeatedly attempt to restore the real
+ * updater if it exists and then call finishTest after the restore is
+ * successful.
+ */
+function cleanupRestoreUpdaterBackup() {
+  debugDump("entering");
+
+  try {
+    // Windows debug builds keep the updater file in use for a short period of
+    // time after the updater process exits.
+    restoreUpdaterBackup();
+  } catch (e) {
+    logTestInfo("Attempt to restore the backed up updater failed... " +
+                "will try again, Exception: " + e);
+    SimpleTest.executeSoon(cleanupRestoreUpdaterBackup);
+    return;
+  }
+
+  SimpleTest.executeSoon(finishTest);
 }
 
 function finishTest() {
   debugDump("entering");
 
   if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_LOG)) {
     Services.prefs.clearUserPref(PREF_APP_UPDATE_LOG);
   }
--- a/toolkit/mozapps/update/tests/chrome/utils.js
+++ b/toolkit/mozapps/update/tests/chrome/utils.js
@@ -211,16 +211,17 @@ var gExtUpdateURL;                // ext
 
 var gTestCounter = -1;
 var gWin;
 var gDocElem;
 var gPrefToCheck;
 var gDisableNoUpdateAddon = false;
 var gDisableUpdateCompatibilityAddon = false;
 var gDisableUpdateVersionAddon = false;
+var gUseTestUpdater = false;
 
 // Set to true to log additional information for debugging. To log additional
 // information for an individual test set DEBUG_AUS_TEST to true in the test's
 // onload function.
 var DEBUG_AUS_TEST = false;
 
 const DATA_URI_SPEC = "chrome://mochitests/content/chrome/toolkit/mozapps/update/tests/data/";
 Services.scriptloader.loadSubScript(DATA_URI_SPEC + "shared.js", this);
@@ -325,19 +326,20 @@ function runTestDefaultWaitForWindowClos
     SimpleTest.executeSoon(runTestDefaultWaitForWindowClosed);
   } else {
     Services.ww.registerNotification(gWindowObserver);
 
     gCloseWindowTimeoutCounter = 0;
 
     setupFiles();
     setupPrefs();
+    gEnv.set("MOZ_TEST_SKIP_UPDATE_STAGE", "1");
     removeUpdateDirsAndFiles();
     reloadUpdateManagerData();
-    setupAddons(runTest);
+    setupAddons(setupTestUpdater);
   }
 }
 
 /**
  * Default test finish function that can be used by most tests. This function
  * uses protective measures to prevent the next test from failing provided by
  * |finishTestDefaultWaitForWindowClosed| helper functions to prevent failure
  * due to an update window being left open.
@@ -353,26 +355,27 @@ function finishTestDefault() {
     debugDump("channel = " + gChannel);
     gChannel = null;
     gPrefRoot.removeObserver(PREF_APP_UPDATE_CHANNEL, observer);
   }
 
   verifyTestsRan();
 
   resetPrefs();
+  gEnv.set("MOZ_TEST_SKIP_UPDATE_STAGE", "");
   resetFiles();
   removeUpdateDirsAndFiles();
   reloadUpdateManagerData();
 
   Services.ww.unregisterNotification(gWindowObserver);
   if (gDocElem) {
     gDocElem.removeEventListener("pageshow", onPageShowDefault, false);
   }
 
-  finishTestDefaultWaitForWindowClosed();
+  finishTestRestoreUpdaterBackup();
 }
 
 /**
  * nsITimerCallback for the timeout timer to cleanly finish a test if the Update
  * Window doesn't close for a test. This allows the next test to run properly if
  * a previous test fails.
  *
  * @param  aTimer
@@ -386,25 +389,48 @@ function finishTestTimeout(aTimer) {
     finishTest();
   }
   catch (e) {
     finishTestDefault();
   }
 }
 
 /**
+ * When a test finishes this will repeatedly attempt to restore the real updater
+ * for tests that use the test updater and then call
+ * finishTestDefaultWaitForWindowClosed after the restore is successful.
+ */
+function finishTestRestoreUpdaterBackup() {
+  if (gUseTestUpdater) {
+    try {
+      // Windows debug builds keep the updater file in use for a short period of
+      // time after the updater process exits.
+      restoreUpdaterBackup();
+    } catch (e) {
+      logTestInfo("Attempt to restore the backed up updater failed... " +
+                  "will try again, Exception: " + e);
+      SimpleTest.executeSoon(finishTestRestoreUpdaterBackup);
+      return;
+    }
+  }
+
+  finishTestDefaultWaitForWindowClosed();
+}
+
+/**
  * If an update window is found SimpleTest.executeSoon can callback before the
  * update window is fully closed especially with debug builds. If an update
  * window is found this function will call itself using SimpleTest.executeSoon
  * up to the amount declared in CLOSE_WINDOW_TIMEOUT_MAXCOUNT until the update
  * window has closed before finishing the test.
  */
 function finishTestDefaultWaitForWindowClosed() {
   gCloseWindowTimeoutCounter++;
   if (gCloseWindowTimeoutCounter > CLOSE_WINDOW_TIMEOUT_MAXCOUNT) {
+    SimpleTest.requestCompleteLog();
     SimpleTest.finish();
     return;
   }
 
   // The update window should not be open at this time. If it is the call to
   // |closeUpdateWindow| will close it and cause the test to fail.
   if (closeUpdateWindow()) {
     SimpleTest.executeSoon(finishTestDefaultWaitForWindowClosed);
@@ -747,17 +773,17 @@ function checkRadioGroupSelectedIndex() 
 /**
  * Checks that only incompatible add-ons (e.g. noupdate_X add-ons) that don't
  * have an update are listed in the add-ons incompatible list.
  */
 function checkIncompatbleList() {
   for (let i = 0; i < gIncompatibleListbox.itemCount; i++) {
     let label = gIncompatibleListbox.getItemAtIndex(i).label;
     // Use indexOf since locales can change the text displayed
-    ok(label.indexOf("noupdate") != -1, "Checking that only incompatible " + 
+    ok(label.indexOf("noupdate") != -1, "Checking that only incompatible " +
        "add-ons that don't have an update are listed in the incompatible list");
   }
 }
 
 /**
  * Compares the return value of prefHasUserValue for the preference specified in
  * gPrefToCheck with the value passed in the aPrefHasValue parameter or the
  * value specified in the current test's prefHasUserValue property if
@@ -871,74 +897,125 @@ function verifyTestsRan() {
     let msg = "Checking if TESTS[" + i + "] test was performed... " +
               "callback function name = " + gCallback.name + ", " +
               "pageid = " + test.pageid;
     ok(test.ranTest, msg);
   }
 }
 
 /**
- * Restore the updater that was backed up.  This is called both in setupFiles
- * and resetFiles.  It is called in setupFiles before the backup is done in
- * case the previous test failed.  It is called in resetFiles to put things
- * back to its original state.
- */
-function resetUpdaterBackup() {
-  let baseAppDir = getAppBaseDir();
-  let updater = baseAppDir.clone();
-  let updaterBackup = baseAppDir.clone();
-  updater.append(FILE_UPDATER_BIN);
-  updaterBackup.append(FILE_UPDATER_BIN_BAK);
-  if (updaterBackup.exists()) {
-    if (updater.exists()) {
-      updater.remove(true);
-    }
-    updaterBackup.moveTo(baseAppDir, FILE_UPDATER_BIN);
-  }
-}
-
-/**
  * Creates a backup of files the tests need to modify so they can be restored to
  * the original file when the test has finished and then modifies the files.
  */
 function setupFiles() {
   // Backup the updater-settings.ini file if it exists by moving it.
   let baseAppDir = getAppBaseDir();
   let updateSettingsIni = baseAppDir.clone();
   updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI);
   if (updateSettingsIni.exists()) {
     updateSettingsIni.moveTo(baseAppDir, FILE_UPDATE_SETTINGS_INI_BAK);
   }
   updateSettingsIni = baseAppDir.clone();
   updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI);
   writeFile(updateSettingsIni, UPDATE_SETTINGS_CONTENTS);
-
-  // Just in case the last test failed, try to reset.
-  resetUpdaterBackup();
+}
 
-  // Move away the real updater
-  let updater = baseAppDir.clone();
-  updater.append(FILE_UPDATER_BIN);
-  updater.moveTo(baseAppDir, FILE_UPDATER_BIN_BAK);
+/**
+ * For tests that use the test updater restores the backed up real updater if
+ * it exists and tries again on failure since Windows debug builds at times
+ * leave the file in use. After success moveRealUpdater is called to continue
+ * the setup of the test updater. For tests that don't use the test updater
+ * runTest will be called.
+ */
+function setupTestUpdater() {
+  if (!gUseTestUpdater) {
+    runTest();
+    return;
+  }
 
-  // Move in the test only updater
-  let testUpdaterDir = Cc["@mozilla.org/file/directory_service;1"].
-    getService(Ci.nsIProperties).
-    get("CurWorkD", Ci.nsILocalFile);
+  try {
+    restoreUpdaterBackup();
+  } catch (e) {
+    logTestInfo("Attempt to restore the backed up updater failed... " +
+                "will try again, Exception: " + e);
+    SimpleTest.executeSoon(setupTestUpdater);
+    return;
+  }
+  moveRealUpdater();
+}
 
-  let relPath = REL_PATH_DATA;
-  let pathParts = relPath.split("/");
-  for (let i = 0; i < pathParts.length; ++i) {
-    testUpdaterDir.append(pathParts[i]);
+/**
+ * Backs up the real updater and tries again on failure since Windows debug
+ * builds at times leave the file in use. After success it will call
+ * copyTestUpdater to continue the setup of the test updater.
+ */
+function moveRealUpdater() {
+  try {
+    // Move away the real updater
+    let baseAppDir = getAppBaseDir();
+    let updater = baseAppDir.clone();
+    updater.append(FILE_UPDATER_BIN);
+    updater.moveTo(baseAppDir, FILE_UPDATER_BIN_BAK);
+  } catch (e) {
+    logTestInfo("Attempt to move the real updater out of the way failed... " +
+                "will try again, Exception: " + e);
+    SimpleTest.executeSoon(moveRealUpdater);
+    return;
   }
 
-  let testUpdater = testUpdaterDir.clone();
-  testUpdater.append(FILE_UPDATER_BIN);
-  if (testUpdater.exists()) {
+  copyTestUpdater();
+}
+
+/**
+ * Copies the test updater so it can be used by tests and tries again on failure
+ * since Windows debug builds at times leave the file in use. After success it
+ * will call runTest to continue the test.
+ */
+function copyTestUpdater() {
+  try {
+    // Copy the test updater
+    let baseAppDir = getAppBaseDir();
+    let testUpdaterDir = Services.dirsvc.get("CurWorkD", Ci.nsILocalFile);
+    let relPath = REL_PATH_DATA;
+    let pathParts = relPath.split("/");
+    for (let i = 0; i < pathParts.length; ++i) {
+      testUpdaterDir.append(pathParts[i]);
+    }
+
+    let testUpdater = testUpdaterDir.clone();
+    testUpdater.append(FILE_UPDATER_BIN);
     testUpdater.copyToFollowingLinks(baseAppDir, FILE_UPDATER_BIN);
+  } catch (e) {
+    logTestInfo("Attempt to copy the test updater failed... " +
+                "will try again, Exception: " + e);
+    SimpleTest.executeSoon(copyTestUpdater);
+    return;
+  }
+
+  runTest();
+}
+
+/**
+ * Restores the updater that was backed up. This is called in setupTestUpdater
+ * before the backup of the real updater is done in case the previous test
+ * failed to restore the updater, in finishTestDefaultWaitForWindowClosed when
+ * the test has finished, and in test_9999_cleanup.xul after all tests have
+ * finished.
+ */
+function restoreUpdaterBackup() {
+  let baseAppDir = getAppBaseDir();
+  let updater = baseAppDir.clone();
+  let updaterBackup = baseAppDir.clone();
+  updater.append(FILE_UPDATER_BIN);
+  updaterBackup.append(FILE_UPDATER_BIN_BAK);
+  if (updaterBackup.exists()) {
+    if (updater.exists()) {
+      updater.remove(true);
+    }
+    updaterBackup.moveTo(baseAppDir, FILE_UPDATER_BIN);
   }
 }
 
 /**
  * Sets the most common preferences used by tests to values used by the majority
  * of the tests and when necessary saves the preference's original values if
  * present so they can be set back to the original values when the test has
  * finished.
@@ -1017,17 +1094,16 @@ function resetFiles() {
     try {
       removeDirRecursive(updatedDir);
     }
     catch (e) {
       logTestInfo("Unable to remove directory. Path: " + updatedDir.path +
                   ", Exception: " + e);
     }
   }
-  resetUpdaterBackup();
 }
 
 /**
  * Resets the most common preferences used by tests to their original values.
  */
 function resetPrefs() {
   if (gAppUpdateURL !== undefined) {
     Services.prefs.setCharPref(PREF_APP_UPDATE_URL_OVERRIDE, gAppUpdateURL);
--- a/toolkit/mozapps/update/tests/data/shared.js
+++ b/toolkit/mozapps/update/tests/data/shared.js
@@ -125,16 +125,20 @@ XPCOMUtils.defineLazyGetter(this, "gUP",
 XPCOMUtils.defineLazyGetter(this, "gDefaultPrefBranch", function test_gDPB() {
   return Services.prefs.getDefaultBranch(null);
 });
 
 XPCOMUtils.defineLazyGetter(this, "gPrefRoot", function test_gPR() {
   return Services.prefs.getBranch(null);
 });
 
+XPCOMUtils.defineLazyServiceGetter(this, "gEnv",
+                                   "@mozilla.org/process/environment;1",
+                                   "nsIEnvironment");
+
 XPCOMUtils.defineLazyGetter(this, "gZipW", function test_gZipW() {
   return Cc["@mozilla.org/zipwriter;1"].
          createInstance(Ci.nsIZipWriter);
 });
 
 /* Initializes the update service stub */
 function initUpdateServiceStub() {
   Cc["@mozilla.org/updates/update-service-stub;1"].
--- a/toolkit/mozapps/update/updater/archivereader.cpp
+++ b/toolkit/mozapps/update/updater/archivereader.cpp
@@ -13,17 +13,17 @@
 #ifdef XP_WIN
 #include "nsAlgorithm.h" // Needed by nsVersionComparator.cpp
 #include "updatehelper.h"
 #endif
 
 // These are generated at compile time based on the DER file for the channel
 // being used
 #ifdef MOZ_VERIFY_MAR_SIGNATURE
-#ifdef UPDATER_XPCSHELL_CERT
+#ifdef TEST_UPDATER
 #include "../xpcshellCert.h"
 #else
 #include "primaryCert.h"
 #include "secondaryCert.h"
 #endif
 #endif
 
 #define UPDATER_NO_STRING_GLUE_STL
@@ -80,17 +80,17 @@ ArchiveReader::VerifySignature()
 {
   if (!mArchive) {
     return ARCHIVE_NOT_OPEN;
   }
 
 #ifndef MOZ_VERIFY_MAR_SIGNATURE
   return OK;
 #else
-#ifdef UPDATER_XPCSHELL_CERT
+#ifdef TEST_UPDATER
   int rv = VerifyLoadedCert(mArchive, xpcshellCertData);
 #else
   int rv = VerifyLoadedCert(mArchive, primaryCertData);
   if (rv != OK) {
     rv = VerifyLoadedCert(mArchive, secondaryCertData);
   }
 #endif
   return rv;
--- a/toolkit/mozapps/update/updater/updater-xpcshell/moz.build
+++ b/toolkit/mozapps/update/updater/updater-xpcshell/moz.build
@@ -3,11 +3,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 Program('updater-xpcshell')
 
 updater_rel_path = '../'
 DIST_INSTALL = False
-DEFINES['UPDATER_XPCSHELL_CERT'] = True
+DEFINES['TEST_UPDATER'] = True
 include('../updater-common.build')
 FAIL_ON_WARNINGS = True
--- a/toolkit/mozapps/update/updater/updater.cpp
+++ b/toolkit/mozapps/update/updater/updater.cpp
@@ -2257,17 +2257,28 @@ UpdateThreadFunc(void *param)
 
         rv = gArchiveReader.VerifyProductInformation(MARStrings.MARChannelID,
                                                      MOZ_APP_VERSION);
       }
     }
 #endif
 
     if (rv == OK && sStagedUpdate && !sIsOSUpdate) {
+#ifdef TEST_UPDATER
+      // The MOZ_TEST_SKIP_UPDATE_STAGE environment variable prevents copying
+      // the files in dist/bin in the test updater when staging an update since
+      // this can cause tests to timeout.
+      if (getenv("MOZ_TEST_SKIP_UPDATE_STAGE")) {
+        rv = OK;
+      } else {
+        rv = CopyInstallDirToDestDir();
+      }
+#else
       rv = CopyInstallDirToDestDir();
+#endif
     }
 
     if (rv == OK) {
       rv = DoUpdate();
       gArchiveReader.Close();
       NS_tchar updatingDir[MAXPATHLEN];
       NS_tsnprintf(updatingDir, sizeof(updatingDir)/sizeof(updatingDir[0]),
                    NS_T("%s/updating"), gWorkingDirPath);
--- a/toolkit/mozapps/update/updater/updater.rc
+++ b/toolkit/mozapps/update/updater/updater.rc
@@ -1,15 +1,15 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // Microsoft Visual C++ generated resource script.
 //
-#ifdef UPDATER_XPCSHELL_CERT
+#ifdef TEST_UPDATER
 #include "../resource.h"
 #define MANIFEST_PATH "../updater.exe.manifest"
 #define COMCTL32_MANIFEST_PATH "../updater.exe.comctl32.manifest"
 #define ICON_PATH "../updater.ico"
 #else
 #include "resource.h"
 #define MANIFEST_PATH "updater.exe.manifest"
 #define COMCTL32_MANIFEST_PATH "updater.exe.comctl32.manifest"