Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 14 Jan 2015 16:31:26 -0500
changeset 250945 c1f6345f280392427d99dda1d0757d9be541ea1d
parent 250926 327efe8b3de5b9500263aaebf499b52a71d02bd5 (current diff)
parent 250944 4f5eedfd24593dc22e8a9bc6906d314bf3827f22 (diff)
child 250946 a1344737184b259eb91f5802e25d8df0a3154617
child 250976 565179dcd6341492cef8b9aca66e11cf3d6209fe
child 250999 af7c466285817115717a4994ef38031c9d4a4fff
push id4610
push userjlund@mozilla.com
push dateMon, 30 Mar 2015 18:32:55 +0000
treeherdermozilla-beta@4df54044d9ef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone38.0a1
first release with
nightly linux32
c1f6345f2803 / 38.0a1 / 20150115030228 / files
nightly linux64
c1f6345f2803 / 38.0a1 / 20150115030228 / files
nightly mac
c1f6345f2803 / 38.0a1 / 20150115030228 / files
nightly win32
c1f6345f2803 / 38.0a1 / 20150115030228 / files
nightly win64
c1f6345f2803 / 38.0a1 / 20150115030228 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c. a=merge
mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_back.png
mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_forward.png
mobile/android/base/resources/drawable-large-hdpi-v11/ic_menu_reload.png
mobile/android/base/resources/drawable-large-hdpi-v11/menu.png
mobile/android/base/resources/drawable-large-mdpi-v11/ic_menu_back.png
mobile/android/base/resources/drawable-large-mdpi-v11/ic_menu_forward.png
mobile/android/base/resources/drawable-large-mdpi-v11/ic_menu_reload.png
mobile/android/base/resources/drawable-large-mdpi-v11/menu.png
mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_back.png
mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_forward.png
mobile/android/base/resources/drawable-large-xhdpi-v11/ic_menu_reload.png
mobile/android/base/resources/drawable-large-xhdpi-v11/menu.png
mobile/android/base/resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_add.png
mobile/android/base/resources/drawable-xlarge-hdpi-v11/ic_menu_bookmark_remove.png
mobile/android/base/resources/drawable-xlarge-mdpi-v11/ic_menu_bookmark_add.png
mobile/android/base/resources/drawable-xlarge-mdpi-v11/ic_menu_bookmark_remove.png
mobile/android/base/resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_add.png
mobile/android/base/resources/drawable-xlarge-xhdpi-v11/ic_menu_bookmark_remove.png
toolkit/components/places/nsPIPlacesHistoryListenersNotifier.idl
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Merge day clobber
\ No newline at end of file
+Bug 1101553 - remove nsPIPlacesHistoryListenersNotifier
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -6445,18 +6445,28 @@ function WindowIsClosing()
     return false;
 
   // Bug 967873 - Proxy nsDocumentViewer::PermitUnload to the child process
   if (gMultiProcessBrowser)
     return true;
 
   for (let browser of gBrowser.browsers) {
     let ds = browser.docShell;
-    if (ds.contentViewer && !ds.contentViewer.permitUnload())
+    // Passing true to permitUnload indicates we plan on closing the window.
+    // This means that once unload is permitted, all further calls to
+    // permitUnload will be ignored. This avoids getting multiple prompts
+    // to unload the page.
+    if (ds.contentViewer && !ds.contentViewer.permitUnload(true)) {
+      // ... however, if the user aborts closing, we need to undo that,
+      // to ensure they get prompted again when we next try to close the window.
+      // We do this on the window's toplevel docshell instead of on the tab, so
+      // that all tabs we iterated before will get this reset.
+      window.getInterface(Ci.nsIDocShell).contentViewer.resetCloseWindow();
       return false;
+    }
   }
 
   return true;
 }
 
 /**
  * Checks if this is the last full *browser* window around. If it is, this will
  * be communicated like quitting. Otherwise, we warn about closing multiple tabs.
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2038,35 +2038,16 @@
         <parameter name="aCloseWindowFastpath"/>
         <body>
           <![CDATA[
             if (aTab.closing ||
                 this._windowIsClosing)
               return false;
 
             var browser = this.getBrowserForTab(aTab);
-            if (!aTab._pendingPermitUnload && !aTabWillBeMoved) {
-              let ds = browser.docShell;
-              if (ds && ds.contentViewer) {
-                // We need to block while calling permitUnload() because it
-                // processes the event queue and may lead to another removeTab()
-                // call before permitUnload() even returned.
-                aTab._pendingPermitUnload = true;
-                let permitUnload = ds.contentViewer.permitUnload();
-                delete aTab._pendingPermitUnload;
-                // If we were closed during onbeforeunload, we return false now
-                // so we don't (try to) close the same tab again. Of course, we
-                // also stop if the unload was cancelled by the user:
-                if (aTab.closing || !permitUnload) {
-                  // NB: deliberately keep the _closedDuringPermitUnload set to
-                  // true so we keep exiting early in case of multiple calls.
-                  return false;
-                }
-              }
-            }
 
             var closeWindow = false;
             var newTab = false;
             if (this.tabs.length - this._removingTabs.length == 1) {
               closeWindow = aCloseWindowWithLastTab != null ? aCloseWindowWithLastTab :
                             !window.toolbar.visible ||
                               Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab");
 
@@ -2080,16 +2061,36 @@
                 // cancels the operation.  We are finished here in both cases.
                 this._windowIsClosing = window.closeWindow(true, window.warnAboutClosingWindow);
                 return null;
               }
 
               newTab = true;
             }
 
+            if (!aTab._pendingPermitUnload && !aTabWillBeMoved) {
+              let ds = browser.docShell;
+              if (ds && ds.contentViewer) {
+                // We need to block while calling permitUnload() because it
+                // processes the event queue and may lead to another removeTab()
+                // call before permitUnload() returns.
+                aTab._pendingPermitUnload = true;
+                let permitUnload = ds.contentViewer.permitUnload();
+                delete aTab._pendingPermitUnload;
+                // If we were closed during onbeforeunload, we return false now
+                // so we don't (try to) close the same tab again. Of course, we
+                // also stop if the unload was cancelled by the user:
+                if (aTab.closing || !permitUnload) {
+                  // NB: deliberately keep the _closedDuringPermitUnload set to
+                  // true so we keep exiting early in case of multiple calls.
+                  return false;
+                }
+              }
+            }
+
             aTab.closing = true;
             this._removingTabs.push(aTab);
             this._visibleTabs = null; // invalidate cache
 
             // Invalidate hovered tab state tracking for this closing tab.
             if (this.tabContainer._hoveredTab == aTab)
               aTab._mouseleave();
 
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -121,16 +121,18 @@ skip-if = e10s # Bug 1093153 - no about:
 [browser_autocomplete_a11y_label.js]
 skip-if = e10s # Bug 1101993 - times out for unknown reasons when run in the dir (works on its own)
 [browser_autocomplete_no_title.js]
 [browser_autocomplete_autoselect.js]
 [browser_autocomplete_oldschool_wrap.js]
 [browser_autocomplete_tag_star_visibility.js]
 [browser_backButtonFitts.js]
 skip-if = os != "win" || e10s # The Fitts Law back button is only supported on Windows (bug 571454) / e10s - Bug 1099154: test touches content (attempts to add an event listener directly to the contentWindow)
+[browser_beforeunload_duplicate_dialogs.js]
+skip-if = e10s # bug 967873 means permitUnload doesn't work in e10s mode
 [browser_blob-channelname.js]
 [browser_bookmark_titles.js]
 skip-if = buildapp == 'mulet' || toolkit == "windows" || e10s # Disabled on Windows due to frequent failures (bugs 825739, 841341) / e10s - Bug 1094205 - places doesn't return the right thing in e10s mode, for some reason
 [browser_bug304198.js]
 skip-if = e10s
 [browser_bug321000.js]
 skip-if = true # browser_bug321000.js is disabled because newline handling is shaky (bug 592528)
 [browser_bug329212.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_beforeunload_duplicate_dialogs.js
@@ -0,0 +1,44 @@
+const TEST_PAGE = "http://mochi.test:8888/browser/browser/base/content/test/general/file_double_close_tab.html";
+
+let expectingDialog = false;
+function onTabModalDialogLoaded(node) {
+  ok(expectingDialog, "Should be expecting this dialog.");
+  expectingDialog = false;
+  // This accepts the dialog, closing it
+  node.Dialog.ui.button0.click();
+}
+
+
+// Listen for the dialog being created
+Services.obs.addObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded", false);
+registerCleanupFunction(() => {
+  Services.prefs.clearUserPref("browser.tabs.warnOnClose");
+  Services.obs.removeObserver(onTabModalDialogLoaded, "tabmodal-dialog-loaded");
+});
+
+add_task(function* closeLastTabInWindow() {
+  let newWin = yield promiseOpenAndLoadWindow({}, true);
+  let firstTab = newWin.gBrowser.selectedTab;
+  yield promiseTabLoadEvent(firstTab, TEST_PAGE);
+  let windowClosedPromise = promiseWindowWillBeClosed(newWin);
+  expectingDialog = true;
+  // close tab:
+  document.getAnonymousElementByAttribute(firstTab, "anonid", "close-button").click();
+  yield windowClosedPromise;
+  ok(!expectingDialog, "There should have been a dialog.");
+  ok(newWin.closed, "Window should be closed.");
+});
+
+add_task(function* closeWindowWithMultipleTabsIncludingOneBeforeUnload() {
+  Services.prefs.setBoolPref("browser.tabs.warnOnClose", false);
+  let newWin = yield promiseOpenAndLoadWindow({}, true);
+  let firstTab = newWin.gBrowser.selectedTab;
+  yield promiseTabLoadEvent(firstTab, TEST_PAGE);
+  yield promiseTabLoadEvent(newWin.gBrowser.addTab(), "http://example.com/");
+  let windowClosedPromise = promiseWindowWillBeClosed(newWin);
+  expectingDialog = true;
+  newWin.BrowserTryToCloseWindow();
+  yield windowClosedPromise;
+  ok(!expectingDialog, "There should have been a dialog.");
+  ok(newWin.closed, "Window should be closed.");
+});
--- a/browser/base/content/test/general/file_double_close_tab.html
+++ b/browser/base/content/test/general/file_double_close_tab.html
@@ -1,13 +1,13 @@
 <!DOCTYPE html>
 <html>
   <head>
     <meta charset="utf-8">
-    <title>Test for bug 1050638 - clicking tab close button twice should close tab even in beforeunload case</title>
+    <title>Test page that blocks beforeunload. Used in tests for bug 1050638 and bug 305085</title>
   </head>
   <body>
     This page will block beforeunload. It should still be user-closable at all times.
     <script>
       window.onbeforeunload = function() {
         return "stop";
       };
     </script>
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -197,24 +197,31 @@ function resetBlocklist() {
 function whenNewWindowLoaded(aOptions, aCallback) {
   let win = OpenBrowserWindow(aOptions);
   win.addEventListener("load", function onLoad() {
     win.removeEventListener("load", onLoad, false);
     aCallback(win);
   }, false);
 }
 
+function promiseWindowWillBeClosed(win) {
+  return new Promise((resolve, reject) => {
+    Services.obs.addObserver(function observe(subject, topic) {
+      if (subject == win) {
+        Services.obs.removeObserver(observe, topic);
+        resolve();
+      }
+    }, "domwindowclosed", false);
+  });
+}
+
 function promiseWindowClosed(win) {
-  let deferred = Promise.defer();
-  win.addEventListener("unload", function onunload() {
-    win.removeEventListener("unload", onunload);
-    deferred.resolve();
-  });
+  let promise = promiseWindowWillBeClosed(win);
   win.close();
-  return deferred.promise;
+  return promise;
 }
 
 function promiseOpenAndLoadWindow(aOptions, aWaitForDelayedStartup=false) {
   let deferred = Promise.defer();
   let win = OpenBrowserWindow(aOptions);
   if (aWaitForDelayedStartup) {
     Services.obs.addObserver(function onDS(aSubject, aTopic, aData) {
       if (aSubject != win) {
--- a/browser/components/downloads/DownloadsCommon.jsm
+++ b/browser/components/downloads/DownloadsCommon.jsm
@@ -606,22 +606,18 @@ XPCOMUtils.defineLazyGetter(DownloadsCom
  *
  * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton
  * objects, one accessing non-private downloads, and the other accessing private
  * ones.
  */
 function DownloadsDataCtor(aPrivate) {
   this._isPrivate = aPrivate;
 
-  // This Object contains all the available DownloadsDataItem objects, indexed by
-  // their globally unique identifier.  The identifiers of downloads that have
-  // been removed from the Download Manager data are still present, however the
-  // associated objects are replaced with the value "null".  This is required to
-  // prevent race conditions when populating the list asynchronously.
-  this.dataItems = {};
+  // Contains all the available DownloadsDataItem objects.
+  this.dataItems = new Set();
 
   // Array of view objects that should be notified when the available download
   // data changes.
   this._views = [];
 
   // Maps Download objects to DownloadDataItem objects.
   this._downloadToDataItemMap = new Map();
 }
