Bug 1345122 - Update NewTabUtils.jsm for Activity Stream's needs draft
authorUrsula Sarracini
Thu, 27 Apr 2017 14:23:08 -0400
changeset 569611 a4437bfc53999877add5066b70ebc6b1d1e09b9f
parent 568509 0f5ba06c4c5959030a05cb852656d854065e2226
child 626265 4dc4e3e8aee1d543f98f37b38e18ca096ef35e1f
push id56238
push userusarracini@mozilla.com
push dateThu, 27 Apr 2017 18:34:43 +0000
bugs1345122
milestone55.0a1
Bug 1345122 - Update NewTabUtils.jsm for Activity Stream's needs MozReview-Commit-ID: 4XWMdtnEuHE
browser/base/content/test/static/browser_all_files_referenced.js
browser/components/newtab/NewTabMessages.jsm
browser/components/newtab/PlacesProvider.jsm
browser/components/newtab/moz.build
browser/components/newtab/tests/xpcshell/test_PlacesProvider.js
browser/components/newtab/tests/xpcshell/xpcshell.ini
browser/components/nsBrowserGlue.js
browser/extensions/activity-stream/lib/TopSitesFeed.jsm
toolkit/components/places/tests/PlacesTestUtils.jsm
toolkit/modules/NewTabUtils.jsm
toolkit/modules/tests/xpcshell/head.js
toolkit/modules/tests/xpcshell/test_FinderIterator.js
toolkit/modules/tests/xpcshell/test_Integration.js
toolkit/modules/tests/xpcshell/test_JSONFile.js
toolkit/modules/tests/xpcshell/test_NewTabUtils.js
toolkit/modules/tests/xpcshell/xpcshell.ini
--- a/browser/base/content/test/static/browser_all_files_referenced.js
+++ b/browser/base/content/test/static/browser_all_files_referenced.js
@@ -71,16 +71,21 @@ var whitelist = new Set([
   {file: "resource://gre/greprefs.js"},
 
   // browser/extensions/pdfjs/content/web/viewer.js
   {file: "resource://pdf.js/build/pdf.worker.js"},
 
   // Add-on API introduced in bug 1118285
   {file: "resource://app/modules/NewTabURL.jsm"},
 
+  // browser/components/newtab bug 1355166
+  {file: "resource://app/modules/NewTabSearchProvider.jsm"},
+  {file: "resource://app/modules/NewTabWebChannel.jsm"},
+  {file: "resource://app/modules/PreviewProvider.jsm"},
+
   // layout/mathml/nsMathMLChar.cpp
   {file: "resource://gre/res/fonts/mathfontSTIXGeneral.properties"},
   {file: "resource://gre/res/fonts/mathfontUnicode.properties"},
 
   // toolkit/components/places/ColorAnalyzer_worker.js
   {file: "resource://gre/modules/ClusterLib.js"},
   {file: "resource://gre/modules/ColorConversion.js"},
 
deleted file mode 100644
--- a/browser/components/newtab/NewTabMessages.jsm
+++ /dev/null
@@ -1,242 +0,0 @@
-/* global
-  NewTabWebChannel,
-  NewTabPrefsProvider,
-  PlacesProvider,
-  PreviewProvider,
-  NewTabSearchProvider,
-  Preferences,
-  XPCOMUtils,
-  Task
-*/
-
-/* exported NewTabMessages */
-
-"use strict";
-
-const {utils: Cu} = Components;
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesProvider",
-                                  "resource:///modules/PlacesProvider.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "PreviewProvider",
-                                  "resource:///modules/PreviewProvider.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NewTabPrefsProvider",
-                                  "resource:///modules/NewTabPrefsProvider.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NewTabSearchProvider",
-                                  "resource:///modules/NewTabSearchProvider.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NewTabWebChannel",
-                                  "resource:///modules/NewTabWebChannel.jsm");
-
-this.EXPORTED_SYMBOLS = ["NewTabMessages"];
-
-const PREF_ENABLED = "browser.newtabpage.remote";
-const CURRENT_ENGINE = "browser-search-engine-modified";
-
-// Action names are from the content's perspective. in from chrome == out from content
-// Maybe replace the ACTION objects by a bi-directional Map a bit later?
-const ACTIONS = {
-  inboundActions: [
-    "REQUEST_PREFS",
-    "REQUEST_THUMB",
-    "REQUEST_FRECENT",
-    "REQUEST_UISTRINGS",
-    "REQUEST_SEARCH_SUGGESTIONS",
-    "REQUEST_MANAGE_ENGINES",
-    "REQUEST_SEARCH_STATE",
-    "REQUEST_REMOVE_FORM_HISTORY",
-    "REQUEST_PERFORM_SEARCH",
-    "REQUEST_CYCLE_ENGINE",
-  ],
-  prefs: {
-    inPrefs: "REQUEST_PREFS",
-    outPrefs: "RECEIVE_PREFS",
-  },
-  preview: {
-    inThumb: "REQUEST_THUMB",
-    outThumb: "RECEIVE_THUMB",
-  },
-  links: {
-    inFrecent: "REQUEST_FRECENT",
-    outFrecent: "RECEIVE_FRECENT",
-    outPlacesChange: "RECEIVE_PLACES_CHANGE",
-  },
-  search: {
-    inSearch: {
-        UIStrings: "REQUEST_UISTRINGS",
-        suggestions: "REQUEST_SEARCH_SUGGESTIONS",
-        manageEngines: "REQUEST_MANAGE_ENGINES",
-        state: "REQUEST_SEARCH_STATE",
-        removeFormHistory: "REQUEST_REMOVE_FORM_HISTORY",
-        performSearch: "REQUEST_PERFORM_SEARCH",
-        cycleEngine: "REQUEST_CYCLE_ENGINE"
-    },
-    outSearch: {
-      UIStrings: "RECEIVE_UISTRINGS",
-      suggestions: "RECEIVE_SEARCH_SUGGESTIONS",
-      state: "RECEIVE_SEARCH_STATE",
-      currentEngine: "RECEIVE_CURRENT_ENGINE"
-    },
-  }
-};
-
-let NewTabMessages = {
-
-  _prefs: {},
-
-  /** NEWTAB EVENT HANDLERS **/
-
-  handleContentRequest(actionName, {data, target}) {
-    switch (actionName) {
-      case ACTIONS.prefs.inPrefs:
-        // Return to the originator all newtabpage prefs
-        let results = NewTabPrefsProvider.prefs.newtabPagePrefs;
-        NewTabWebChannel.send(ACTIONS.prefs.outPrefs, results, target);
-        break;
-      case ACTIONS.preview.inThumb:
-        // Return to the originator a preview URL
-        PreviewProvider.getThumbnail(data).then(imgData => {
-          NewTabWebChannel.send(ACTIONS.preview.outThumb, {url: data, imgData}, target);
-        });
-        break;
-      case ACTIONS.links.inFrecent:
-        // Return to the originator the top frecent links
-        PlacesProvider.links.getLinks().then(links => {
-          NewTabWebChannel.send(ACTIONS.links.outFrecent, links, target);
-        });
-        break;
-      case ACTIONS.search.inSearch.UIStrings:
-        // Return to the originator all search strings to display
-        let strings = NewTabSearchProvider.search.searchSuggestionUIStrings;
-        NewTabWebChannel.send(ACTIONS.search.outSearch.UIStrings, strings, target);
-        break;
-      case ACTIONS.search.inSearch.suggestions:
-        // Return to the originator all search suggestions
-        Task.spawn(function*() {
-          try {
-            let {engineName, searchString} = data;
-            let suggestions = yield NewTabSearchProvider.search.asyncGetSuggestions(engineName, searchString, target);
-            NewTabWebChannel.send(ACTIONS.search.outSearch.suggestions, suggestions, target);
-          } catch (e) {
-            Cu.reportError(e);
-          }
-        });
-        break;
-      case ACTIONS.search.inSearch.manageEngines:
-        // Open about:preferences to manage search state
-        NewTabSearchProvider.search.manageEngines(target.browser);
-        break;
-      case ACTIONS.search.inSearch.state:
-        // Return the state of the search component (i.e current engine and visible engine details)
-        Task.spawn(function*() {
-          try {
-            let state = yield NewTabSearchProvider.search.asyncGetState();
-            NewTabWebChannel.broadcast(ACTIONS.search.outSearch.state, state);
-          } catch (e) {
-            Cu.reportError(e);
-          }
-        });
-        break;
-      case ACTIONS.search.inSearch.removeFormHistory:
-        // Remove a form history entry from the search component
-        let suggestion = data;
-        NewTabSearchProvider.search.removeFormHistory(target, suggestion);
-        break;
-      case ACTIONS.search.inSearch.performSearch:
-        // Perform a search
-        NewTabSearchProvider.search.asyncPerformSearch(target, data).catch(Cu.reportError);
-        break;
-      case ACTIONS.search.inSearch.cycleEngine:
-        // Set the new current engine
-        NewTabSearchProvider.search.asyncCycleEngine(data).catch(Cu.reportError);
-        break;
-    }
-  },
-
-  /*
-   * Broadcast places change to all open newtab pages
-   */
-  handlePlacesChange(type, data) {
-    NewTabWebChannel.broadcast(ACTIONS.links.outPlacesChange, {type, data});
-  },
-
-  /*
-   * Broadcast current engine has changed to all open newtab pages
-   */
-  _handleCurrentEngineChange(name, value) { // jshint unused: false
-    let engine = value;
-    NewTabWebChannel.broadcast(ACTIONS.search.outSearch.currentEngine, engine);
-  },
-
-  /*
-   * Broadcast preference changes to all open newtab pages
-   */
-  handlePrefChange(actionName, value) {
-    let prefChange = {};
-    prefChange[actionName] = value;
-    NewTabWebChannel.broadcast(ACTIONS.prefs.outPrefs, prefChange);
-  },
-
-  _handleEnabledChange(prefName, value) {
-    if (prefName === PREF_ENABLED) {
-      if (this._prefs.enabled && !value) {
-        this.uninit();
-      } else if (!this._prefs.enabled && value) {
-        this.init();
-      }
-    }
-  },
-
-  init() {
-    this.handleContentRequest = this.handleContentRequest.bind(this);
-    this._handleEnabledChange = this._handleEnabledChange.bind(this);
-    this._handleCurrentEngineChange = this._handleCurrentEngineChange.bind(this);
-
-    PlacesProvider.links.init();
-    NewTabPrefsProvider.prefs.init();
-    NewTabSearchProvider.search.init();
-    NewTabWebChannel.init();
-
-    this._prefs.enabled = Preferences.get(PREF_ENABLED, false);
-
-    if (this._prefs.enabled) {
-      for (let action of ACTIONS.inboundActions) {
-        NewTabWebChannel.on(action, this.handleContentRequest);
-      }
-
-      NewTabPrefsProvider.prefs.on(PREF_ENABLED, this._handleEnabledChange);
-      NewTabSearchProvider.search.on(CURRENT_ENGINE, this._handleCurrentEngineChange);
-
-      for (let pref of NewTabPrefsProvider.newtabPagePrefSet) {
-        NewTabPrefsProvider.prefs.on(pref, this.handlePrefChange);
-      }
-
-      PlacesProvider.links.on("deleteURI", this.handlePlacesChange);
-      PlacesProvider.links.on("clearHistory", this.handlePlacesChange);
-      PlacesProvider.links.on("linkChanged", this.handlePlacesChange);
-      PlacesProvider.links.on("manyLinksChanged", this.handlePlacesChange);
-    }
-  },
-
-  uninit() {
-    this._prefs.enabled = Preferences.get(PREF_ENABLED, false);
-
-    if (this._prefs.enabled) {
-      NewTabPrefsProvider.prefs.off(PREF_ENABLED, this._handleEnabledChange);
-      NewTabSearchProvider.search.off(CURRENT_ENGINE, this._handleCurrentEngineChange);
-
-      for (let action of ACTIONS.inboundActions) {
-        NewTabWebChannel.off(action, this.handleContentRequest);
-      }
-
-      for (let pref of NewTabPrefsProvider.newtabPagePrefSet) {
-        NewTabPrefsProvider.prefs.off(pref, this.handlePrefChange);
-      }
-    }
-
-    NewTabPrefsProvider.prefs.uninit();
-    NewTabSearchProvider.search.uninit();
-    NewTabWebChannel.uninit();
-  }
-};
deleted file mode 100644
--- a/browser/components/newtab/PlacesProvider.jsm
+++ /dev/null
@@ -1,244 +0,0 @@
-/* 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/. */
-
-/* global XPCOMUtils, Services, PlacesUtils, EventEmitter */
-/* global gLinks */
-/* exported PlacesProvider */
-
-"use strict";
-
-this.EXPORTED_SYMBOLS = ["PlacesProvider"];
-
-const {interfaces: Ci, utils: Cu} = Components;
-Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
-  "resource://gre/modules/PlacesUtils.jsm");
-
-XPCOMUtils.defineLazyGetter(this, "EventEmitter", function() {
-  const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
-  return EventEmitter;
-});
-
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
-                                  "resource://gre/modules/Task.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
-                                  "resource://gre/modules/NewTabUtils.jsm");
-
-// The maximum number of results PlacesProvider retrieves from history.
-const HISTORY_RESULTS_LIMIT = 100;
-
-/* Queries history to retrieve the most visited sites. Emits events when the
- * history changes.
- * Implements the EventEmitter interface.
- */
-let Links = function Links() {
-  EventEmitter.decorate(this);
-};
-
-Links.prototype = {
-  /**
-   * Set this to change the maximum number of links the provider will provide.
-   */
-  get maxNumLinks() {
-    // getter, so it can't be replaced dynamically
-    return HISTORY_RESULTS_LIMIT;
-  },
-
-  /**
-   * A set of functions called by @mozilla.org/browser/nav-historyservice
-   * All history events are emitted from this object.
-   */
-  historyObserver: {
-    _batchProcessingDepth: 0,
-    _batchCalledFrecencyChanged: false,
-
-    /**
-     * Called by the history service.
-     */
-    onBeginUpdateBatch() {
-      this._batchProcessingDepth += 1;
-    },
-
-    onEndUpdateBatch() {
-      this._batchProcessingDepth -= 1;
-      if (this._batchProcessingDepth == 0 && this._batchCalledFrecencyChanged) {
-        this.onManyFrecenciesChanged();
-        this._batchCalledFrecencyChanged = false;
-      }
-    },
-
-    onDeleteURI: function historyObserver_onDeleteURI(aURI) {
-      // let observers remove sensetive data associated with deleted visit
-      gLinks.emit("deleteURI", {
-        url: aURI.spec,
-      });
-    },
-
-    onClearHistory: function historyObserver_onClearHistory() {
-      gLinks.emit("clearHistory");
-    },
-
-    onFrecencyChanged: function historyObserver_onFrecencyChanged(aURI,
-                           aNewFrecency, aGUID, aHidden, aLastVisitDate) { // jshint ignore:line
-
-      // If something is doing a batch update of history entries we don't want
-      // to do lots of work for each record. So we just track the fact we need
-      // to call onManyFrecenciesChanged() once the batch is complete.
-      if (this._batchProcessingDepth > 0) {
-        this._batchCalledFrecencyChanged = true;
-        return;
-      }
-
-      // The implementation of the query in getLinks excludes hidden and
-      // unvisited pages, so it's important to exclude them here, too.
-      if (!aHidden && aLastVisitDate &&
-          NewTabUtils.linkChecker.checkLoadURI(aURI.spec)) {
-        gLinks.emit("linkChanged", {
-          url: aURI.spec,
-          frecency: aNewFrecency,
-          lastVisitDate: aLastVisitDate,
-          type: "history",
-        });
-      }
-    },
-
-    onManyFrecenciesChanged: function historyObserver_onManyFrecenciesChanged() {
-      // Called when frecencies are invalidated and also when clearHistory is called
-      // See toolkit/components/places/tests/unit/test_frecency_observers.js
-      gLinks.emit("manyLinksChanged");
-    },
-
-    onVisit(aURI, aVisitId, aTime, aSessionId, aReferrerVisitId, aTransitionType,
-            aGuid, aHidden, aVisitCount, aTyped, aLastKnownTitle) {
-      // For new visits, if we're not batch processing, notify for a title update
-      if (!this._batchProcessingDepth && aVisitCount == 1 && aLastKnownTitle) {
-        this.onTitleChanged(aURI, aLastKnownTitle, aGuid);
-      }
-    },
-
-    onTitleChanged: function historyObserver_onTitleChanged(aURI, aNewTitle) {
-      if (NewTabUtils.linkChecker.checkLoadURI(aURI.spec)) {
-        gLinks.emit("linkChanged", {
-          url: aURI.spec,
-          title: aNewTitle
-        });
-      }
-    },
-
-    QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
-                                           Ci.nsISupportsWeakReference])
-  },
-
-  /**
-   * Must be called before the provider is used.
-   * Makes it easy to disable under pref
-   */
-  init: function PlacesProvider_init() {
-    try {
-      PlacesUtils.history.addObserver(this.historyObserver, true);
-    } catch (e) {
-      Cu.reportError(e);
-    }
-  },
-
-  /**
-   * Gets the current set of links delivered by this provider.
-   *
-   * @returns {Promise} Returns a promise with the array of links as payload.
-   */
-  getLinks: Task.async(function*() {
-    // Select a single page per host with highest frecency, highest recency.
-    // Choose N top such pages. Note +rev_host, to turn off optimizer per :mak
-    // suggestion.
-    let sqlQuery = `SELECT url, title, frecency,
-                          last_visit_date as lastVisitDate,
-                          "history" as type
-                   FROM moz_places
-                   WHERE frecency in (
-                     SELECT MAX(frecency) as frecency
-                     FROM moz_places
-                     WHERE hidden = 0 AND last_visit_date NOTNULL
-                     GROUP BY +rev_host
-                     ORDER BY frecency DESC
-                     LIMIT :limit
-                   )
-                   GROUP BY rev_host HAVING MAX(lastVisitDate)
-                   ORDER BY frecency DESC, lastVisitDate DESC, url`;
-
-    let links = yield this.executePlacesQuery(sqlQuery, {
-                  columns: ["url", "title", "lastVisitDate", "frecency", "type"],
-                  params: {limit: this.maxNumLinks}
-                });
-
-    return links.filter(link => NewTabUtils.linkChecker.checkLoadURI(link.url));
-  }),
-
-  /**
-   * Executes arbitrary query against places database
-   *
-   * @param {String} aSql
-   *        SQL query to execute
-   * @param {Object} [optional] aOptions
-   *        aOptions.columns - an array of column names. if supplied the returned
-   *        items will consist of objects keyed on column names. Otherwise
-   *        an array of raw values is returned in the select order
-   *        aOptions.param - an object of SQL binding parameters
-   *        aOptions.callback - a callback to handle query rows
-   *
-   * @returns {Promise} Returns a promise with the array of retrieved items
-   */
-  executePlacesQuery: Task.async(function*(aSql, aOptions = {}) {
-    let {columns, params, callback} = aOptions;
-    let items = [];
-    let queryError = null;
-    let conn = yield PlacesUtils.promiseDBConnection();
-    yield conn.executeCached(aSql, params, aRow => {
-      try {
-        // check if caller wants to handle query raws
-        if (callback) {
-          callback(aRow);
-        } else {
-          // otherwise fill in the item and add items array
-          let item = null;
-          // if columns array is given construct an object
-          if (columns && Array.isArray(columns)) {
-            item = {};
-            columns.forEach(column => {
-              item[column] = aRow.getResultByName(column);
-            });
-          } else {
-            // if no columns - make an array of raw values
-            item = [];
-            for (let i = 0; i < aRow.numEntries; i++) {
-              item.push(aRow.getResultByIndex(i));
-            }
-          }
-          items.push(item);
-        }
-      } catch (e) {
-        queryError = e;
-        throw StopIteration;
-      }
-    });
-    if (queryError) {
-      throw new Error(queryError);
-    }
-    return items;
-  }),
-};
-
-/**
- * Singleton that serves as the default link provider for the grid.
- */
-const gLinks = new Links(); // jshint ignore:line
-
-let PlacesProvider = {
-  links: gLinks,
-};
-
-// Kept only for backwards-compatibility
-XPCOMUtils.defineLazyGetter(PlacesProvider, "LinkChecker",
-  () => NewTabUtils.linkChecker);
--- a/browser/components/newtab/moz.build
+++ b/browser/components/newtab/moz.build
@@ -9,23 +9,21 @@ with Files("**"):
 
 BROWSER_CHROME_MANIFESTS += ['tests/browser/browser.ini']
 
 XPCSHELL_TESTS_MANIFESTS += [
     'tests/xpcshell/xpcshell.ini',
 ]
 
 EXTRA_JS_MODULES += [
-    'NewTabMessages.jsm',
     'NewTabPrefsProvider.jsm',
     'NewTabRemoteResources.jsm',
     'NewTabSearchProvider.jsm',
     'NewTabURL.jsm',
     'NewTabWebChannel.jsm',
-    'PlacesProvider.jsm',
     'PreviewProvider.jsm'
 ]
 
 XPIDL_SOURCES += [
     'nsIAboutNewTabService.idl',
 ]
 
 XPIDL_MODULE = 'browser-newtab'
deleted file mode 100644
--- a/browser/components/newtab/tests/xpcshell/test_PlacesProvider.js
+++ /dev/null
@@ -1,357 +0,0 @@
-"use strict";
-
-/* global XPCOMUtils, PlacesUtils, PlacesTestUtils, PlacesProvider, NetUtil */
-/* global do_get_profile, run_next_test, add_task */
-/* global equal, ok */
-
-const {
-  utils: Cu,
-  interfaces: Ci,
-} = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesProvider",
-    "resource:///modules/PlacesProvider.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
-    "resource://gre/modules/PlacesUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
-    "resource://testing-common/PlacesTestUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
-    "resource://gre/modules/NetUtil.jsm");
-
-// ensure a profile exists
-do_get_profile();
-
-function run_test() {
-  PlacesProvider.links.init();
-  run_next_test();
-}
-
-// url prefix for test history population
-const TEST_URL = "https://mozilla.com/";
-// time when the test starts execution
-const TIME_NOW = new Date();
-
-// utility function to compute past timestap
-function timeDaysAgo(numDays) {
-  return new Date(TIME_NOW - (numDays * 24 * 60 * 60 * 1000));
-}
-
-// utility function to make a visit for insetion into places db
-function makeVisit(index, daysAgo, isTyped, domain = TEST_URL) {
-  let {
-    TRANSITION_TYPED,
-    TRANSITION_LINK
-  } = PlacesUtils.history;
-
-  return {
-    uri: NetUtil.newURI(`${domain}${index}`),
-    visitDate: timeDaysAgo(daysAgo),
-    transition: (isTyped) ? TRANSITION_TYPED : TRANSITION_LINK,
-  };
-}
-
-/** Test LinkChecker **/
-
-add_task(function test_LinkChecker_securityCheck() {
-
-  let urls = [
-    {url: "javascript:alert('hello')", expected: false}, // jshint ignore:line
-    {url: "data:image/png;base64,XXX", expected: false},
-    {url: "about:newtab", expected: true},
-    {url: "https://example.com", expected: true},
-    {url: "ftp://example.com", expected: true},
-    {url: "file://home/file/image.png", expected: true},
-    {url: "resource:///modules/PlacesProvider.jsm", expected: true},
-  ];
-  for (let {url, expected} of urls) {
-    let observed = PlacesProvider.LinkChecker.checkLoadURI(url);
-    equal(observed, expected, `can load "${url}"?`);
-  }
-});
-
-/** Test Provider **/
-
-add_task(function* test_Links_getLinks() {
-  yield PlacesTestUtils.clearHistory();
-  let provider = PlacesProvider.links;
-
-  let links = yield provider.getLinks();
-  equal(links.length, 0, "empty history yields empty links");
-
-  // add a visit
-  let testURI = NetUtil.newURI("http://mozilla.com");
-  yield PlacesTestUtils.addVisits(testURI);
-
-  links = yield provider.getLinks();
-  equal(links.length, 1, "adding a visit yields a link");
-  equal(links[0].url, testURI.spec, "added visit corresponds to added url");
-});
-
-add_task(function* test_Links_getLinks_Order() {
-  yield PlacesTestUtils.clearHistory();
-  let provider = PlacesProvider.links;
-
-  // all four visits must come from different domains to avoid deduplication
-  let visits = [
-    makeVisit(0, 0, true, "http://bar.com/"), // frecency 200, today
-    makeVisit(1, 0, true, "http://foo.com/"), // frecency 200, today
-    makeVisit(2, 2, true, "http://buz.com/"), // frecency 200, 2 days ago
-    makeVisit(3, 2, false, "http://aaa.com/"), // frecency 10, 2 days ago, transition
-  ];
-
-  let links = yield provider.getLinks();
-  equal(links.length, 0, "empty history yields empty links");
-  yield PlacesTestUtils.addVisits(visits);
-
-  links = yield provider.getLinks();
-  equal(links.length, visits.length, "number of links added is the same as obtain by getLinks");
-  for (let i = 0; i < links.length; i++) {
-    equal(links[i].url, visits[i].uri.spec, "links are obtained in the expected order");
-  }
-});
-
-add_task(function* test_Links_getLinks_Deduplication() {
-  yield PlacesTestUtils.clearHistory();
-  let provider = PlacesProvider.links;
-
-  // all for visits must come from different domains to avoid deduplication
-  let visits = [
-    makeVisit(0, 2, true, "http://bar.com/"), // frecency 200, 2 days ago
-    makeVisit(1, 0, true, "http://bar.com/"), // frecency 200, today
-    makeVisit(2, 0, false, "http://foo.com/"), // frecency 10, today
-    makeVisit(3, 0, true, "http://foo.com/"), // frecency 200, today
-  ];
-
-  let links = yield provider.getLinks();
-  equal(links.length, 0, "empty history yields empty links");
-  yield PlacesTestUtils.addVisits(visits);
-
-  links = yield provider.getLinks();
-  equal(links.length, 2, "only two links must be left after deduplication");
-  equal(links[0].url, visits[1].uri.spec, "earliest link is present");
-  equal(links[1].url, visits[3].uri.spec, "most fresent link is present");
-});
-
-add_task(function* test_Links_onLinkChanged() {
-  let provider = PlacesProvider.links;
-
-  let url = "https://example.com/onFrecencyChanged1";
-  let linkChangedMsgCount = 0;
-
-  let linkChangedPromise = new Promise(resolve => {
-    let handler = (_, link) => { // jshint ignore:line
-      /* There are 2 linkChanged events:
-       * 1. visit insertion (-1 frecency by default)
-       * 2. frecency score update (after transition type calculation etc)
-       */
-      if (link.url === url) {
-        equal(link.url, url, `expected url on linkChanged event`);
-        linkChangedMsgCount += 1;
-        if (linkChangedMsgCount === 2) {
-          ok(true, `all linkChanged events captured`);
-          provider.off("linkChanged", this);
-          resolve();
-        }
-      }
-    };
-    provider.on("linkChanged", handler);
-  });
-
-  // add a visit
-  let testURI = NetUtil.newURI(url);
-  yield PlacesUtils.history.insert({
-    url: testURI,
-    visits: [{ transition: PlacesUtils.history.TRANSITIONS.LINK }]
-  });
-  yield linkChangedPromise;
-
-  yield PlacesTestUtils.clearHistory();
-});
-
-add_task(function* test_Links_onClearHistory() {
-  let provider = PlacesProvider.links;
-
-  let clearHistoryPromise = new Promise(resolve => {
-    let handler = () => {
-      ok(true, `clearHistory event captured`);
-      provider.off("clearHistory", handler);
-      resolve();
-    };
-    provider.on("clearHistory", handler);
-  });
-
-  // add visits
-  for (let i = 0; i <= 10; i++) {
-    let url = `https://example.com/onClearHistory${i}`;
-    let testURI = NetUtil.newURI(url);
-    yield PlacesTestUtils.addVisits(testURI);
-  }
-  yield PlacesTestUtils.clearHistory();
-  yield clearHistoryPromise;
-});
-
-add_task(function* test_Links_onDeleteURI() {
-  let provider = PlacesProvider.links;
-
-  let testURL = "https://example.com/toDelete";
-
-  let deleteURIPromise = new Promise(resolve => {
-    let handler = (_, {url}) => { // jshint ignore:line
-      equal(testURL, url, "deleted url and expected url are the same");
-      provider.off("deleteURI", handler);
-      resolve();
-    };
-
-    provider.on("deleteURI", handler);
-  });
-
-  let testURI = NetUtil.newURI(testURL);
-  yield PlacesTestUtils.addVisits(testURI);
-  yield PlacesUtils.history.remove(testURL);
-  yield deleteURIPromise;
-});
-
-add_task(function* test_Links_onManyLinksChanged() {
-  let provider = PlacesProvider.links;
-
-  let promise = new Promise(resolve => {
-    let handler = () => {
-      ok(true);
-      provider.off("manyLinksChanged", handler);
-      resolve();
-    };
-
-    provider.on("manyLinksChanged", handler);
-  });
-
-  let testURL = "https://example.com/toDelete";
-  let testURI = NetUtil.newURI(testURL);
-  yield PlacesTestUtils.addVisits(testURI);
-
-  // trigger DecayFrecency
-  PlacesUtils.history.QueryInterface(Ci.nsIObserver).
-    observe(null, "idle-daily", "");
-
-  yield promise;
-});
-
-add_task(function* test_Links_execute_query() {
-  yield PlacesTestUtils.clearHistory();
-  let provider = PlacesProvider.links;
-
-  let visits = [
-    makeVisit(0, 0, true), // frecency 200, today
-    makeVisit(1, 0, true), // frecency 200, today
-    makeVisit(2, 2, true), // frecency 200, 2 days ago
-    makeVisit(3, 2, false), // frecency 10, 2 days ago, transition
-  ];
-
-  yield PlacesTestUtils.addVisits(visits);
-
-  function testItemValue(results, index, value) {
-    equal(results[index][0], `${TEST_URL}${value}`, "raw url");
-    equal(results[index][1], `test visit for ${TEST_URL}${value}`, "raw title");
-  }
-
-  function testItemObject(results, index, columnValues) {
-    Object.keys(columnValues).forEach(name => {
-      equal(results[index][name], columnValues[name], "object name " + name);
-    });
-  }
-
-  // select all 4 records
-  let results = yield provider.executePlacesQuery("select url, title from moz_places");
-  equal(results.length, 4, "expect 4 items");
-  // check for insert order sequence
-  for (let i = 0; i < results.length; i++) {
-    testItemValue(results, i, i);
-  }
-
-  // test parameter passing
-  results = yield provider.executePlacesQuery(
-              "select url, title from moz_places limit :limit",
-              {params: {limit: 2}}
-            );
-  equal(results.length, 2, "expect 2 items");
-  for (let i = 0; i < results.length; i++) {
-    testItemValue(results, i, i);
-  }
-
-  // test extracting items by name
-  results = yield provider.executePlacesQuery(
-              "select url, title from moz_places limit :limit",
-              {columns: ["url", "title"], params: {limit: 4}}
-            );
-  equal(results.length, 4, "expect 4 items");
-  for (let i = 0; i < results.length; i++) {
-    testItemObject(results, i, {
-      "url": `${TEST_URL}${i}`,
-      "title": `test visit for ${TEST_URL}${i}`,
-    });
-  }
-
-  // test ordering
-  results = yield provider.executePlacesQuery(
-              "select url, title, last_visit_date, frecency from moz_places " +
-              "order by frecency DESC, last_visit_date DESC, url DESC limit :limit",
-              {columns: ["url", "title", "last_visit_date", "frecency"], params: {limit: 4}}
-            );
-  equal(results.length, 4, "expect 4 items");
-  testItemObject(results, 0, {url: `${TEST_URL}1`});
-  testItemObject(results, 1, {url: `${TEST_URL}0`});
-  testItemObject(results, 2, {url: `${TEST_URL}2`});
-  testItemObject(results, 3, {url: `${TEST_URL}3`});
-
-  // test callback passing
-  results = [];
-  function handleRow(aRow) {
-    results.push({
-      url:              aRow.getResultByName("url"),
-      title:            aRow.getResultByName("title"),
-      last_visit_date:  aRow.getResultByName("last_visit_date"),
-      frecency:         aRow.getResultByName("frecency")
-    });
-  }
-  yield provider.executePlacesQuery(
-        "select url, title, last_visit_date, frecency from moz_places " +
-        "order by frecency DESC, last_visit_date DESC, url DESC",
-        {callback: handleRow}
-      );
-  equal(results.length, 4, "expect 4 items");
-  testItemObject(results, 0, {url: `${TEST_URL}1`});
-  testItemObject(results, 1, {url: `${TEST_URL}0`});
-  testItemObject(results, 2, {url: `${TEST_URL}2`});
-  testItemObject(results, 3, {url: `${TEST_URL}3`});
-
-  // negative test cases
-  // bad sql
-  try {
-    yield provider.executePlacesQuery("select from moz");
-    do_throw("bad sql should've thrown");
-  } catch (e) {
-    do_check_true("expected failure - bad sql");
-  }
-  // missing bindings
-  try {
-    yield provider.executePlacesQuery("select * from moz_places limit :limit");
-    do_throw("bad sql should've thrown");
-  } catch (e) {
-    do_check_true("expected failure - missing bidning");
-  }
-  // non-existent column name
-  try {
-    yield provider.executePlacesQuery("select * from moz_places limit :limit",
-                                     {columns: ["no-such-column"], params: {limit: 4}});
-    do_throw("bad sql should've thrown");
-  } catch (e) {
-    do_check_true("expected failure - wrong column name");
-  }
-
-  // cleanup
-  yield PlacesTestUtils.clearHistory();
-});
--- a/browser/components/newtab/tests/xpcshell/xpcshell.ini
+++ b/browser/components/newtab/tests/xpcshell/xpcshell.ini
@@ -2,9 +2,8 @@
 head =
 firefox-appdir = browser
 skip-if = toolkit == 'android'
 
 [test_AboutNewTabService.js]
 [test_NewTabPrefsProvider.js]
 [test_NewTabSearchProvider.js]
 [test_NewTabURL.js]