@@ -639,18 +635,18 @@ DownloadsDataCtor.prototype = {
     }
   },
   _dataLinkInitialized: false,
 
   /**
    * True if there are finished downloads that can be removed from the list.
    */
   get canRemoveFinished() {
-    for (let [, dataItem] of Iterator(this.dataItems)) {
-      if (dataItem && !dataItem.inProgress) {
+    for (let dataItem of this.dataItems) {
+      if (!dataItem.inProgress) {
         return true;
       }
     }
     return false;
   },
 
   /**
    * Asks the back-end to remove finished downloads from the list.
@@ -663,17 +659,17 @@ DownloadsDataCtor.prototype = {
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Integration with the asynchronous Downloads back-end
 
   onDownloadAdded(aDownload) {
     let dataItem = new DownloadsDataItem(aDownload);
     this._downloadToDataItemMap.set(aDownload, dataItem);
-    this.dataItems[dataItem.downloadGuid] = dataItem;
+    this.dataItems.add(dataItem);
 
     for (let view of this._views) {
       view.onDataItemAdded(dataItem, true);
     }
 
     this._updateDataItemState(dataItem);
   },
 
@@ -690,17 +686,17 @@ DownloadsDataCtor.prototype = {
   onDownloadRemoved(aDownload) {
     let dataItem = this._downloadToDataItemMap.get(aDownload);
     if (!dataItem) {
       Cu.reportError("Download doesn't exist.");
       return;
     }
 
     this._downloadToDataItemMap.delete(aDownload);
-    this.dataItems[dataItem.downloadGuid] = null;
+    this.dataItems.delete(dataItem);
     for (let view of this._views) {
       view.onDataItemRemoved(dataItem);
     }
   },
 
   /**
    * Updates the given data item and sends related notifications.
    */
@@ -713,17 +709,17 @@ DownloadsDataCtor.prototype = {
 
     if (wasInProgress && !aDataItem.inProgress) {
       aDataItem.endTime = Date.now();
     }
 
     if (oldState != aDataItem.state) {
       for (let view of this._views) {
         try {
-          view.getViewItem(aDataItem).onStateChange(oldState);
+          view.onDataItemStateChanged(aDataItem, oldState);
         } catch (ex) {
           Cu.reportError(ex);
         }
       }
 
       // This state transition code should actually be located in a Downloads
       // API module (bug 941009).  Moreover, the fact that state is stored as
       // annotations should be ideally hidden behind methods of
@@ -751,17 +747,17 @@ DownloadsDataCtor.prototype = {
       this._notifyDownloadEvent("start");
     }
 
     if (!wasDone && aDataItem.done) {
       this._notifyDownloadEvent("finish");
     }
 
     for (let view of this._views) {
-      view.getViewItem(aDataItem).onProgressChange();
+      view.onDataItemChanged(aDataItem);
     }
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Registration of views
 
   /**
    * Adds an object to be notified when the available download data changes.
@@ -796,19 +792,17 @@ DownloadsDataCtor.prototype = {
    *        DownloadsView object to be initialized.
    */
   _updateView(aView) {
     // Indicate to the view that a batch loading operation is in progress.
     aView.onDataLoadStarting();
 
     // Sort backwards by start time, ensuring that the most recent
     // downloads are added first regardless of their state.
-    let loadedItemsArray = [dataItem
-                            for each (dataItem in this.dataItems)
-                            if (dataItem)];
+    let loadedItemsArray = [...this.dataItems];
     loadedItemsArray.sort((a, b) => b.startTime - a.startTime);
     loadedItemsArray.forEach(dataItem => aView.onDataItemAdded(dataItem, false));
 
     // Notify the view that all data is available.
     aView.onDataLoadCompleted();
   },
 
   //////////////////////////////////////////////////////////////////////////////
@@ -876,34 +870,26 @@ XPCOMUtils.defineLazyGetter(this, "Downl
  * The endTime property is initialized to the current date and time.
  *
  * @param aDownload
  *        The Download object with the current state.
  */
 function DownloadsDataItem(aDownload) {
   this._download = aDownload;
 
-  this.downloadGuid = "id:" + this._autoIncrementId;
   this.file = aDownload.target.path;
   this.target = OS.Path.basename(aDownload.target.path);
   this.uri = aDownload.source.url;
   this.endTime = Date.now();
 
   this.updateFromDownload();
 }
 
 DownloadsDataItem.prototype = {
   /**
-   * The JavaScript API does not need identifiers for Download objects, so they
-   * are generated sequentially for the corresponding DownloadDataItem.
-   */
-  get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId,
-  __lastId: 0,
-
-  /**
    * Updates this object from the underlying Download object.
    */
   updateFromDownload() {
     // Collapse state using the correct priority.
     if (this._download.succeeded) {
       this.state = nsIDM.DOWNLOAD_FINISHED;
     } else if (this._download.error &&
                this._download.error.becauseBlockedByParentalControls) {
@@ -1245,26 +1231,36 @@ const DownloadsViewPrototype = {
    *
    * @note Subclasses should override this.
    */
   onDataItemRemoved(aDataItem) {
     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   },
 
   /**
-   * Returns the view item associated with the provided data item for this view.
+   * Called when the "state" property of a DownloadsDataItem has changed.
    *
-   * @param aDataItem
-   *        DownloadsDataItem object for which the view item is requested.
-   *
-   * @return Object that can be used to notify item status events.
+   * The onDataItemChanged notification will be sent afterwards.
    *
    * @note Subclasses should override this.
    */
-  getViewItem(aDataItem) {
+  onDataItemStateChanged(aDataItem) {
+    throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  /**
+   * Called every time any state property of a DownloadsDataItem may have
+   * changed, including progress properties and the "state" property.
+   *
+   * Note that progress notification changes are throttled at the Downloads.jsm
+   * API level, and there is no throttling mechanism in the front-end.
+   *
+   * @note Subclasses should override this.
+   */
+  onDataItemChanged(aDataItem) {
     throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
   },
 
   /**
    * Private function used to refresh the internal properties being sent to
    * each registered view.
    *
    * @note Subclasses should override this.
@@ -1353,44 +1349,31 @@ DownloadsIndicatorDataCtor.prototype = {
    * @param aDataItem
    *        DownloadsDataItem object that is being removed.
    */
   onDataItemRemoved(aDataItem) {
     this._itemCount--;
     this._updateViews();
   },
 
-  /**
-   * Returns the view item associated with the provided data item for this view.
-   *
-   * @param aDataItem
-   *        DownloadsDataItem object for which the view item is requested.
-   *
-   * @return Object that can be used to notify item status events.
-   */
-  getViewItem(aDataItem) {
-    let data = this._isPrivate ? PrivateDownloadsIndicatorData
-                               : DownloadsIndicatorData;
-    return Object.freeze({
-      onStateChange(aOldState) {
-        if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
-            aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
-          data.attention = true;
-        }
+  // DownloadsView
+  onDataItemStateChanged(aDataItem, aOldState) {
+    if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
+        aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
+      this.attention = true;
+    }
 
-        // Since the state of a download changed, reset the estimated time left.
-        data._lastRawTimeLeft = -1;
-        data._lastTimeLeft = -1;
+    // Since the state of a download changed, reset the estimated time left.
+    this._lastRawTimeLeft = -1;
+    this._lastTimeLeft = -1;
+  },
 
-        data._updateViews();
-      },
-      onProgressChange() {
-        data._updateViews();
-      }
-    });
+  // DownloadsView
+  onDataItemChanged() {
+    this._updateViews();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Propagation of properties to our views
 
   // The following properties are updated by _refreshProperties and are then
   // propagated to the views.  See _refreshProperties for details.
   _hasDownloads: false,
@@ -1475,17 +1458,17 @@ DownloadsIndicatorDataCtor.prototype = {
    * A generator function for the dataItems that this summary is currently
    * interested in. This generator is passed off to summarizeDownloads in order
    * to generate statistics about the dataItems we care about - in this case,
    * it's all dataItems for active downloads.
    */
   _activeDataItems() {
     let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems
                                     : DownloadsData.dataItems;
-    for each (let dataItem in dataItems) {
+    for (let dataItem of dataItems) {
       if (dataItem && dataItem.inProgress) {
         yield dataItem;
       }
     }
   },
 
   /**
    * Computes aggregate values based on the current state of downloads.
@@ -1619,29 +1602,26 @@ DownloadsSummaryData.prototype = {
   },
 
   onDataItemRemoved(aDataItem) {
     let itemIndex = this._dataItems.indexOf(aDataItem);
     this._dataItems.splice(itemIndex, 1);
     this._updateViews();
   },
 
-  getViewItem(aDataItem) {
-    let self = this;
-    return Object.freeze({
-      onStateChange(aOldState) {
-        // Since the state of a download changed, reset the estimated time left.
-        self._lastRawTimeLeft = -1;
-        self._lastTimeLeft = -1;
-        self._updateViews();
-      },
-      onProgressChange() {
-        self._updateViews();
-      }
-    });
+  // DownloadsView
+  onDataItemStateChanged(aOldState) {
+    // Since the state of a download changed, reset the estimated time left.
+    this._lastRawTimeLeft = -1;
+    this._lastTimeLeft = -1;
+  },
+
+  // DownloadsView
+  onDataItemChanged() {
+    this._updateViews();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Propagation of properties to our views
 
   /**
    * Computes aggregate values and propagates the changes to our views.
    */
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -48,24 +48,20 @@ const NOT_AVAILABLE = Number.MAX_VALUE;
  *
  * Once initialized with either a data item or a places node, the created richlistitem
  * can be accessed through the |element| getter, and can then be inserted/removed from
  * a richlistbox.
  *
  * The shell doesn't take care of inserting the item, or removing it when it's no longer
  * valid. That's the caller (a DownloadsPlacesView object) responsibility.
  *
- * The caller is also responsible for "passing over" notification from both the
- * download-view and the places-result-observer, in the following manner:
- *  - The DownloadsPlacesView object implements getViewItem of the download-view
- *    pseudo interface.  It returns this object (therefore we implement
- *    onStateChangea and onProgressChange here).
- *  - The DownloadsPlacesView object adds itself as a places result observer and
- *    calls this object's placesNodeIconChanged, placesNodeTitleChanged and
- *    placeNodeAnnotationChanged from its callbacks.
+ * The caller is also responsible for "passing over" notifications. The
+ * DownloadsPlacesView object implements onDataItemStateChanged and
+ * onDataItemChanged of the DownloadsView pseudo interface, and registers as a
+ * Places result observer.
  *
  * @param [optional] aDataItem
  *        The data item of a the session download. Required if aPlacesNode is not set
  * @param [optional] aPlacesNode
  *        The places node for a past download. Required if aDataItem is not set.
  * @param [optional] aAnnotations
  *        Map containing annotations values, to speed up the initial loading.
  */
@@ -171,23 +167,20 @@ DownloadElementShell.prototype = {
 
   _getIcon() {
     let metaData = this.getDownloadMetaData();
     if ("filePath" in metaData) {
       return "moz-icon://" + metaData.filePath + "?size=32";
     }
 
     if (this._placesNode) {
-      // Try to extract an extension from the uri.
-      let ext = this._downloadURIObj.QueryInterface(Ci.nsIURL).fileExtension;
-      if (ext) {
-        return "moz-icon://." + ext + "?size=32";
-      }
-      return this._placesNode.icon || "moz-icon://.unknown?size=32";
+      return "moz-icon://.unknown?size=32";
     }
+
+    // Assert unreachable.
     if (this._dataItem) {
       throw new Error("Session-download items should always have a target file uri");
     }
 
     throw new Error("Unexpected download element state");
   },
 
   // Helper for getting a places annotation set for the download.
@@ -299,19 +292,19 @@ DownloadElementShell.prototype = {
    * - endTime: the end time of the download.
    * - filePath: the downloaded file path on the file system, when it
    *   was downloaded.  The file may not exist.  This is set for session
    *   downloads that have a local file set, and for history downloads done
    *   after the landing of bug 591289.
    * - fileName: the downloaded file name on the file system. Set if filePath
    *   is set.
    * - displayName: the user-facing label for the download.  This is always
-   *   set.  If available, it's set to the downloaded file name.  If not,
-   *   the places title for the download uri is used it's set.  As a last
-   *   resort, we fallback to the download uri.
+   *   set.  If available, it's set to the downloaded file name.  If not, this
+   *   means the download does not have Places metadata because it is very old,
+   *   and in this rare case the download uri is used.
    * - fileSize (only set for downloads which completed successfully):
    *   the downloaded file size.  For downloads done after the landing of
    *   bug 826991, this value is "static" - that is, it does not necessarily
    *   mean that the file is in place and has this size.
    */
   getDownloadMetaData() {
     if (!this._metaData) {
       if (this._dataItem) {
@@ -343,17 +336,17 @@ DownloadElementShell.prototype = {
         }
 
         try {
           let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
           [this._metaData.filePath, this._metaData.fileName] =
             this._extractFilePathAndNameFromFileURI(targetFileURI);
           this._metaData.displayName = this._metaData.fileName;
         } catch (ex) {
-          this._metaData.displayName = this._placesNode.title || this.downloadURI;
+          this._metaData.displayName = this.downloadURI;
         }
       }
     }
     return this._metaData;
   },
 
   _getStatusText() {
     let s = DownloadsCommon.strings;
@@ -484,119 +477,61 @@ DownloadElementShell.prototype = {
     // Dispatch the ValueChange event for accessibility, if possible.
     if (this._progressElement) {
       let event = document.createEvent("Events");
       event.initEvent("ValueChange", true, true);
       this._progressElement.dispatchEvent(event);
     }
   },
 
-  _updateDisplayNameAndIcon() {
-    let metaData = this.getDownloadMetaData();
-    this._element.setAttribute("displayName", metaData.displayName);
-    this._element.setAttribute("image", this._getIcon());
-  },
-
   _updateUI() {
     if (!this.active) {
       throw new Error("Trying to _updateUI on an inactive download shell");
     }
 
     this._metaData = null;
     this._targetFileInfoFetched = false;
 
-    this._updateDisplayNameAndIcon();
+    let metaData = this.getDownloadMetaData();
+    this._element.setAttribute("displayName", metaData.displayName);
+    this._element.setAttribute("image", this._getIcon());
 
     // For history downloads done in past releases, the downloads/metaData
     // annotation is not set, and therefore we cannot tell the download
     // state without the target file information.
     if (this._dataItem || this.getDownloadMetaData().state !== undefined) {
       this._updateDownloadStatusUI();
     } else {
       this._fetchTargetFileInfo(true);
     }
   },
 
-  placesNodeIconChanged() {
-    if (!this._dataItem) {
-      this._element.setAttribute("image", this._getIcon());
-    }
-  },
-
-  placesNodeTitleChanged() {
-    // If there's a file path, we use the leaf name for the title.
-    if (!this._dataItem && this.active &&
-        !this.getDownloadMetaData().filePath) {
-      this._metaData = null;
-      this._updateDisplayNameAndIcon();
-    }
-  },
-
-  placesNodeAnnotationChanged(aAnnoName) {
-    this._annotations.delete(aAnnoName);
-    if (!this._dataItem && this.active) {
-      if (aAnnoName == DOWNLOAD_META_DATA_ANNO) {
-        let metaData = this.getDownloadMetaData();
-        let annotatedMetaData = this._getAnnotatedMetaData();
-        metaData.endTime = annotatedMetaData.endTime;
-        if ("fileSize" in annotatedMetaData) {
-          metaData.fileSize = annotatedMetaData.fileSize;
-        } else {
-          delete metaData.fileSize;
-        }
-
-        if (metaData.state != annotatedMetaData.state) {
-          metaData.state = annotatedMetaData.state;
-          if (this._element.selected) {
-            goUpdateDownloadCommands();
-          }
-        }
-
-        this._updateDownloadStatusUI();
-      } else if (aAnnoName == DESTINATION_FILE_URI_ANNO) {
-        let metaData = this.getDownloadMetaData();
-        let targetFileURI = this._getAnnotation(DESTINATION_FILE_URI_ANNO);
-        [metaData.filePath, metaData.fileName] =
-            this._extractFilePathAndNameFromFileURI(targetFileURI);
-        metaData.displayName = metaData.fileName;
-        this._updateDisplayNameAndIcon();
-
-        if (this._targetFileInfoFetched) {
-          // This will also update the download commands if necessary.
-          this._targetFileInfoFetched = false;
-          this._fetchTargetFileInfo();
-        }
-      }
-    }
-  },
-
-  /* DownloadView */
-  onStateChange(aOldState) {
+  onStateChanged(aOldState) {
     let metaData = this.getDownloadMetaData();
     metaData.state = this.dataItem.state;
     if (aOldState != nsIDM.DOWNLOAD_FINISHED && aOldState != metaData.state) {
       // See comment in DVI_onStateChange in downloads.js (the panel-view)
       this._element.setAttribute("image", this._getIcon() + "&state=normal");
       metaData.fileSize = this._dataItem.maxBytes;
       if (this._targetFileInfoFetched) {
         this._targetFileInfoFetched = false;
         this._fetchTargetFileInfo();
       }
     }
 
     this._updateDownloadStatusUI();
+
     if (this._element.selected) {
       goUpdateDownloadCommands();
     } else {
       goUpdateCommand("downloadsCmd_clearDownloads");
     }
   },
 
-  /* DownloadView */
-  onProgressChange() {
+  onChanged() {
     this._updateDownloadStatusUI();
   },
 
   /* nsIController */
   isCommandEnabled(aCommand) {
     // The only valid command for inactive elements is cmd_delete.
     if (!this.active && aCommand != "cmd_delete") {
       return false;
@@ -832,25 +767,16 @@ DownloadsPlacesView.prototype = {
   get active() this._active,
   set active(val) {
     this._active = val;
     if (this._active)
       this._ensureVisibleElementsAreActive();
     return this._active;
   },
 
-  _forEachDownloadElementShellForURI(aURI, aCallback) {
-    if (this._downloadElementsShellsForURI.has(aURI)) {
-      let downloadElementShells = this._downloadElementsShellsForURI.get(aURI);
-      for (let des of downloadElementShells) {
-        aCallback(des);
-      }
-    }
-  },
-
   _getAnnotationsFor(aURI) {
     if (!this._cachedAnnotations) {
       this._cachedAnnotations = new Map();
       for (let name of [ DESTINATION_FILE_URI_ANNO,
                          DOWNLOAD_META_DATA_ANNO ]) {
         let results = PlacesUtils.annotations.getAnnotationsWithName(name);
         for (let result of results) {
           let url = result.uri.spec;
@@ -929,17 +855,17 @@ DownloadsPlacesView.prototype = {
     //    another shell for the download (so we have one shell for each data
     //    item).
     //
     // Note: If a cancelled session download is already in the list, and the
     // download is retired, onDataItemAdded is called again for the same
     // data item. Thus, we also check that we make sure we don't have a view item
     // already.
     if (!shouldCreateShell &&
-        aDataItem && this.getViewItem(aDataItem) == null) {
+        aDataItem && !this._viewItemsForDataItems.has(aDataItem)) {
       // If there's a past-download-only shell for this download-uri with no
       // associated data item, use it for the new data item. Otherwise, go ahead
       // and create another shell.
       shouldCreateShell = true;
       for (let shell of shellsForURI) {
         if (!shell.dataItem) {
           shouldCreateShell = false;
           shell.dataItem = aDataItem;
@@ -1046,17 +972,17 @@ DownloadsPlacesView.prototype = {
   },
 
   _removeSessionDownloadFromView(aDataItem) {
     let shells = this._downloadElementsShellsForURI.get(aDataItem.uri);
     if (shells.size == 0) {
       throw new Error("Should have had at leaat one shell for this uri");
     }
 
-    let shell = this.getViewItem(aDataItem);
+    let shell = this._viewItemsForDataItems.get(aDataItem);
     if (!shells.has(shell)) {
       throw new Error("Missing download element shell in shells list for url");
     }
 
     // If there's more than one item for this download uri, we can let the
     // view item for this this particular data item go away.
     // If there's only one item for this download uri, we should only
     // keep it if it is associated with a history download.
@@ -1272,31 +1198,19 @@ DownloadsPlacesView.prototype = {
   nodeInserted(aParent, aPlacesNode) {
     this._addDownloadData(null, aPlacesNode);
   },
 
   nodeRemoved(aParent, aPlacesNode, aOldIndex) {
     this._removeHistoryDownloadFromView(aPlacesNode);
   },
 
-  nodeIconChanged(aNode) {
-    this._forEachDownloadElementShellForURI(aNode.uri,
-                                            des => des.placesNodeIconChanged());
-  },
-
-  nodeAnnotationChanged(aNode, aAnnoName) {
-    this._forEachDownloadElementShellForURI(aNode.uri,
-                                            des => des.placesNodeAnnotationChanged(aAnnoName));
-  },
-
-  nodeTitleChanged(aNode, aNewTitle) {
-    this._forEachDownloadElementShellForURI(aNode.uri,
-                                            des => des.placesNodeTitleChanged());
-  },
-
+  nodeAnnotationChanged() {},
+  nodeIconChanged() {},
+  nodeTitleChanged() {},
   nodeKeywordChanged() {},
   nodeDateAddedChanged() {},
   nodeLastModifiedChanged() {},
   nodeHistoryDetailsChanged() {},
   nodeTagsChanged() {},
   sortingChanged() {},
   nodeMoved() {},
   nodeURIChanged() {},
@@ -1357,18 +1271,24 @@ DownloadsPlacesView.prototype = {
   onDataItemAdded(aDataItem, aNewest) {
     this._addDownloadData(aDataItem, null, aNewest);
   },
 
   onDataItemRemoved(aDataItem) {
     this._removeSessionDownloadFromView(aDataItem);
   },
 
-  getViewItem(aDataItem) {
-    return this._viewItemsForDataItems.get(aDataItem, null);
+  // DownloadsView
+  onDataItemStateChanged(aDataItem, aOldState) {
+    this._viewItemsForDataItems.get(aDataItem).onStateChanged(aOldState);
+  },
+
+  // DownloadsView
+  onDataItemChanged(aDataItem) {
+    this._viewItemsForDataItems.get(aDataItem).onChanged();
   },
 
   supportsCommand(aCommand) {
     if (DOWNLOAD_VIEW_SUPPORTED_COMMANDS.indexOf(aCommand) != -1) {
       // The clear-downloads command may be performed by the toolbar-button,
       // which can be focused on OS X.  Thus enable this command even if the
       // richlistbox is not focused.
       // For other commands, be prudent and disable them unless the richlistview
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -549,17 +549,17 @@ const DownloadsPanel = {
         DownloadsCommon.error("Downloads button cannot be found.");
         return;
       }
 
       // When the panel is opened, we check if the target files of visible items
       // still exist, and update the allowed items interactions accordingly.  We
       // do these checks on a background thread, and don't prevent the panel to
       // be displayed while these checks are being performed.
-      for each (let viewItem in DownloadsView._viewItems) {
+      for (let viewItem of DownloadsView._visibleViewItems.values()) {
         viewItem.verifyTargetExists();
       }
 
       DownloadsCommon.log("Opening downloads panel popup.");
       this.panel.openPopup(anchor, "bottomcenter topright", 0, 0, false, null);
     });
   },
 };
@@ -668,21 +668,21 @@ const DownloadsView = {
    * Ordered array of all DownloadsDataItem objects.  We need to keep this array
    * because only a limited number of items are shown at once, and if an item
    * that is currently visible is removed from the list, we might need to take
    * another item from the array and make it appear at the bottom.
    */
   _dataItems: [],
 
   /**
-   * Object containing the available DownloadsViewItem objects, indexed by their
-   * numeric download identifier.  There is a limited number of view items in
-   * the panel at any given time.
+   * Associates the visible DownloadsDataItem objects with their corresponding
+   * DownloadsViewItem object.  There is a limited number of view items in the
+   * panel at any given time.
    */
-  _viewItems: {},
+  _visibleViewItems: new Map(),
 
   /**
    * Called when the number of items in the list changes.
    */
   _itemCountChanged() {
     DownloadsCommon.log("The downloads item count has changed - we are tracking",
                         this._dataItems.length, "downloads in total.");
     let count = this._dataItems.length;
@@ -809,73 +809,77 @@ const DownloadsView = {
         // Reinsert the next item into the panel.
         this._addViewItem(this._dataItems[this.kItemCountLimit - 1], false);
       }
     }
 
     this._itemCountChanged();
   },
 
-  /**
-   * Returns the view item associated with the provided data item for this view.
-   *
-   * @param aDataItem
-   *        DownloadsDataItem object for which the view item is requested.
-   *
-   * @return Object that can be used to notify item status events.
-   */
-  getViewItem(aDataItem) {
-    // If the item is visible, just return it, otherwise return a mock object
-    // that doesn't react to notifications.
-    if (aDataItem.downloadGuid in this._viewItems) {
-      return this._viewItems[aDataItem.downloadGuid];
+  // DownloadsView
+  onDataItemStateChanged(aDataItem, aOldState) {
+    let viewItem = this._visibleViewItems.get(aDataItem);
+    if (viewItem) {
+      viewItem.onStateChanged(aOldState);
     }
-    return this._invisibleViewItem;
+  },
+
+  // DownloadsView
+  onDataItemChanged(aDataItem) {
+    let viewItem = this._visibleViewItems.get(aDataItem);
+    if (viewItem) {
+      viewItem.onChanged();
+    }
   },
 
   /**
-   * Mock DownloadsDataItem object that doesn't react to notifications.
+   * Associates each richlistitem for a download with its corresponding
+   * DownloadsViewItemController object.
    */
-  _invisibleViewItem: Object.freeze({
-    onStateChange() {},
-    onProgressChange() {},
-  }),
+  _controllersForElements: new Map(),
+
+  controllerForElement(element) {
+    return this._controllersForElements.get(element);
+  },
 
   /**
    * Creates a new view item associated with the specified data item, and adds
    * it to the top or the bottom of the list.
    */
   _addViewItem(aDataItem, aNewest)
   {
     DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.",
                         "aNewest =", aNewest);
 
     let element = document.createElement("richlistitem");
     let viewItem = new DownloadsViewItem(aDataItem, element);
-    this._viewItems[aDataItem.downloadGuid] = viewItem;
+    this._visibleViewItems.set(aDataItem, viewItem);
+    let viewItemController = new DownloadsViewItemController(aDataItem);
+    this._controllersForElements.set(element, viewItemController);
     if (aNewest) {
       this.richListBox.insertBefore(element, this.richListBox.firstChild);
     } else {
       this.richListBox.appendChild(element);
     }
   },
 
   /**
    * Removes the view item associated with the specified data item.
    */
   _removeViewItem(aDataItem) {
     DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list.");
-    let element = this.getViewItem(aDataItem)._element;
+    let element = this._visibleViewItems.get(aDataItem)._element;
     let previousSelectedIndex = this.richListBox.selectedIndex;
     this.richListBox.removeChild(element);
     if (previousSelectedIndex != -1) {
       this.richListBox.selectedIndex = Math.min(previousSelectedIndex,
                                                 this.richListBox.itemCount - 1);
     }
-    delete this._viewItems[aDataItem.downloadGuid];
+    this._visibleViewItems.delete(aDataItem);
+    this._controllersForElements.delete(element);
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// User interface event functions
 
   /**
    * Helper function to do commands on a specific download item.
    *
@@ -886,17 +890,17 @@ const DownloadsView = {
    * @param aCommand
    *        The command to be performed.
    */
   onDownloadCommand(aEvent, aCommand) {
     let target = aEvent.target;
     while (target.nodeName != "richlistitem") {
       target = target.parentNode;
     }
-    new DownloadsViewItemController(target).doCommand(aCommand);
+    DownloadsView.controllerForElement(target).doCommand(aCommand);
   },
 
   onDownloadClick(aEvent) {
     // Handle primary clicks only, and exclude the action button.
     if (aEvent.button == 0 &&
         !aEvent.originalTarget.hasAttribute("oncommand")) {
       goDoCommand("downloadsCmd_open");
     }
@@ -960,18 +964,18 @@ const DownloadsView = {
   },
 
   onDownloadDragStart(aEvent) {
     let element = this.richListBox.selectedItem;
     if (!element) {
       return;
     }
 
-    let controller = new DownloadsViewItemController(element);
-    let localFile = controller.dataItem.localFile;
+    let localFile = DownloadsView.controllerForElement(element)
+                                 .dataItem.localFile;
     if (!localFile.exists()) {
       return;
     }
 
     let dataTransfer = aEvent.dataTransfer;
     dataTransfer.mozSetDataAt("application/x-moz-file", localFile, 0);
     dataTransfer.effectAllowed = "copyMove";
     var url = Services.io.newFileURI(localFile).spec;
@@ -1005,18 +1009,16 @@ function DownloadsViewItem(aDataItem, aE
   // as bug 239948 comment 12 is handled, the "file" property will be always a
   // file URL rather than a file name.  At that point we should remove the "//"
   // (double slash) from the icon URI specification (see test_moz_icon_uri.js).
   this.image = "moz-icon://" + this.dataItem.file + "?size=32";
 
   let attributes = {
     "type": "download",
     "class": "download-state",
-    "id": "downloadsItem_" + this.dataItem.downloadGuid,
-    "downloadGuid": this.dataItem.downloadGuid,
     "state": this.dataItem.state,
     "progress": this.dataItem.inProgress ? this.dataItem.percentComplete : 100,
     "target": this.dataItem.target,
     "image": this.image
   };
 
   for (let attributeName in attributes) {
     this._element.setAttribute(attributeName, attributes[attributeName]);
@@ -1047,17 +1049,17 @@ DownloadsViewItem.prototype = {
   //////////////////////////////////////////////////////////////////////////////
   //// Callback functions from DownloadsData
 
   /**
    * Called when the download state might have changed.  Sometimes the state of
    * the download might be the same as before, if the data layer received
    * multiple events for the same download.
    */
-  onStateChange(aOldState) {
+  onStateChanged(aOldState) {
     // If a download just finished successfully, it means that the target file
     // now exists and we can extract its specific icon.  To ensure that the icon
     // is reloaded, we must change the URI used by the XUL image element, for
     // example by adding a query parameter.  Since this URI has a "moz-icon"
     // scheme, this only works if we add one of the parameters explicitly
     // supported by the nsIMozIconURI interface.
     if (aOldState != Ci.nsIDownloadManager.DOWNLOAD_FINISHED &&
         aOldState != this.dataItem.state) {
@@ -1067,24 +1069,22 @@ DownloadsViewItem.prototype = {
       // successfully, without checking the condition in the background.  If the
       // panel is already open, this will take effect immediately.  If the panel
       // is opened later, a new background existence check will be performed.
       this._element.setAttribute("exists", "true");
     }
 
     // Update the user interface after switching states.
     this._element.setAttribute("state", this.dataItem.state);
-    this._updateProgress();
-    this._updateStatusLine();
   },
 
   /**
    * Called when the download progress has changed.
    */
-  onProgressChange() {
+  onChanged() {
     this._updateProgress();
     this._updateStatusLine();
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// Functions for updating the user interface
 
   /**
@@ -1278,32 +1278,32 @@ const DownloadsViewController = {
   isCommandEnabled(aCommand) {
     // Handle commands that are not selection-specific.
     if (aCommand == "downloadsCmd_clearList") {
       return DownloadsCommon.getData(window).canRemoveFinished;
     }
 
     // Other commands are selection-specific.
     let element = DownloadsView.richListBox.selectedItem;
-    return element &&
-           new DownloadsViewItemController(element).isCommandEnabled(aCommand);
+    return element && DownloadsView.controllerForElement(element)
+                                   .isCommandEnabled(aCommand);
   },
 
   doCommand(aCommand) {
     // If this command is not selection-specific, execute it.
     if (aCommand in this.commands) {
       this.commands[aCommand].apply(this);
       return;
     }
 
     // Other commands are selection-specific.
     let element = DownloadsView.richListBox.selectedItem;
     if (element) {
       // The doCommand function also checks if the command is enabled.
-      new DownloadsViewItemController(element).doCommand(aCommand);
+      DownloadsView.controllerForElement(element).doCommand(aCommand);
     }
   },
 
   onEvent() {},
 
   //////////////////////////////////////////////////////////////////////////////
   //// Other functions
 
@@ -1329,19 +1329,18 @@ const DownloadsViewController = {
 
 ////////////////////////////////////////////////////////////////////////////////
 //// DownloadsViewItemController
 
 /**
  * Handles all the user interaction events, in particular the "commands",
  * related to a single item in the downloads list widgets.
  */
-function DownloadsViewItemController(aElement) {
-  let downloadGuid = aElement.getAttribute("downloadGuid");
-  this.dataItem = DownloadsCommon.getData(window).dataItems[downloadGuid];
+function DownloadsViewItemController(aDataItem) {
+  this.dataItem = aDataItem;
 }
 
 DownloadsViewItemController.prototype = {
   //////////////////////////////////////////////////////////////////////////////
   //// Command dispatching
 
   /**
    * The DownloadDataItem controlled by this object.
--- a/browser/components/downloads/test/browser/browser_basic_functionality.js
+++ b/browser/components/downloads/test/browser/browser_basic_functionality.js
@@ -44,12 +44,12 @@ add_task(function* test_basic_functional
   let richlistbox = document.getElementById("downloadsListBox");
   /* disabled for failing intermittently (bug 767828)
     is(richlistbox.children.length, DownloadData.length,
        "There is the correct number of richlistitems");
   */
   let itemCount = richlistbox.children.length;
   for (let i = 0; i < itemCount; i++) {
     let element = richlistbox.children[itemCount - i - 1];
-    let dataItem = new DownloadsViewItemController(element).dataItem;
+    let dataItem = DownloadsView.controllerForElement(element).dataItem;
     is(dataItem.state, DownloadData[i].state, "Download states match up");
   }
 });
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -766,17 +766,17 @@ loop.conversationViews = (function(mozL1
       return React.createElement("p", {className: "error"}, mozL10n.get("unable_retrieve_url"));
     },
 
     _getTitleMessage: function() {
       var callStateReason =
         this.props.store.getStoreState("callStateReason");
 
       if (callStateReason === "reject" || callStateReason === "busy" ||
-          callStateReason === "setup") {
+          callStateReason === "user-unknown") {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
         }
 
         return mozL10n.get("generic_contact_unavailable_title");
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -766,17 +766,17 @@ loop.conversationViews = (function(mozL1
       return <p className="error">{mozL10n.get("unable_retrieve_url")}</p>;
     },
 
     _getTitleMessage: function() {
       var callStateReason =
         this.props.store.getStoreState("callStateReason");
 
       if (callStateReason === "reject" || callStateReason === "busy" ||
-          callStateReason === "setup") {
+          callStateReason === "user-unknown") {
         var contactDisplayName = _getContactDisplayName(this.props.contact);
         if (contactDisplayName.length) {
           return mozL10n.get(
             "contact_unavailable_title",
             {"contactName": contactDisplayName});
         }
 
         return mozL10n.get("generic_contact_unavailable_title");
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -369,18 +369,22 @@ loop.store = loop.store || {};
       appendContactValues("email");
       appendContactValues("tel", true);
 
       this.client.setupOutgoingCall(contactAddresses,
         this.getStoreState("callType"),
         function(err, result) {
           if (err) {
             console.error("Failed to get outgoing call data", err);
+            var failureReason = "setup";
+            if (err.errno == 122) {
+              failureReason = "user-unknown";
+            }
             this.dispatcher.dispatch(
-              new sharedActions.ConnectionFailure({reason: "setup"}));
+              new sharedActions.ConnectionFailure({reason: failureReason}));
             return;
           }
 
           // Success, dispatch a new action.
           this.dispatcher.dispatch(
             new sharedActions.ConnectCall({sessionData: result}));
         }.bind(this)
       );
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -439,23 +439,33 @@ describe("loop.conversationViews", funct
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
           {contactName: loop.conversationViews._getContactDisplayName(contact)});
       });
 
-    it("should show 'contact unavailable' when the reason is 'setup'",
+    it("should show 'something went wrong' when the reason is 'setup'",
       function () {
         store.setStoreState({callStateReason: "setup"});
 
         view = mountTestComponent({contact: contact});
 
         sinon.assert.calledWithExactly(document.mozL10n.get,
+          "generic_failure_title");
+      });
+
+    it("should show 'contact unavailable' when the reason is 'user-unknown'",
+      function () {
+        store.setStoreState({callStateReason: "user-unknown"});
+
+        view = mountTestComponent({contact: contact});
+
+        sinon.assert.calledWithExactly(document.mozL10n.get,
           "contact_unavailable_title",
           {contactName: loop.conversationViews._getContactDisplayName(contact)});
       });
 
     it("should display a generic contact unavailable msg when the reason is" +
        " 'busy' and no display name is available", function() {
         store.setStoreState({callStateReason: "busy"});
         var phoneOnlyContact = {
--- a/mobile/android/base/gfx/ImmutableViewportMetrics.java
+++ b/mobile/android/base/gfx/ImmutableViewportMetrics.java
@@ -228,22 +228,22 @@ public class ImmutableViewportMetrics {
 
     public ImmutableViewportMetrics offsetViewportBy(float dx, float dy) {
         return setViewportOrigin(viewportRectLeft + dx, viewportRectTop + dy);
     }
 
     public ImmutableViewportMetrics offsetViewportByAndClamp(float dx, float dy) {
         if (isRTL) {
             return setViewportOrigin(
-                Math.min(pageRectRight - getWidth(), Math.max(viewportRectLeft + dx, pageRectLeft)),
-                Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
+                Math.min(pageRectRight - getWidthWithoutMargins(), Math.max(viewportRectLeft + dx, pageRectLeft)),
+                Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeightWithoutMargins())));
         }
         return setViewportOrigin(
-            Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidth())),
-            Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeight())));
+            Math.max(pageRectLeft, Math.min(viewportRectLeft + dx, pageRectRight - getWidthWithoutMargins())),
+            Math.max(pageRectTop, Math.min(viewportRectTop + dy, pageRectBottom - getHeightWithoutMargins())));
     }
 
     public ImmutableViewportMetrics setPageRect(RectF pageRect, RectF cssPageRect) {
         return new ImmutableViewportMetrics(
             pageRect.left, pageRect.top, pageRect.right, pageRect.bottom,
             cssPageRect.left, cssPageRect.top, cssPageRect.right, cssPageRect.bottom,
             viewportRectLeft, viewportRectTop, viewportRectRight, viewportRectBottom,
             marginLeft, marginTop, marginRight, marginBottom,
deleted file mode 100644
index 549f83c9cfea15443d5a15ee5acc290e4947f74a..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index c7a00935f19c3b7892d06d20b5e1ca6fdd9d49ef..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 6d0ef5e4bf549a72b354baf7297b09520c4739eb..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 4bfb224494577cfb755ca31d89f381febd0da805..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 3f9cadb199f0ee735a1f8cd27eef050d2241be06..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index d271ba173b1f43b348e034a949836e25a3078101..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index c4c898c8afa0af5582567bac4fd903424563a2d5..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 95733dfe05d35106bc429c8a631df5accb9daf02..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-v11/ic_menu_back.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<!-- Bug 1106935: Temporary replacement for old tablet resources -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@null"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-v11/ic_menu_forward.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<!-- Bug 1106935: Temporary replacement for old tablet resources -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@null"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-v11/ic_menu_reload.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<!-- Bug 1106935: Temporary replacement for old tablet resources -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@null"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-large-v11/menu.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<!-- Bug 1106935: Temporary replacement for old tablet resources -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@null"/>
deleted file mode 100644
index 3bc39622b1e9091ff0083406f3b9771f3b55b88c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index b7b5a2005cd5730a407169187d0f8a94355ecb62..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 8b3e09a38b0a65a8807716608d0dd1917ee7a37d..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 6832553c41203855ef5d9f26169e9c5cd308feee..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 51016f3aeace87e568fadce2f6b0c8065a6408df..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index c4013a0f7b60cb922e788b243fc74f6bff083af6..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 00cfda36b6b71a5309c907242f85981fe7b86469..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index cc0449bf18fc9704105797df0b2824673ca7282e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xlarge-v11/ic_menu_bookmark_add.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<!-- Bug 1106935: Temporary replacement for old tablet resources -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@null"/>
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable-xlarge-v11/ic_menu_bookmark_remove.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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/. -->
+
+<!-- Bug 1106935: Temporary replacement for old tablet resources -->
+<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
+        android:src="@null"/>
deleted file mode 100644
index 91167c70c16b71ecba669fe809398dfec94a7e57..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 615d5eabbe01819cd81635639d5842172db8b458..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/mobile/android/base/resources/menu-v11/preferences_search_menu.xml
+++ b/mobile/android/base/resources/menu-v11/preferences_search_menu.xml
@@ -1,11 +1,11 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 	<item android:id="@+id/restore_defaults"
-	      android:drawable="@drawable/menu"
+	      android:drawable="@drawable/new_tablet_menu"
 	      android:showAsAction="never"
 	      android:title="@string/pref_search_restore_defaults" />
 </menu>
--- a/mobile/android/base/updater/UpdateService.java
+++ b/mobile/android/base/updater/UpdateService.java
@@ -113,21 +113,16 @@ public class UpdateService extends Inten
         if (mDownloading && UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) {
             Log.i(LOGTAG, "will apply update when download finished");
 
             mApplyImmediately = true;
             showDownloadNotification();
         } else if (UpdateServiceHelper.ACTION_CANCEL_DOWNLOAD.equals(intent.getAction())) {
             mCancelDownload = true;
         } else {
-            if (!UpdateServiceHelper.ACTION_APPLY_UPDATE.equals(intent.getAction())) {
-                // Delete the update package used to install the current version.
-                deleteUpdatePackage(getLastFileName());
-            }
-
             super.onStartCommand(intent, flags, startId);
         }
 
         return Service.START_REDELIVER_INTENT;
     }
 
     @Override
     protected void onHandleIntent (Intent intent) {
@@ -460,16 +455,21 @@ public class UpdateService extends Inten
                 Log.i(LOGTAG, "using existing update package");
                 return downloadFile;
             } else {
                 // Didn't match, so we're going to download a new one.
                 downloadFile.delete();
             }
         }
 
+        if (!info.buildID.equals(getLastBuildID())) {
+            // Delete the previous package when a new version becomes available.
+            deleteUpdatePackage(getLastFileName());
+        }
+
         Log.i(LOGTAG, "downloading update package");
         sendCheckUpdateResult(UpdateServiceHelper.CheckUpdateResult.DOWNLOADING);
 
         OutputStream output = null;
         InputStream input = null;
 
         mDownloading = true;
         mCancelDownload = false;
--- a/mobile/android/modules/HelperApps.jsm
+++ b/mobile/android/modules/HelperApps.jsm
@@ -98,26 +98,34 @@ var HelperApps =  {
         });
       } catch(e) {}
     }
 
     return results;
   },
 
   getAppsForUri: function getAppsForUri(uri, flags = { }, callback) {
+    // Return early for well-known internal schemes
+    if (!uri || uri.schemeIs("about") || uri.schemeIs("chrome")) {
+      if (callback) {
+        callback([]);
+      }
+      return [];
+    }
+
     flags.filterBrowsers = "filterBrowsers" in flags ? flags.filterBrowsers : true;
     flags.filterHtml = "filterHtml" in flags ? flags.filterHtml : true;
 
     // Query for apps that can/can't handle the mimetype
     let msg = this._getMessage("Intent:GetHandlers", uri, flags);
     let parseData = (d) => {
       let apps = []
-
-      if (!d)
+      if (!d) {
         return apps;
+      }
 
       apps = this._parseApps(d.apps);
 
       if (flags.filterBrowsers) {
         apps = apps.filter((app) => {
           return app.name && !this.defaultBrowsers[app.name];
         });
       }
@@ -135,18 +143,16 @@ var HelperApps =  {
         }
       }
 
       return apps;
     };
 
     if (!callback) {
       let data = this._sendMessageSync(msg);
-      if (!data)
-        return [];
       return parseData(data);
     } else {
       Messaging.sendRequestForResult(msg).then(function(data) {
         callback(parseData(data));
       });
     }
   },
 
@@ -168,19 +174,20 @@ var HelperApps =  {
                  "activityName" : appInfo[i+3]}));
     }
 
     return apps;
   },
 
   _getMessage: function(type, uri, options = {}) {
     let mimeType = options.mimeType;
-    if (uri && mimeType == undefined)
+    if (uri && mimeType == undefined) {
       mimeType = ContentAreaUtils.getMIMETypeForURI(uri) || "";
-      
+    }
+
     return {
       type: type,
       mime: mimeType,
       action: options.action || "", // empty action string defaults to android.intent.action.VIEW
       url: uri ? uri.spec : "",
       packageName: options.packageName || "",
       className: options.className || ""
     };
@@ -206,14 +213,15 @@ var HelperApps =  {
 
   _sendMessageSync: function(msg) {
     let res = null;
     Messaging.sendRequestForResult(msg).then(function(data) {
       res = data;
     });
 
     let thread = Services.tm.currentThread;
-    while (res == null)
+    while (res == null) {
       thread.processNextEvent(true);
+    }
 
     return res;
   },
 };
--- a/toolkit/components/places/History.jsm
+++ b/toolkit/components/places/History.jsm
@@ -72,19 +72,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
                                   "resource://gre/modules/Sqlite.jsm");
-XPCOMUtils.defineLazyServiceGetter(this, "gNotifier",
-                                   "@mozilla.org/browser/nav-history-service;1",
-                                   Ci.nsPIPlacesHistoryListenersNotifier);
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 Cu.importGlobalProperties(["URL"]);
 
 /**
  * Whenever we update or remove numerous pages, it is preferable
  * to yield time to the main thread every so often to avoid janking.
  * This constant determines the maximal number of notifications we
@@ -524,24 +521,23 @@ let remove = Task.async(function*({guids
     if (hasPagesToRemove) {
       let ids = [p.id for (p of pages) if (p.toRemove)];
       yield db.execute(`DELETE FROM moz_places
                         WHERE id IN (${ sqlList(ids) })
                        `);
     }
 
     // 5. Notify observers.
+    let observers = PlacesUtils.history.getObservers();
+    let reason = Ci.nsINavHistoryObserver.REASON_DELETED;
     for (let {guid, uri, toRemove} of pages) {
-      gNotifier.notifyOnPageExpired(
-        uri, // uri
-        0, // visitTime - There are no more visits
-        toRemove, // wholeEntry
-        guid, // guid
-        Ci.nsINavHistoryObserver.REASON_DELETED, // reason
-        -1 // transition
-      );
+      if (toRemove) {
+        notify(observers, "onDeleteURI", [uri, guid, reason]);
+      } else {
+        notify(observers, "onDeleteVisits", [uri, 0, guid, reason, 0]);
+      }
     }
   });
 
   PlacesUtils.history.clearEmbedVisits();
 
   return hasPagesToRemove;
 });
--- a/toolkit/components/places/moz.build
+++ b/toolkit/components/places/moz.build
@@ -23,17 +23,16 @@ if CONFIG['MOZ_PLACES']:
         'mozIColorAnalyzer.idl',
         'mozIPlacesAutoComplete.idl',
         'nsIAnnotationService.idl',
         'nsIBrowserHistory.idl',
         'nsIFaviconService.idl',
         'nsINavBookmarksService.idl',
         'nsITaggingService.idl',
         'nsPIPlacesDatabase.idl',
-        'nsPIPlacesHistoryListenersNotifier.idl',
     ]
 
     EXPORTS.mozilla.places = [
         'Database.h',
         'History.h',
     ]
 
     UNIFIED_SOURCES += [
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -163,17 +163,16 @@ NS_IMPL_RELEASE(nsNavHistory)
 NS_IMPL_CLASSINFO(nsNavHistory, nullptr, nsIClassInfo::SINGLETON,
                   NS_NAVHISTORYSERVICE_CID)
 NS_INTERFACE_MAP_BEGIN(nsNavHistory)
   NS_INTERFACE_MAP_ENTRY(nsINavHistoryService)
   NS_INTERFACE_MAP_ENTRY(nsIBrowserHistory)
   NS_INTERFACE_MAP_ENTRY(nsIObserver)
   NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
   NS_INTERFACE_MAP_ENTRY(nsPIPlacesDatabase)
-  NS_INTERFACE_MAP_ENTRY(nsPIPlacesHistoryListenersNotifier)
   NS_INTERFACE_MAP_ENTRY(mozIStorageVacuumParticipant)
   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsINavHistoryService)
   NS_IMPL_QUERY_CLASSINFO(nsNavHistory)
 NS_INTERFACE_MAP_END
 
 // We don't care about flattening everything
 NS_IMPL_CI_INTERFACE_GETTER(nsNavHistory,
                             nsINavHistoryService,
@@ -3046,19 +3045,17 @@ nsNavHistory::AsyncExecuteLegacyQueries(
 
   rv = statement->ExecuteAsync(aCallback, _stmt);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 
-// nsPIPlacesHistoryListenersNotifier ******************************************
-
-NS_IMETHODIMP
+nsresult
 nsNavHistory::NotifyOnPageExpired(nsIURI *aURI, PRTime aVisitTime,
                                   bool aWholeEntry, const nsACString& aGUID,
                                   uint16_t aReason, uint32_t aTransitionType)
 {
   // Invalidate the cached value for whether there's history or not.
   mDaysOfHistory = -1;
 
   MOZ_ASSERT(!aGUID.IsEmpty());
--- a/toolkit/components/places/nsNavHistory.h
+++ b/toolkit/components/places/nsNavHistory.h
@@ -3,17 +3,16 @@
  * 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/. */
 
 #ifndef nsNavHistory_h_
 #define nsNavHistory_h_
 
 #include "nsINavHistoryService.h"
 #include "nsPIPlacesDatabase.h"
-#include "nsPIPlacesHistoryListenersNotifier.h"
 #include "nsIBrowserHistory.h"
 #include "nsINavBookmarksService.h"
 #include "nsIFaviconService.h"
 
 #include "nsIObserverService.h"
 #include "nsICollation.h"
 #include "nsIStringBundle.h"
 #include "nsITimer.h"
@@ -62,30 +61,28 @@ class nsIAutoCompleteController;
 
 // nsNavHistory
 
 class nsNavHistory MOZ_FINAL : public nsSupportsWeakReference
                              , public nsINavHistoryService
                              , public nsIObserver
                              , public nsIBrowserHistory
                              , public nsPIPlacesDatabase
-                             , public nsPIPlacesHistoryListenersNotifier
                              , public mozIStorageVacuumParticipant
 {
   friend class PlacesSQLQueryBuilder;
 
 public:
   nsNavHistory();
 
   NS_DECL_THREADSAFE_ISUPPORTS
   NS_DECL_NSINAVHISTORYSERVICE
   NS_DECL_NSIBROWSERHISTORY
   NS_DECL_NSIOBSERVER
   NS_DECL_NSPIPLACESDATABASE
-  NS_DECL_NSPIPLACESHISTORYLISTENERSNOTIFIER
   NS_DECL_MOZISTORAGEVACUUMPARTICIPANT
 
   /**
    * Obtains the nsNavHistory object.
    */
   static already_AddRefed<nsNavHistory> GetSingleton();
 
   /**
@@ -179,16 +176,39 @@ public:
    *
    * @param aPlacesIdsQueryString
    *        Query string containing list of places to be invalidated.  If it's
    *        an empty string all places will be invalidated.
    */
   nsresult invalidateFrecencies(const nsCString& aPlaceIdsQueryString);
 
   /**
+   * Calls onDeleteVisits and onDeleteURI notifications on registered listeners
+   * with the history service.
+   *
+   * @param aURI
+   *        The nsIURI object representing the URI of the page being expired.
+   * @param aVisitTime
+   *        The time, in microseconds, that the page being expired was visited.
+   * @param aWholeEntry
+   *        Indicates if this is the last visit for this URI.
+   * @param aGUID
+   *        The unique ID associated with the page.
+   * @param aReason
+   *        Indicates the reason for the removal.
+   *        See nsINavHistoryObserver::REASON_* constants.
+   * @param aTransitionType
+   *        If it's a valid TRANSITION_* value, all visits of the specified type
+   *        have been removed.
+   */
+  nsresult NotifyOnPageExpired(nsIURI *aURI, PRTime aVisitTime,
+                               bool aWholeEntry, const nsACString& aGUID,
+                               uint16_t aReason, uint32_t aTransitionType);
+
+  /**
    * These functions return non-owning references to the locale-specific
    * objects for places components.
    */
   nsIStringBundle* GetBundle();
   nsIStringBundle* GetDateFormatBundle();
   nsICollation* GetCollation();
   void GetStringFromName(const char16_t* aName, nsACString& aResult);
   void GetAgeInDaysString(int32_t aInt, const char16_t *aName,
deleted file mode 100644
--- a/toolkit/components/places/nsPIPlacesHistoryListenersNotifier.idl
+++ /dev/null
@@ -1,46 +0,0 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
- * vim: sw=2 ts=2 sts=2 expandtab
- * 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/. */
-
-#include "nsISupports.idl"
-
-interface nsIURI;
-
-/**
- * This is a private interface used by Places components to notify history
- * listeners about important notifications.  These should not be used by any
- * code that is not part of core.
- *
- * @note See also: nsINavHistoryObserver
- */
-[scriptable, uuid(808cf36c-4c9a-4bdb-91a4-d60a6fc25add)]
-interface nsPIPlacesHistoryListenersNotifier : nsISupports
-{
-  /**
-   * Calls onDeleteVisits and onDeleteURI notifications on registered listeners
-   * with the history service.
-   *
-   * @param aURI
-   *        The nsIURI object representing the URI of the page being expired.
-   * @param aVisitTime
-   *        The time, in microseconds, that the page being expired was visited.
-   * @param aWholeEntry
-   *        Indicates if this is the last visit for this URI.
-   * @param aGUID
-   *        The unique ID associated with the page.
-   * @param aReason
-   *        Indicates the reason for the removal.
-   *        See nsINavHistoryObserver::REASON_* constants.
-   * @param aTransitionType
-   *        If it's a valid TRANSITION_* value, all visits of the specified type
-   *        have been removed.
-   */
-  void notifyOnPageExpired(in nsIURI aURI,
-                           in PRTime aVisitTime,
-                           in boolean aWholeEntry,
-                           in ACString aGUID,
-                           in unsigned short aReason,
-                           in unsigned long aTransitionType);
-};
--- a/toolkit/components/places/nsPlacesExpiration.js
+++ b/toolkit/components/places/nsPlacesExpiration.js
@@ -22,16 +22,19 @@
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+  "resource://gre/modules/PlacesUtils.jsm");
+
 ////////////////////////////////////////////////////////////////////////////////
 //// Constants
 
 // Last expiration step should run before the final sync.
 const TOPIC_SHUTDOWN = "places-will-close-connection";
 const TOPIC_PREF_CHANGED = "nsPref:changed";
 const TOPIC_DEBUG_START_EXPIRATION = "places-debug-start-expiration";
 const TOPIC_EXPIRATION_FINISHED = "places-expiration-finished";
@@ -395,16 +398,34 @@ const EXPIRATION_QUERIES = {
   },
   QUERY_ANALYZE_MOZ_INPUTHISTORY: {
     sql: "ANALYZE moz_inputhistory",
     actions: ACTION.TIMED | ACTION.TIMED_OVERLIMIT | ACTION.TIMED_ANALYZE |
              ACTION.CLEAR_HISTORY | ACTION.IDLE_DAILY | ACTION.DEBUG
   },
 };
 
+/**
+ * Sends a bookmarks notification through the given observers.
+ *
+ * @param observers
+ *        array of nsINavBookmarkObserver objects.
+ * @param notification
+ *        the notification name.
+ * @param args
+ *        array of arguments to pass to the notification.
+ */
+function notify(observers, notification, args = []) {
+  for (let observer of observers) {
+    try {
+      observer[notification](...args);
+    } catch (ex) {}
+  }
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 //// nsPlacesExpiration definition
 
 function nsPlacesExpiration()
 {
   //////////////////////////////////////////////////////////////////////////////
   //// Smart Getters
 
@@ -425,19 +446,16 @@ function nsPlacesExpiration()
        , expected_results INTEGER NOT NULL
        )`);
     stmt.executeAsync();
     stmt.finalize();
 
     return db;
   });
 
-  XPCOMUtils.defineLazyServiceGetter(this, "_hsn",
-                                     "@mozilla.org/browser/nav-history-service;1",
-                                     "nsPIPlacesHistoryListenersNotifier");
   XPCOMUtils.defineLazyServiceGetter(this, "_sys",
                                      "@mozilla.org/system-info;1",
                                      "nsIPropertyBag2");
   XPCOMUtils.defineLazyServiceGetter(this, "_idle",
                                      "@mozilla.org/widget/idleservice;1",
                                      "nsIIdleService");
 
   this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
@@ -621,19 +639,25 @@ nsPlacesExpiration.prototype = {
         this._expectedResultsCount = row.getResultByName("expected_results");
       if (this._expectedResultsCount > 0)
         this._expectedResultsCount--;
 
       let uri = Services.io.newURI(row.getResultByName("url"), null, null);
       let guid = row.getResultByName("guid");
       let visitDate = row.getResultByName("visit_date");
       let wholeEntry = row.getResultByName("whole_entry");
+      let reason = Ci.nsINavHistoryObserver.REASON_EXPIRED;
+      let observers = PlacesUtils.history.getObservers();
+
       // Dispatch expiration notifications to history.
-      this._hsn.notifyOnPageExpired(uri, visitDate, wholeEntry, guid,
-                                    Ci.nsINavHistoryObserver.REASON_EXPIRED, 0);
+      if (wholeEntry) {
+        notify(observers, "onDeleteURI", [uri, guid, reason]);
+      } else {
+        notify(observers, "onDeleteVisits", [uri, visitDate, guid, reason, 0]);
+      }
     }
   },
 
   handleError: function PEX_handleError(aError)
   {
     Cu.reportError("Async statement execution returned with '" +
                    aError.result + "', '" + aError.message + "'");
   },
--- a/toolkit/devtools/server/actors/animation.js
+++ b/toolkit/devtools/server/actors/animation.js
@@ -106,16 +106,40 @@ let AnimationPlayerActor = ActorClass({
     if (durationText.indexOf(",") !== -1) {
       durationText = durationText.split(",")[this.playerIndex];
     }
 
     return parseFloat(durationText) * 1000;
   },
 
   /**
+   * Get the animation delay from this player, in milliseconds.
+   * Note that the Web Animations API doesn't yet offer a way to retrieve this
+   * directly from the AnimationPlayer object, so for now, a delay is only
+   * returned if found in the node's computed styles.
+   * @return {Number}
+   */
+  getDelay: function() {
+    let delayText;
+    if (this.styles.animationDelay !== "0s") {
+      delayText = this.styles.animationDelay;
+    } else if (this.styles.transitionDelay !== "0s") {
+      delayText = this.styles.transitionDelay;
+    } else {
+      return 0;
+    }
+
+    if (delayText.indexOf(",") !== -1) {
+      delayText = delayText.split(",")[this.playerIndex];
+    }
+
+    return parseFloat(delayText) * 1000;
+  },
+
+  /**
    * Get the animation iteration count for this player. That is, how many times
    * is the animation scheduled to run.
    * Note that the Web Animations API doesn't yet offer a way to retrieve this
    * directly from the AnimationPlayer object, so for now, check for
    * animationIterationCount in the node's computed styles, and return that.
    * This style property defaults to 1 anyway.
    * @return {Number}
    */
@@ -140,16 +164,17 @@ let AnimationPlayerActor = ActorClass({
        * Return the player's current startTime value.
        * Will be null whenever the animation is paused or waiting to start.
        */
       startTime: this.player.startTime,
       currentTime: this.player.currentTime,
       playState: this.player.playState,
       name: this.player.source.effect.name,
       duration: this.getDuration(),
+      delay: this.getDelay(),
       iterationCount: this.getIterationCount(),
       /**
        * Is the animation currently running on the compositor. This is important for
        * developers to know if their animation is hitting the fast path or not.
        * Currently this will only be true for Firefox OS though (where we have
        * compositor animations enabled).
        * Returns false whenever the animation is paused as it is taken off the
        * compositor then.
@@ -234,16 +259,17 @@ let AnimationPlayerFront = FrontClass(An
    */
   get initialState() {
     return {
       startTime: this._form.startTime,
       currentTime: this._form.currentTime,
       playState: this._form.playState,
       name: this._form.name,
       duration: this._form.duration,
+      delay: this._form.delay,
       iterationCount: this._form.iterationCount,
       isRunningOnCompositor: this._form.isRunningOnCompositor
     }
   },
 
   // About auto-refresh:
   //
   // The AnimationPlayerFront is capable of automatically refreshing its state
--- a/toolkit/devtools/server/tests/browser/animation.html
+++ b/toolkit/devtools/server/tests/browser/animation.html
@@ -1,57 +1,93 @@
 <!DOCTYPE html>
 <style>
   .simple-animation {
     display: inline-block;
 
-    width: 150px;
-    height: 150px;
+    width: 50px;
+    height: 50px;
     border-radius: 50%;
     background: red;
 
     animation: move 2s infinite;
   }
 
   .multiple-animations {
     display: inline-block;
 
-    width: 150px;
-    height: 150px;
+    width: 50px;
+    height: 50px;
     border-radius: 50%;
     background: #eee;
 
     animation: move 2s infinite, glow 1s 5;
   }
 
   .transition {
     display: inline-block;
 
-    width: 150px;
-    height: 150px;
+    width: 50px;
+    height: 50px;
     border-radius: 50%;
     background: #f06;
 
     transition: width 5s;
   }
   .transition.get-round {
     width: 200px;
   }
 
   .short-animation {
     display: inline-block;
 
-    width: 150px;
-    height: 150px;
+    width: 50px;
+    height: 50px;
     border-radius: 50%;
     background: purple;
 
     animation: move 1s;
   }
 
+  .delayed-animation {
+    display: inline-block;
+
+    width: 50px;
+    height: 50px;
+    border-radius: 50%;
+    background: rebeccapurple;
+
+    animation: move 2s 5s infinite;
+  }
+
+  .delayed-transition {
+    display: inline-block;
+
+    width: 50px;
+    height: 50px;
+    border-radius: 50%;
+    background: black;
+
+    transition: width 5s 3s;
+  }
+  .delayed-transition.get-round {
+    width: 200px;
+  }
+
+  .delayed-multiple-animations {
+    display: inline-block;
+
+    width: 50px;
+    height: 50px;
+    border-radius: 50%;
+    background: green;
+
+    animation: move .5s 1s 10, glow 1s .75s 30;
+  }
+
   @keyframes move {
     100% {
       transform: translateY(100px);
     }
   }
 
   @keyframes glow {
     100% {
@@ -59,15 +95,19 @@
     }
   }
 </style>
 <div class="not-animated"></div>
 <div class="simple-animation"></div>
 <div class="multiple-animations"></div>
 <div class="transition"></div>
 <div class="short-animation"></div>
+<div class="delayed-animation"></div>
+<div class="delayed-transition"></div>
+<div class="delayed-multiple-animations"></div>
 <script type="text/javascript">
-  // Get the transition started when the page loads
+  // Get the transitions started when the page loads
   var players;
   addEventListener("load", function() {
     document.querySelector(".transition").classList.add("get-round");
+    document.querySelector(".delayed-transition").classList.add("get-round");
   });
 </script>
--- a/toolkit/devtools/server/tests/browser/browser.ini
+++ b/toolkit/devtools/server/tests/browser/browser.ini
@@ -14,16 +14,17 @@ support-files =
   timeline-iframe-child.html
   timeline-iframe-parent.html
 
 [browser_animation_actors_01.js]
 [browser_animation_actors_02.js]
 [browser_animation_actors_03.js]
 [browser_animation_actors_04.js]
 [browser_animation_actors_05.js]
+[browser_animation_actors_06.js]
 [browser_navigateEvents.js]
 [browser_storage_dynamic_windows.js]
 [browser_storage_listings.js]
 [browser_storage_updates.js]
 [browser_timeline.js]
 skip-if = buildapp == 'mulet'
 [browser_timeline_actors.js]
 skip-if = buildapp == 'mulet'
--- a/toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_03.js
@@ -31,16 +31,17 @@ function* playerHasAnInitialState(walker
   let [player] = yield front.getAnimationPlayersForNode(node);
 
   ok(player.initialState, "The player front has an initial state");
   ok("startTime" in player.initialState, "Player's state has startTime");
   ok("currentTime" in player.initialState, "Player's state has currentTime");
   ok("playState" in player.initialState, "Player's state has playState");
   ok("name" in player.initialState, "Player's state has name");
   ok("duration" in player.initialState, "Player's state has duration");
+  ok("delay" in player.initialState, "Player's state has delay");
   ok("iterationCount" in player.initialState, "Player's state has iterationCount");
   ok("isRunningOnCompositor" in player.initialState, "Player's state has isRunningOnCompositor");
 }
 
 function* playerStateIsCorrect(walker, front) {
   info("Checking the state of the simple animation");
 
   let state = yield getAnimationStateForNode(walker, front, ".simple-animation", 0);
@@ -62,16 +63,26 @@ function* playerStateIsCorrect(walker, f
   info("Checking the state of one of multiple animations on a node");
 
   // Checking the 2nd player
   state = yield getAnimationStateForNode(walker, front, ".multiple-animations", 1);
   is(state.name, "glow", "The 2nd animation's name is correct");
   is(state.duration, 1000, "The 2nd animation's duration is correct");
   is(state.iterationCount, 5, "The 2nd animation's iteration count is correct");
   is(state.playState, "running", "The 2nd animation's playState is correct");
+
+  info("Checking the state of an animation with delay");
+
+  state = yield getAnimationStateForNode(walker, front, ".delayed-animation", 0);
+  is(state.delay, 5000, "The animation delay is correct");
+
+  info("Checking the state of an transition with delay");
+
+  state = yield getAnimationStateForNode(walker, front, ".delayed-transition", 0);
+  is(state.delay, 3000, "The transition delay is correct");
 }
 
 function* getAnimationStateForNode(walker, front, nodeSelector, playerIndex) {
   let node = yield walker.querySelector(walker.rootNode, nodeSelector);
   let players = yield front.getAnimationPlayersForNode(node);
   let player = players[playerIndex];
   yield player.ready();
   let state = yield player.getCurrentState();
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/browser/browser_animation_actors_06.js
@@ -0,0 +1,50 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that the duration, iterationCount and delay are retrieved correctly for
+// multiple animations.
+
+const {AnimationsFront} = require("devtools/server/actors/animation");
+const {InspectorFront} = require("devtools/server/actors/inspector");
+
+add_task(function*() {
+  let doc = yield addTab(MAIN_DOMAIN + "animation.html");
+
+  initDebuggerServer();
+  let client = new DebuggerClient(DebuggerServer.connectPipe());
+  let form = yield connectDebuggerClient(client);
+  let inspector = InspectorFront(client, form);
+  let walker = yield inspector.getWalker();
+  let front = AnimationsFront(client, form);
+
+  yield playerHasAnInitialState(walker, front);
+
+  yield closeDebuggerClient(client);
+  gBrowser.removeCurrentTab();
+});
+
+function* playerHasAnInitialState(walker, front) {
+  let state = yield getAnimationStateForNode(walker, front,
+    ".delayed-multiple-animations", 0);
+  ok(state.duration, 500, "The duration of the first animation is correct");
+  ok(state.iterationCount, 10, "The iterationCount of the first animation is correct");
+  ok(state.delay, 1000, "The delay of the first animation is correct");
+
+  state = yield getAnimationStateForNode(walker, front,
+    ".delayed-multiple-animations", 1);
+  ok(state.duration, 1000, "The duration of the secon animation is correct");
+  ok(state.iterationCount, 30, "The iterationCount of the secon animation is correct");
+  ok(state.delay, 750, "The delay of the secon animation is correct");
+}
+
+function* getAnimationStateForNode(walker, front, nodeSelector, playerIndex) {
+  let node = yield walker.querySelector(walker.rootNode, nodeSelector);
+  let players = yield front.getAnimationPlayersForNode(node);
+  let player = players[playerIndex];
+  yield player.ready();
+  let state = yield player.getCurrentState();
+  return state;
+}