-[test_PlacesProvider.js]
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -22,17 +22,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
 /* global AboutHome:false, AboutNewTab:false, AddonManager:false,
           AsyncShutdown:false, AutoCompletePopup:false, BookmarkHTMLUtils:false,
           BookmarkJSONUtils:false, BrowserUITelemetry:false, BrowserUsageTelemetry:false,
           ContentClick:false, ContentPrefServiceParent:false, ContentSearch:false,
           DateTimePickerHelper:false, DirectoryLinksProvider:false,
           ExtensionsUI:false, Feeds:false,
           FileUtils:false, FormValidationHandler:false, Integration:false,
           LightweightThemeManager:false, LoginHelper:false, LoginManagerParent:false,
-          NetUtil:false, NewTabMessages:false, NewTabUtils:false, OS:false,
+          NetUtil:false, NewTabUtils:false, OS:false,
           PageThumbs:false, PdfJs:false, PermissionUI:false, PlacesBackups:false,
           PlacesUtils:false, PluralForm:false, PrivateBrowsingUtils:false,
           ProcessHangMonitor:false, ReaderParent:false, RecentWindow:false,
           RemotePrompt:false, SelfSupportBackend:false, SessionStore:false,
           ShellService:false, SimpleServiceDiscovery:false, TabCrashHandler:false,
           Task:false, UITour:false, WebChannel:false,
           WindowsRegistry:false, webrtcUI:false, UserAgentOverrides: false */
 
@@ -60,17 +60,16 @@ XPCOMUtils.defineLazyServiceGetter(this,
   ["Feeds", "resource:///modules/Feeds.jsm"],
   ["FileUtils", "resource://gre/modules/FileUtils.jsm"],
   ["FormValidationHandler", "resource:///modules/FormValidationHandler.jsm"],
   ["Integration", "resource://gre/modules/Integration.jsm"],
   ["LightweightThemeManager", "resource://gre/modules/LightweightThemeManager.jsm"],
   ["LoginHelper", "resource://gre/modules/LoginHelper.jsm"],
   ["LoginManagerParent", "resource://gre/modules/LoginManagerParent.jsm"],
   ["NetUtil", "resource://gre/modules/NetUtil.jsm"],
-  ["NewTabMessages", "resource:///modules/NewTabMessages.jsm"],
   ["NewTabUtils", "resource://gre/modules/NewTabUtils.jsm"],
   ["OS", "resource://gre/modules/osfile.jsm"],
   ["PageThumbs", "resource://gre/modules/PageThumbs.jsm"],
   ["PdfJs", "resource://pdf.js/PdfJs.jsm"],
   ["PermissionUI", "resource:///modules/PermissionUI.jsm"],
   ["PlacesBackups", "resource://gre/modules/PlacesBackups.jsm"],
   ["PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"],
   ["PluralForm", "resource://gre/modules/PluralForm.jsm"],
@@ -504,18 +503,16 @@ BrowserGlue.prototype = {
     webrtcUI.init();
     AboutHome.init();
 
     DirectoryLinksProvider.init();
     NewTabUtils.init();
     NewTabUtils.links.addProvider(DirectoryLinksProvider);
     AboutNewTab.init();
 
-    NewTabMessages.init();
-
     SessionStore.init();
     BrowserUsageTelemetry.init();
     BrowserUITelemetry.init();
     ContentSearch.init();
     FormValidationHandler.init();
 
     ContentClick.init();
     RemotePrompt.init();
@@ -894,18 +891,18 @@ BrowserGlue.prototype = {
     if (this._bookmarksBackupIdleTime) {
       this._idleService.removeIdleObserver(this, this._bookmarksBackupIdleTime);
       delete this._bookmarksBackupIdleTime;
     }
 
     BrowserUsageTelemetry.uninit();
     SelfSupportBackend.uninit();
     PageThumbs.uninit();
-    NewTabMessages.uninit();
     AboutNewTab.uninit();
+    NewTabUtils.uninit();
     webrtcUI.uninit();
     FormValidationHandler.uninit();
     AutoCompletePopup.uninit();
     DateTimePickerHelper.uninit();
   },
 
   _initServiceDiscovery() {
     if (!Services.prefs.getBoolPref("browser.casting.enabled")) {
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -2,17 +2,17 @@
  * 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/. */
  /* globals PlacesProvider, PreviewProvider */
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
-Cu.import("resource:///modules/PlacesProvider.jsm");
+Cu.import("resource://gre/modules/NewTabUtils.jsm");
 Cu.import("resource:///modules/PreviewProvider.jsm");
 
 const TOP_SITES_SHOWMORE_LENGTH = 12;
 const UPDATE_TIME = 15 * 60 * 1000; // 15 minutes
 const DEFAULT_TOP_SITES = [
   {"url": "https://www.facebook.com/"},
   {"url": "https://www.youtube.com/"},
   {"url": "http://www.amazon.com/"},
@@ -26,17 +26,17 @@ this.TopSitesFeed = class TopSitesFeed {
     this.lastUpdated = 0;
   }
   async getScreenshot(url) {
     let screenshot = await PreviewProvider.getThumbnail(url);
     const action = {type: at.SCREENSHOT_UPDATED, data: {url, screenshot}};
     this.store.dispatch(ac.BroadcastToContent(action));
   }
   async getLinksWithDefaults(action) {
-    let links = await PlacesProvider.links.getLinks();
+    let links = await NewTabUtils.activityStreamLinks.getTopSites();
 
     if (!links) {
       links = [];
     } else {
       links = links.filter(link => link && link.type !== "affiliate").slice(0, 12);
     }
 
     if (links.length < TOP_SITES_SHOWMORE_LENGTH) {
--- a/toolkit/components/places/tests/PlacesTestUtils.jsm
+++ b/toolkit/components/places/tests/PlacesTestUtils.jsm
@@ -82,16 +82,53 @@ this.PlacesTestUtils = Object.freeze({
         date: visitDate,
         referrer: place.referrer
       }];
       infos.push(info);
     }
     return PlacesUtils.history.insertMany(infos);
   }),
 
+   /*
+    * Add Favicons
+    *
+    * @param {Map} faviconURLs  keys are page URLs, values are their
+    *                           associated favicon URLs.
+    */
+
+  addFavicons: Task.async(function*(faviconURLs) {
+    let faviconPromises = [];
+
+    // If no favicons were provided, we do not want to continue on
+    if (!faviconURLs) {
+      throw new Error("No favicon URLs were provided");
+    }
+    for (let [key, val] of faviconURLs) {
+      if (!val) {
+        throw new Error("URL does not exist");
+      }
+      faviconPromises.push(new Promise((resolve, reject) => {
+        let uri = NetUtil.newURI(key);
+        let faviconURI = NetUtil.newURI(val);
+        try {
+          PlacesUtils.favicons.setAndFetchFaviconForPage(
+            uri,
+            faviconURI,
+            false,
+            PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+            resolve,
+            Services.scriptSecurityManager.getSystemPrincipal());
+        } catch (ex) {
+          reject(ex);
+        }
+      }));
+    }
+    yield Promise.all(faviconPromises);
+  }),
+
   /**
    * Clear all history.
    *
    * @return {Promise}
    * @resolves When history was cleared successfully.
    * @rejects JavaScript exception.
    */
   clearHistory() {
--- a/toolkit/modules/NewTabUtils.jsm
+++ b/toolkit/modules/NewTabUtils.jsm
@@ -8,16 +8,17 @@ this.EXPORTED_SYMBOLS = ["NewTabUtils"];
 
 const Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
+Cu.importGlobalProperties(["btoa"]);
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
   "resource://gre/modules/PlacesUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PageThumbs",
   "resource://gre/modules/PageThumbs.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "BinarySearch",
@@ -48,16 +49,22 @@ const PREF_NEWTAB_COLUMNS = "browser.new
 const HISTORY_RESULTS_LIMIT = 100;
 
 // The maximum number of links Links.getLinks will return.
 const LINKS_GET_LINKS_LIMIT = 100;
 
 // The gather telemetry topic.
 const TOPIC_GATHER_TELEMETRY = "gather-telemetry";
 
+// The number of top sites to display on Activity Stream page
+const TOP_SITES_LENGTH = 6;
+
+// Use double the number to allow for immediate display when blocking sites
+const TOP_SITES_LIMIT = TOP_SITES_LENGTH * 2;
+
 /**
  * Calculate the MD5 hash for a string.
  * @param aValue
  *        The string to convert.
  * @return The base64 representation of the MD5 hash.
  */
 function toHash(aValue) {
   let value = gUnicodeConverter.convertToByteArray(aValue);
@@ -343,16 +350,24 @@ var GridPrefs = {
   /**
    * Initializes object. Adds a preference observer
    */
   init: function GridPrefs_init() {
     Services.prefs.addObserver(PREF_NEWTAB_ROWS, this);
     Services.prefs.addObserver(PREF_NEWTAB_COLUMNS, this);
   },
 
+ /**
+  * Uninitializes object. Removes the preference observers
+  */
+  uninit: function GridPrefs_uninit() {
+    Services.prefs.removeObserver(PREF_NEWTAB_ROWS, this);
+    Services.prefs.removeObserver(PREF_NEWTAB_COLUMNS, this);
+  },
+
   /**
    * Implements the nsIObserver interface to get notified when the preference
    * value changes.
    */
   observe: function GridPrefs_observe(aSubject, aTopic, aData) {
     if (aData == PREF_NEWTAB_ROWS) {
       this._gridRows = null;
     } else {
@@ -507,16 +522,23 @@ var BlockedLinks = {
   /**
    * Registers an object that will be notified when the blocked links change.
    */
   addObserver(aObserver) {
     this._observers.push(aObserver);
   },
 
   /**
+   * Remove the observers.
+   */
+  removeObservers() {
+    this._observers = [];
+  },
+
+  /**
    * The list of blocked links.
    */
   get links() {
     if (!this._links)
       this._links = Storage.get("blockedLinks", {});
 
     return this._links;
   },
@@ -796,16 +818,419 @@ var PlacesProvider = {
     }
   },
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
                                          Ci.nsISupportsWeakReference]),
 };
 
 /**
+ * Queries history to retrieve the most frecent sites. Emits events when the
+ * history changes.
+ */
+var ActivityStreamProvider = {
+
+  /**
+   * Process links after getting them from the database.
+   *
+   * @param {Array} aLinks
+   *          an array containing link objects
+   *
+   * @returns {Array} an array of checked links with favicons and eTLDs added
+   */
+  _processLinks(aLinks) {
+    let links_ = aLinks.filter(link => LinkChecker.checkLoadURI(link.url));
+    links_ = this._faviconBytesToDataURI(links_);
+    return this._addETLD(links_);
+  },
+
+  /**
+   * From an Array of links, if favicons are present, convert to data URIs
+   *
+   * @param {Array} aLinks
+   *          an array containing objects with favicon data and mimeTypes
+   *
+   * @returns {Array} an array of links with favicons as data uri
+   */
+  _faviconBytesToDataURI(aLinks) {
+    return aLinks.map(link => {
+      if (link.favicon) {
+        let encodedData = btoa(String.fromCharCode.apply(null, link.favicon));
+        link.favicon = `data:${link.mimeType};base64,${encodedData}`;
+      }
+      delete link.mimeType;
+      return link;
+    });
+  },
+
+  /**
+   * Computes favicon data for each url in a set of links
+   *
+   * @param {Array} links
+   *          an array containing objects without favicon data or mimeTypes yet
+   *
+   * @returns {Promise} Returns a promise with the array of links with favicon data,
+   *                    mimeType, and byte array length
+   */
+  _addFavicons: Task.async(function*(aLinks) {
+    if (aLinks.length) {
+      // Each link in the array needs a favicon for it's page - so we fire off
+      // a promise for each link to compute the favicon data and attach it back
+      // to the original link object. We must wait until all favicons for
+      // the array of links are computed before returning
+      yield Promise.all(aLinks.map(link => new Promise(resolve => {
+        return PlacesUtils.favicons.getFaviconDataForPage(
+            Services.io.newURI(link.url),
+            (iconuri, len, data, mime) => {
+              // Due to the asynchronous behaviour of inserting a favicon into
+              // moz_favicons, the data may not be available to us just yet,
+              // since we listen on a history entry being inserted. As a result,
+              // we don't want to throw if the icon uri is not here yet, we
+              // just want to resolve on an empty favicon. Activity Stream
+              // knows how to handle null favicons
+              if (!iconuri) {
+                link.favicon = null;
+                link.mimeType = null;
+              } else {
+                link.favicon = data;
+                link.mimeType = mime;
+                link.faviconLength = len;
+              }
+              return resolve(link);
+            });
+        }).catch(() => {
+          // If something goes wrong - that's ok - just return a null favicon
+          // without rejecting the entire Promise.all
+          link.favicon = null;
+          link.mimeType = null;
+          return link;
+        })
+      ));
+    }
+    return aLinks;
+  }),
+
+  /**
+   * Add the eTLD to each link in the array of links.
+   *
+   * @param {Array} aLinks
+   *          an array containing objects with urls
+   *
+   * @returns {Array} an array of links with eTLDs added
+   */
+  _addETLD(aLinks) {
+    return aLinks.map(link => {
+      try {
+        link.eTLD = Services.eTLD.getPublicSuffix(Services.io.newURI(link.url));
+      } catch (e) {
+        link.eTLD = "";
+      }
+      return link;
+    });
+  },
+
+  /*
+   * Initializes Activity Stream provider - adds a history observer and a
+   * bookmarks observer.
+   */
+  init() {
+    PlacesUtils.history.addObserver(this.historyObserver, true);
+    PlacesUtils.bookmarks.addObserver(this.bookmarksObsever, true);
+  },
+
+  /**
+   * A set of functions called by @mozilla.org/browser/nav-historyservice
+   * All history events are emitted from this object.
+   */
+  historyObserver: {
+    onDeleteURI(uri) {
+      Services.obs.notifyObservers(null, "newtab-deleteURI", {url: uri.spec});
+    },
+
+    onClearHistory() {
+      Services.obs.notifyObservers(null, "newtab-clearHistory");
+    },
+
+    onFrecencyChanged(uri, newFrecency, guid, hidden, lastVisitDate) {
+      if (!hidden && lastVisitDate) {
+        Services.obs.notifyObservers(null, "newtab-linkChanged", {
+          url: uri.spec,
+          frecency: newFrecency,
+          lastVisitDate,
+          type: "history"
+        });
+      }
+    },
+
+    onManyFrecenciesChanged() {
+      Services.obs.notifyObservers(null, "newtab-manyLinksChanged");
+    },
+
+    onTitleChanged(uri, newTitle) {
+      Services.obs.notifyObservers(null, "newtab-linkChanged", {url: uri.spec, title: newTitle});
+    },
+
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver,
+                                           Ci.nsISupportsWeakReference])
+  },
+
+  /**
+   * A set of functions called by @mozilla.org/browser/nav-bookmarks-service
+   * All bookmark events are emitted from this object.
+   */
+  bookmarksObsever: {
+    onItemAdded(id, folderId, index, type, uri, title, dateAdded, guid) {
+      if (type === PlacesUtils.bookmarks.TYPE_BOOKMARK) {
+        ActivityStreamProvider.getBookmark(guid).then(bookmark => {
+          Services.obs.notifyObservers(null, "newtab-bookmarkAdded", bookmark);
+        }).catch(Cu.reportError);
+      }
+    },
+
+    onItemRemoved(id, folderId, index, type, uri) {
+      if (type === PlacesUtils.bookmarks.TYPE_BOOKMARK) {
+        Services.obs.notifyObservers(null, "newtab-bookmarkRemoved", {bookmarkId: id, url: uri.spec});
+      }
+    },
+
+    onItemChanged(id, property, isAnnotation, value, lastModified, type, parent, guid) {
+      if (type === PlacesUtils.bookmarks.TYPE_BOOKMARK) {
+        ActivityStreamProvider.getBookmark(guid).then(bookmark => {
+          Services.obs.notifyObservers(null, "newtab-bookmarkChanged", bookmark);
+        }).catch(Cu.reportError);
+      }
+    },
+
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver,
+                                           Ci.nsISupportsWeakReference])
+  },
+
+  /*
+   * Gets the top frecent sites for Activity Stream.
+   *
+   * @param {Object} aOptions
+   *          options.ignoreBlocked: Do not filter out blocked links .
+   *
+   * @returns {Promise} Returns a promise with the array of links as payload.
+   */
+  getTopFrecentSites: Task.async(function*(aOptions = {}) {
+    let {ignoreBlocked} = aOptions;
+
+    // GROUP first by rev_host to get the most-frecent page of an exact host
+    // then GROUP by rev_nowww to dedupe between top two pages of nowww host.
+    // Note that unlike mysql, sqlite picks the last raw from groupby bucket.
+    // Which is why subselect orders frecency and last_visit_date backwards.
+    // In general the groupby behavior in the absence of aggregates is not
+    // defined in SQL, hence we are relying on sqlite implementation that may
+    // change in the future.
+
+    const limit = Object.keys(BlockedLinks.links).length + TOP_SITES_LIMIT;
+    let sqlQuery = `/* do not warn (bug N/A): do not need index */
+                    SELECT url, title, SUM(frecency) frecency, guid, bookmarkGuid,
+                     last_visit_date / 1000 as lastVisitDate, "history" as type
+                    FROM (SELECT * FROM (
+                      SELECT
+                        rev_host,
+                        fixup_url(get_unreversed_host(rev_host)) AS rev_nowww,
+                        moz_places.url,
+                        moz_places.title,
+                        frecency,
+                        last_visit_date,
+                        moz_places.guid AS guid,
+                        moz_bookmarks.guid AS bookmarkGuid
+                      FROM moz_places
+                      LEFT JOIN moz_bookmarks
+                      on moz_places.id = moz_bookmarks.fk
+                      WHERE hidden = 0 AND last_visit_date NOTNULL
+                      ORDER BY frecency, last_visit_date, moz_places.url DESC
+                    ) GROUP BY rev_host)
+                    GROUP BY rev_nowww
+                    ORDER BY frecency DESC, lastVisitDate DESC, url
+                    LIMIT ${limit}`;
+
+    let links = yield this.executePlacesQuery(sqlQuery, {
+      columns: [
+        "bookmarkGuid",
+        "frecency",
+        "guid",
+        "lastVisitDate",
+        "title",
+        "type",
+        "url"
+      ]
+    });
+
+    if (!ignoreBlocked) {
+      links = links.filter(link => !BlockedLinks.isBlocked(link));
+    }
+    links = links.slice(0, TOP_SITES_LIMIT);
+    links = yield this._addFavicons(links);
+    return this._processLinks(links);
+  }),
+
+  /**
+   * Gets a specific bookmark given an id
+   *
+   * @param {String} aGuid
+   *          A bookmark guid to use as a refrence to fetch the bookmark
+   */
+  getBookmark: Task.async(function*(aGuid) {
+    let bookmark = yield PlacesUtils.bookmarks.fetch(aGuid);
+    if (!bookmark) {
+      return null;
+    }
+    let result = {};
+    result.bookmarkGuid = bookmark.guid;
+    result.bookmarkTitle = bookmark.title;
+    result.lastModified = bookmark.lastModified.getTime();
+    result.url = bookmark.url.href;
+    return result;
+  }),
+
+  /**
+   * Gets History size
+   *
+   * @returns {Promise} Returns a promise with the count of moz_places records
+   */
+  getHistorySize: Task.async(function*() {
+    let sqlQuery = `SELECT count(*) FROM moz_places
+                    WHERE hidden = 0 AND last_visit_date NOT NULL`;
+
+    let result = yield this.executePlacesQuery(sqlQuery);
+    return result;
+  }),
+
+  /**
+   * Gets Bookmarks count
+   *
+   * @returns {Promise} Returns a promise with the count of bookmarks
+   */
+  getBookmarksSize: Task.async(function*() {
+    let sqlQuery = `SELECT count(*) FROM moz_bookmarks WHERE type = :type`;
+
+    let result = yield this.executePlacesQuery(sqlQuery, {params: {type: PlacesUtils.bookmarks.TYPE_BOOKMARK}});
+    return result;
+  }),
+
+  /**
+   * Executes arbitrary query against places database
+   *
+   * @param {String} aQuery
+   *        SQL query to execute
+   * @param {Object} [optional] aOptions
+   *          aOptions.columns - an array of column names. if supplied the return
+   *          items will consists of objects keyed on column names. Otherwise
+   *          array of raw values is returned in the select order
+   *          aOptions.param - an object of SQL binding parameters
+   *
+   * @returns {Promise} Returns a promise with the array of retrieved items
+   */
+  executePlacesQuery: Task.async(function*(aQuery, aOptions = {}) {
+    let {columns, params} = aOptions;
+    let items = [];
+    let queryError = null;
+    let conn = yield PlacesUtils.promiseDBConnection();
+    yield conn.executeCached(aQuery, params, aRow => {
+      try {
+        let item = null;
+        // if columns array is given construct an object
+        if (columns && Array.isArray(columns)) {
+          item = {};
+          columns.forEach(column => {
+            item[column] = aRow.getResultByName(column);
+          });
+        } else {
+          // if no columns - make an array of raw values
+          item = [];
+          for (let i = 0; i < aRow.numEntries; i++) {
+            item.push(aRow.getResultByIndex(i));
+          }
+        }
+        items.push(item);
+      } catch (e) {
+        queryError = e;
+        throw StopIteration;
+      }
+    });
+    if (queryError) {
+      throw new Error(queryError);
+    }
+    return items;
+  })
+};
+
+/**
+ * A set of actions which influence what sites shown on the Activity Stream page
+ */
+var ActivityStreamLinks = {
+  /**
+   * Block a url
+   *
+   * @param {Object} aLink
+   *          The link which contains a URL to add to the block list
+   */
+  blockURL(aLink) {
+    BlockedLinks.block(aLink);
+  },
+
+  onLinkBlocked(aLink) {
+    Services.obs.notifyObservers(null, "newtab-linkChanged", {url: aLink.url, blocked: true})
+  },
+
+  /**
+   * Adds a bookmark
+   *
+   * @param {String} aUrl
+   *          The url to bookmark
+   *
+   * @returns {Promise} Returns a promise set to an object representing the bookmark
+   */
+  addBookmark(aUrl) {
+    return PlacesUtils.bookmarks.insert({
+      url: aUrl,
+      parentGuid: PlacesUtils.bookmarks.unfiledGuid
+    });
+  },
+
+  /**
+   * Removes a bookmark
+   *
+   * @param {String} aBookmarkGuid
+   *          The bookmark guid associated with the bookmark to remove
+   *
+   * @returns {Promise} Returns a promise set to an object representing the
+   *            removed bookmark
+   */
+  deleteBookmark(aBookmarkGuid) {
+    return PlacesUtils.bookmarks.remove(aBookmarkGuid);
+  },
+
+  /**
+   * Removes a history link
+   *
+   * @param {String} aUrl
+   *           The url to be removed from history
+   *
+   * @returns {Promise} Returns a promise set to true if link was removed
+   */
+  deleteHistoryEntry(aUrl) {
+    return PlacesUtils.history.remove(aUrl);
+  },
+
+  /**
+   * Get the top sites to show on Activity Stream
+   *
+   * @return {Promise} Returns a promise with the array of links as the payload
+   */
+  getTopSites: Task.async(function*(aOptions = {}) {
+    return ActivityStreamProvider.getTopFrecentSites(aOptions);
+  })
+};
+
+/**
  * Singleton that provides access to all links contained in the grid (including
  * the ones that don't fit on the grid). A link is a plain object that looks
  * like this:
  *
  * {
  *   url: "http://www.mozilla.org/",
  *   title: "Mozilla",
  *   frecency: 1337,
@@ -1289,16 +1714,20 @@ Links.compareLinks = Links.compareLinks.
 var Telemetry = {
   /**
    * Initializes object.
    */
   init: function Telemetry_init() {
     Services.obs.addObserver(this, TOPIC_GATHER_TELEMETRY);
   },
 
+  uninit: function Telemetry_uninit() {
+    Services.obs.removeObserver(this, TOPIC_GATHER_TELEMETRY);
+  },
+
   /**
    * Collects data.
    */
   _collect: function Telemetry_collect() {
     let probes = [
       { histogram: "NEWTAB_PAGE_ENABLED",
         value: AllPages.enabled },
       { histogram: "NEWTAB_PAGE_ENHANCED",
@@ -1409,31 +1838,41 @@ this.NewTabUtils = {
 
     // Strip off common subdomains of the same site (e.g., www, load balancer)
     return host.replace(/^(m|mobile|www\d*)\./, "");
   },
 
   init: function NewTabUtils_init() {
     if (this.initWithoutProviders()) {
       PlacesProvider.init();
+      ActivityStreamProvider.init();
       Links.addProvider(PlacesProvider);
       BlockedLinks.addObserver(Links);
+      BlockedLinks.addObserver(ActivityStreamLinks);
     }
   },
 
   initWithoutProviders: function NewTabUtils_initWithoutProviders() {
     if (!this._initialized) {
       this._initialized = true;
       ExpirationFilter.init();
       Telemetry.init();
       return true;
     }
     return false;
   },
 
+  uninit: function NewTabUtils_uninit() {
+    if (this.initialized) {
+      Telemetry.uninit();
+      GridPrefs.uninit();
+      BlockedLinks.removeObservers();
+    }
+  },
+
   getProviderLinks(aProvider) {
     let cache = Links._providers.get(aProvider);
     if (cache && cache.sortedLinks) {
       return cache.sortedLinks;
     }
     return [];
   },
 
@@ -1476,10 +1915,12 @@ this.NewTabUtils = {
   },
 
   links: Links,
   allPages: AllPages,
   linkChecker: LinkChecker,
   pinnedLinks: PinnedLinks,
   blockedLinks: BlockedLinks,
   gridPrefs: GridPrefs,
-  placesProvider: PlacesProvider
+  placesProvider: PlacesProvider,
+  activityStreamLinks: ActivityStreamLinks,
+  activityStreamProvider: ActivityStreamProvider
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/tests/xpcshell/head.js
@@ -0,0 +1,78 @@
+var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+Cu.import("resource://gre/modules/NewTabUtils.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.importGlobalProperties(["btoa"]);
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesTestUtils",
+  "resource://testing-common/PlacesTestUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+  "resource://gre/modules/PlacesUtils.jsm");
+
+const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced";
+
+// use time at the start of the tests, chnaging it inside timeDaysAgo()
+// may cause tiny time differences, which break expected sql ordering
+const TIME_NOW = (new Date()).getTime();
+
+Services.prefs.setBoolPref(PREF_NEWTAB_ENHANCED, true);
+
+do_get_profile();
+
+// utility function to compute past timestamp in microseconds
+function timeDaysAgo(numDays) {
+  return (TIME_NOW - (numDays * 24 * 60 * 60 * 1000)) * 1000;
+}
+
+// tests that timestamp falls within 10 days of now
+function isVisitDateOK(timestampMS) {
+  let range = 10 * 24 * 60 * 60 * 1000;
+  return Math.abs(Date.now() - timestampMS) < range;
+}
+
+// a set up function to prep the activity stream provider
+function setUpActivityStreamTest() {
+  return Task.spawn(function*() {
+    yield PlacesTestUtils.clearHistory();
+    yield PlacesUtils.bookmarks.eraseEverything();
+    let faviconExpiredPromise = new Promise(resolve => {
+      Services.obs.addObserver(resolve, "places-favicons-expired");
+    });
+    PlacesUtils.favicons.expireAllFavicons();
+    yield faviconExpiredPromise;
+  });
+}
+
+function do_check_links(actualLinks, expectedLinks) {
+  Assert.ok(Array.isArray(actualLinks));
+  Assert.equal(actualLinks.length, expectedLinks.length);
+  for (let i = 0; i < expectedLinks.length; i++) {
+    let expected = expectedLinks[i];
+    let actual = actualLinks[i];
+    Assert.equal(actual.url, expected.url);
+    Assert.equal(actual.title, expected.title);
+    Assert.equal(actual.frecency, expected.frecency);
+    Assert.equal(actual.lastVisitDate, expected.lastVisitDate);
+  }
+}
+
+function makeLinks(frecRangeStart, frecRangeEnd, step) {
+  let links = [];
+  // Remember, links are ordered by frecency descending.
+  for (let i = frecRangeEnd; i > frecRangeStart; i -= step) {
+    links.push(makeLink(i));
+  }
+  return links;
+}
+
+function makeLink(frecency) {
+  return {
+    url: "http://example" + frecency + ".com/",
+    title: "My frecency is " + frecency,
+    frecency,
+    lastVisitDate: 0,
+  };
+}
--- a/toolkit/modules/tests/xpcshell/test_FinderIterator.js
+++ b/toolkit/modules/tests/xpcshell/test_FinderIterator.js
@@ -1,11 +1,9 @@
-const { interfaces: Ci, classes: Cc, utils: Cu } = Components;
 const { FinderIterator } = Cu.import("resource://gre/modules/FinderIterator.jsm", {});
-Cu.import("resource://gre/modules/Promise.jsm");
 
 var gFindResults = [];
 // Stub the method that instantiates nsIFind and does all the interaction with
 // the docShell to be searched through.
 FinderIterator._iterateDocument = function* (word, window, finder) {
   for (let range of gFindResults)
     yield range;
 };
--- a/toolkit/modules/tests/xpcshell/test_Integration.js
+++ b/toolkit/modules/tests/xpcshell/test_Integration.js
@@ -1,22 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /*
  * Tests the Integration.jsm module.
  */
 
 "use strict";
-
-const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
-
 Cu.import("resource://gre/modules/Integration.jsm", this);
-Cu.import("resource://gre/modules/Services.jsm", this);
-Cu.import("resource://gre/modules/Task.jsm", this);
 
 const TestIntegration = {
   value: "value",
 
   get valueFromThis() {
     return this.value;
   },
 
--- a/toolkit/modules/tests/xpcshell/test_JSONFile.js
+++ b/toolkit/modules/tests/xpcshell/test_JSONFile.js
@@ -1,20 +1,15 @@
 /**
  * Tests the JSONFile object.
  */
 
 "use strict";
 
 // Globals
-
-const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "DownloadPaths",
                                   "resource://gre/modules/DownloadPaths.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
                                   "resource://gre/modules/osfile.jsm");
--- a/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
+++ b/toolkit/modules/tests/xpcshell/test_NewTabUtils.js
@@ -1,26 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // See also browser/base/content/test/newtab/.
 
-var { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
-Cu.import("resource://gre/modules/NewTabUtils.jsm");
-Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource://gre/modules/Task.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
-
-const PREF_NEWTAB_ENHANCED = "browser.newtabpage.enhanced";
-
-function run_test() {
-  Services.prefs.setBoolPref(PREF_NEWTAB_ENHANCED, true);
-  run_next_test();
-}
-
 add_task(function* validCacheMidPopulation() {
   let expectedLinks = makeLinks(0, 3, 1);
 
   let provider = new TestProvider(done => done(expectedLinks));
   provider.maxNumLinks = expectedLinks.length;
 
   NewTabUtils.initWithoutProviders();
   NewTabUtils.links.addProvider(provider);
@@ -315,16 +302,302 @@ add_task(function* extractSite() {
     "file:///Users/user/file",
     "chrome://browser/something",
     "ftp://ftp.mozilla.org/",
   ].forEach(url => {
     do_check_neq(NewTabUtils.extractSite(url), "mozilla.org", "extracted diff url " + url);
   });
 });
 
+add_task(function* faviconBytesToDataURI() {
+  let tests = [
+        [{favicon: "bar".split("").map(s => s.charCodeAt(0)), mimeType: "foo"}],
+        [{favicon: "bar".split("").map(s => s.charCodeAt(0)), mimeType: "foo", xxyy: "quz"}]
+      ];
+  let provider = NewTabUtils.activityStreamProvider;
+
+  for (let test of tests) {
+    let clone = JSON.parse(JSON.stringify(test));
+    delete clone[0].mimeType;
+    clone[0].favicon = `data:foo;base64,${btoa("bar")}`;
+    let result = provider._faviconBytesToDataURI(test);
+    Assert.deepEqual(JSON.stringify(clone), JSON.stringify(result), "favicon converted to data uri");
+  }
+});
+
+add_task(function* addFavicons() {
+  yield setUpActivityStreamTest();
+  let provider = NewTabUtils.activityStreamProvider;
+
+  // start by passing in a bad uri and check that we get a null favicon back
+  let links = [{url: "mozilla.com"}];
+  yield provider._addFavicons(links);
+  Assert.equal(links[0].favicon, null, "Got a null favicon because we passed in a bad url");
+  Assert.equal(links[0].mimeType, null, "Got a null mime type because we passed in a bad url");
+
+  // now fix the url and try again - this time we get good favicon data back
+  links[0].url = "https://mozilla.com";
+  let base64URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
+    "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+
+  let visit = [
+    {uri: links[0].url, visitDate: timeDaysAgo(0), transition: PlacesUtils.history.TRANSITION_TYPED}
+  ];
+  yield PlacesTestUtils.addVisits(visit);
+
+  let faviconData = new Map();
+  faviconData.set("https://mozilla.com", base64URL);
+  yield PlacesTestUtils.addFavicons(faviconData);
+
+  yield provider._addFavicons(links);
+  Assert.equal(links[0].mimeType, "image/png", "Got the right mime type before deleting it");
+  Assert.equal(links[0].faviconLength, links[0].favicon.length, "Got the right length for the byte array");
+  Assert.equal(provider._faviconBytesToDataURI(links)[0].favicon, base64URL, "Got the right favicon");
+});
+
+add_task(function* getTopFrecentSites() {
+  yield setUpActivityStreamTest();
+
+  let provider = NewTabUtils.activityStreamLinks;
+  let links = yield provider.getTopSites();
+  Assert.equal(links.length, 0, "empty history yields empty links");
+
+  // add a visit
+  let testURI = "http://mozilla.com/";
+  yield PlacesTestUtils.addVisits(testURI);
+
+  links = yield provider.getTopSites();
+  Assert.equal(links.length, 1, "adding a visit yields a link");
+  Assert.equal(links[0].url, testURI, "added visit corresponds to added url");
+  Assert.equal(links[0].eTLD, "com", "added visit mozilla.com has 'com' eTLD");
+});
+
+add_task(function* getTopFrecentSites_dedupeWWW() {
+  yield setUpActivityStreamTest();
+
+  let provider = NewTabUtils.activityStreamLinks;
+
+  let links = yield provider.getTopSites();
+  Assert.equal(links.length, 0, "empty history yields empty links");
+
+  // add a visit without www
+  let testURI = "http://mozilla.com";
+  yield PlacesTestUtils.addVisits(testURI);
+
+  // add a visit with www
+  testURI = "http://www.mozilla.com";
+  yield PlacesTestUtils.addVisits(testURI);
+
+  // Test combined frecency score
+  links = yield provider.getTopSites();
+  Assert.equal(links.length, 1, "adding both www. and no-www. yields one link");
+  Assert.equal(links[0].frecency, 200, "frecency scores are combined");
+
+  // add another page visit with www and without www
+  testURI = "http://mozilla.com/page";
+  yield PlacesTestUtils.addVisits(testURI);
+  testURI = "http://www.mozilla.com/page";
+  yield PlacesTestUtils.addVisits(testURI);
+  links = yield provider.getTopSites();
+  Assert.equal(links.length, 1, "adding both www. and no-www. yields one link");
+  Assert.equal(links[0].frecency, 200, "frecency scores are combined ignoring extra pages");
+});
+
+add_task(function* getTopFrencentSites_maxLimit() {
+  yield setUpActivityStreamTest();
+
+  let provider = NewTabUtils.activityStreamLinks;
+
+  // add many visits
+  const MANY_LINKS = 20;
+  for (let i = 0; i < MANY_LINKS; i++) {
+    let testURI = `http://mozilla${i}.com`;
+    yield PlacesTestUtils.addVisits(testURI);
+  }
+
+  let links = yield provider.getTopSites();
+  Assert.ok(links.length < MANY_LINKS, "query default limited to less than many");
+  Assert.ok(links.length > 6, "query default to more than visible count");
+});
+
+add_task(function* getTopFrecentSites_order() {
+  yield setUpActivityStreamTest();
+
+  let provider = NewTabUtils.activityStreamLinks;
+  let {TRANSITION_TYPED} = PlacesUtils.history;
+
+  let timeEarlier = timeDaysAgo(0);
+  let timeLater = timeDaysAgo(2);
+
+  let visits = [
+    // frecency 200
+    {uri: "https://mozilla1.com/0", visitDate: timeEarlier, transition: TRANSITION_TYPED},
+    // sort by url, frecency 200
+    {uri: "https://mozilla2.com/1", visitDate: timeEarlier, transition: TRANSITION_TYPED},
+    // sort by last visit date, frecency 200
+    {uri: "https://mozilla3.com/2", visitDate: timeLater, transition: TRANSITION_TYPED},
+    // sort by frecency, frecency 10
+    {uri: "https://mozilla4.com/3", visitDate: timeLater}
+  ];
+
+  let links = yield provider.getTopSites();
+  Assert.equal(links.length, 0, "empty history yields empty links");
+
+  let base64URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
+    "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+
+  // map of page url to favicon url
+  let faviconData = new Map();
+  faviconData.set("https://mozilla3.com/2", base64URL);
+
+  yield PlacesTestUtils.addVisits(visits);
+  yield PlacesTestUtils.addFavicons(faviconData);
+
+  links = yield provider.getTopSites();
+  Assert.equal(links.length, visits.length, "number of links added is the same as obtain by getTopFrecentSites");
+
+  // first link doesn't have a favicon
+  Assert.equal(links[0].url, visits[0].uri, "links are obtained in the expected order");
+  Assert.equal(null, links[0].favicon, "favicon data is stored as expected");
+  Assert.ok(isVisitDateOK(links[0].lastVisitDate), "visit date within expected range");
+
+  // second link doesn't have a favicon
+  Assert.equal(links[1].url, visits[1].uri, "links are obtained in the expected order");
+  Assert.equal(null, links[1].favicon, "favicon data is stored as expected");
+  Assert.ok(isVisitDateOK(links[1].lastVisitDate), "visit date within expected range");
+
+  // third link should have the favicon data that we added
+  Assert.equal(links[2].url, visits[2].uri, "links are obtained in the expected order");
+  Assert.equal(faviconData.get(links[2].url), links[2].favicon, "favicon data is stored as expected");
+  Assert.ok(isVisitDateOK(links[2].lastVisitDate), "visit date within expected range");
+
+  // fourth link doesn't have a favicon
+  Assert.equal(links[3].url, visits[3].uri, "links are obtained in the expected order");
+  Assert.equal(null, links[3].favicon, "favicon data is stored as expected");
+  Assert.ok(isVisitDateOK(links[3].lastVisitDate), "visit date within expected range");
+});
+
+add_task(function* activitySteamProvider_deleteHistoryLink() {
+  yield setUpActivityStreamTest();
+
+  let provider = NewTabUtils.activityStreamLinks;
+
+  let {TRANSITION_TYPED} = PlacesUtils.history;
+
+  let visits = [
+    // frecency 200
+    {uri: "https://mozilla1.com/0", visitDate: timeDaysAgo(1), transition: TRANSITION_TYPED},
+    // sort by url, frecency 200
+    {uri: "https://mozilla2.com/1", visitDate: timeDaysAgo(0)}
+  ];
+
+  let size = yield NewTabUtils.activityStreamProvider.getHistorySize();
+  Assert.equal(size, 0, "empty history has size 0");
+
+  yield PlacesTestUtils.addVisits(visits);
+
+  size = yield NewTabUtils.activityStreamProvider.getHistorySize();
+  Assert.equal(size, 2, "expected history size");
+
+  // delete a link
+  let deleted = yield provider.deleteHistoryEntry("https://mozilla2.com/1");
+  Assert.equal(deleted, true, "link is deleted");
+
+  // ensure that there's only one link left
+  size = yield NewTabUtils.activityStreamProvider.getHistorySize();
+  Assert.equal(size, 1, "expected history size");
+});
+
+add_task(function* activityStream_addBookmark() {
+  yield setUpActivityStreamTest();
+
+  let provider = NewTabUtils.activityStreamLinks;
+  let bookmarks = [
+    "https://mozilla1.com/0",
+    "https://mozilla1.com/1"
+  ];
+
+  let bookmarksSize = yield NewTabUtils.activityStreamProvider.getBookmarksSize();
+  Assert.equal(bookmarksSize, 0, "empty bookmarks yields 0 size");
+
+  for (let url of bookmarks) {
+    yield provider.addBookmark(url);
+  }
+  bookmarksSize = yield NewTabUtils.activityStreamProvider.getBookmarksSize();
+  Assert.equal(bookmarksSize, 2, "size 2 for 2 bookmarks added");
+});
+
+add_task(function* activityStream_getBookmark() {
+    yield setUpActivityStreamTest();
+
+    let provider = NewTabUtils.activityStreamLinks;
+    let bookmark = yield provider.addBookmark("https://mozilla1.com/0");
+
+    let result = yield NewTabUtils.activityStreamProvider.getBookmark(bookmark.guid);
+    Assert.equal(result.bookmarkGuid, bookmark.guid, "got the correct bookmark guid");
+    Assert.equal(result.bookmarkTitle, bookmark.title, "got the correct bookmark title");
+    Assert.equal(result.lastModified, bookmark.lastModified.getTime(), "got the correct bookmark time");
+    Assert.equal(result.url, bookmark.url.href, "got the correct bookmark url");
+});
+
+add_task(function* activityStream_deleteBookmark() {
+  yield setUpActivityStreamTest();
+
+  let provider = NewTabUtils.activityStreamLinks;
+  let bookmarks = [
+    {url: "https://mozilla1.com/0", parentGuid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_BOOKMARK},
+    {url: "https://mozilla1.com/1", parentGuid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_BOOKMARK}
+  ];
+
+  let bookmarksSize = yield NewTabUtils.activityStreamProvider.getBookmarksSize();
+  Assert.equal(bookmarksSize, 0, "empty bookmarks yields 0 size");
+
+  for (let placeInfo of bookmarks) {
+    yield PlacesUtils.bookmarks.insert(placeInfo);
+  }
+
+  bookmarksSize = yield NewTabUtils.activityStreamProvider.getBookmarksSize();
+  Assert.equal(bookmarksSize, 2, "size 2 for 2 bookmarks added");
+
+  let bookmarkGuid = yield new Promise(resolve => PlacesUtils.bookmarks.fetch(
+    {url: bookmarks[0].url}, bookmark => resolve(bookmark.guid)));
+  let deleted = yield provider.deleteBookmark(bookmarkGuid);
+  Assert.equal(deleted.guid, bookmarkGuid, "the correct bookmark was deleted");
+
+  bookmarksSize = yield NewTabUtils.activityStreamProvider.getBookmarksSize();
+  Assert.equal(bookmarksSize, 1, "size 1 after deleting");
+});
+
+add_task(function* activityStream_blockedURLs() {
+  yield setUpActivityStreamTest();
+
+  let provider = NewTabUtils.activityStreamLinks;
+  NewTabUtils.blockedLinks.addObserver(provider);
+
+  let {TRANSITION_TYPED} = PlacesUtils.history;
+
+  let timeToday = timeDaysAgo(0);
+  let timeEarlier = timeDaysAgo(2);
+
+  let visits = [
+    {uri: "https://example1.com/", visitDate: timeToday, transition: TRANSITION_TYPED},
+    {uri: "https://example2.com/", visitDate: timeToday, transition: TRANSITION_TYPED},
+    {uri: "https://example3.com/", visitDate: timeEarlier, transition: TRANSITION_TYPED},
+    {uri: "https://example4.com/", visitDate: timeEarlier, transition: TRANSITION_TYPED}
+  ];
+  yield PlacesTestUtils.addVisits(visits);
+  yield PlacesUtils.bookmarks.insert({url: "https://example5.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, type: PlacesUtils.bookmarks.TYPE_BOOKMARK});
+
+  let sizeQueryResult;
+
+  // bookmarks
+  sizeQueryResult = yield NewTabUtils.activityStreamProvider.getBookmarksSize();
+  Assert.equal(sizeQueryResult, 1, "got the correct bookmark size");
+});
+
 function TestProvider(getLinksFn) {
   this.getLinks = getLinksFn;
   this._observers = new Set();
 }
 
 TestProvider.prototype = {
   addObserver(observer) {
     this._observers.add(observer);
@@ -341,38 +614,8 @@ TestProvider.prototype = {
     args.unshift(this);
     for (let obs of this._observers) {
       if (obs[observerMethodName])
         obs[observerMethodName].apply(NewTabUtils.links, args);
     }
   },
 };
 
-function do_check_links(actualLinks, expectedLinks) {
-  do_check_true(Array.isArray(actualLinks));
-  do_check_eq(actualLinks.length, expectedLinks.length);
-  for (let i = 0; i < expectedLinks.length; i++) {
-    let expected = expectedLinks[i];
-    let actual = actualLinks[i];
-    do_check_eq(actual.url, expected.url);
-    do_check_eq(actual.title, expected.title);
-    do_check_eq(actual.frecency, expected.frecency);
-    do_check_eq(actual.lastVisitDate, expected.lastVisitDate);
-  }
-}
-
-function makeLinks(frecRangeStart, frecRangeEnd, step) {
-  let links = [];
-  // Remember, links are ordered by frecency descending.
-  for (let i = frecRangeEnd; i > frecRangeStart; i -= step) {
-    links.push(makeLink(i));
-  }
-  return links;
-}
-
-function makeLink(frecency) {
-  return {
-    url: "http://example" + frecency + ".com/",
-    title: "My frecency is " + frecency,
-    frecency,
-    lastVisitDate: 0,
-  };
-}
--- a/toolkit/modules/tests/xpcshell/xpcshell.ini
+++ b/toolkit/modules/tests/xpcshell/xpcshell.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
-head =
+head = head.js
 support-files =
   propertyLists/bug710259_propertyListBinary.plist
   propertyLists/bug710259_propertyListXML.plist
   chromeappsstore.sqlite
   zips/zen.zip
 
 [test_BinarySearch.js]
 skip-if = toolkit == 'android'