Merge fx-team to central, a=merge
authorWes Kocher <wkocher@mozilla.com>
Wed, 08 Jun 2016 17:06:18 -0700
changeset 341171 249d57fa78a0522f63a5b1ed1fdda55f383156e0
parent 341133 051765f8237daf5da7ba0d3e97da16668ce9988c (current diff)
parent 341170 fcd8a5d33cb63d088911f02c601145f2829106ec (diff)
child 341190 8a447c89176fb240f78db350e8d65f62d0a6fabb
child 341241 2c66b75bbb7fe94c60b4a9dbecb8e375facacfd4
push id6389
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:38:22 +0000
treeherdermozilla-beta@01d67bfe6c81 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone50.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to central, a=merge
.eslintignore
mobile/android/base/java/org/mozilla/gecko/home/RecentTabsPanel.java
mobile/android/base/resources/layout-large-land-v11/home_list.xml
mobile/android/base/resources/layout-xlarge-v11/home_list.xml
mobile/android/base/resources/layout/home_list.xml
mobile/android/base/resources/layout/home_list_panel.xml
mobile/android/base/resources/layout/home_open_all_row.xml
--- a/.eslintignore
+++ b/.eslintignore
@@ -109,16 +109,17 @@ devtools/client/webconsole/**
 devtools/client/webide/**
 devtools/server/**
 !devtools/server/actors/webbrowser.js
 !devtools/server/actors/styles.js
 !devtools/server/actors/string.js
 devtools/shared/*.js
 !devtools/shared/css-lexer.js
 !devtools/shared/defer.js
+!devtools/shared/event-emitter.js
 !devtools/shared/task.js
 devtools/shared/*.jsm
 devtools/shared/apps/**
 devtools/shared/client/**
 devtools/shared/discovery/**
 devtools/shared/gcli/**
 !devtools/shared/gcli/templater.js
 devtools/shared/heapsnapshot/**
@@ -131,16 +132,17 @@ devtools/shared/security/**
 devtools/shared/shims/**
 devtools/shared/tests/**
 !devtools/shared/tests/unit/test_csslexer.js
 devtools/shared/touch/**
 devtools/shared/transport/**
 !devtools/shared/transport/transport.js
 devtools/shared/webconsole/test/**
 devtools/shared/worker/**
+!devtools/shared/worker/worker.js
 
 # Ignore devtools pre-processed files
 devtools/client/framework/toolbox-process-window.js
 devtools/client/performance/system.js
 devtools/client/webide/webide-prefs.js
 devtools/client/preferences/**
 
 # Ignore devtools third-party libs
--- a/browser/components/extensions/ext-contextMenus.js
+++ b/browser/components/extensions/ext-contextMenus.js
@@ -35,17 +35,17 @@ var gMaxLabelLength = 64;
 // popuphidden fires.
 var gMenuBuilder = {
   build: function(contextData) {
     let xulMenu = contextData.menu;
     xulMenu.addEventListener("popuphidden", this);
     this.xulMenu = xulMenu;
     for (let [, root] of gRootItems) {
       let rootElement = this.buildElementWithChildren(root, contextData);
-      if (!rootElement.firstChild.childNodes.length) {
+      if (!rootElement.firstChild || !rootElement.firstChild.childNodes.length) {
         // If the root has no visible children, there is no reason to show
         // the root menu item itself either.
         continue;
       }
       rootElement.setAttribute("ext-type", "top-level-menu");
       rootElement = this.removeTopLevelMenuIfNeeded(rootElement);
 
       // Display the extension icon on the root element.
--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -1,75 +1,95 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
-
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
                                   "resource://devtools/shared/event-emitter.js");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
 
-XPCOMUtils.defineLazyGetter(this, "History", () => {
-  Cu.import("resource://gre/modules/PlacesUtils.jsm");
-  return PlacesUtils.history;
-});
-
-Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 const {
   normalizeTime,
   SingletonEventManager,
 } = ExtensionUtils;
 
-let historySvc = Ci.nsINavHistoryService;
+const History = PlacesUtils.history;
 const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
-  ["link", historySvc.TRANSITION_LINK],
-  ["typed", historySvc.TRANSITION_TYPED],
-  ["auto_bookmark", historySvc.TRANSITION_BOOKMARK],
-  ["auto_subframe", historySvc.TRANSITION_EMBED],
-  ["manual_subframe", historySvc.TRANSITION_FRAMED_LINK],
+  ["link", History.TRANSITION_LINK],
+  ["typed", History.TRANSITION_TYPED],
+  ["auto_bookmark", History.TRANSITION_BOOKMARK],
+  ["auto_subframe", History.TRANSITION_EMBED],
+  ["manual_subframe", History.TRANSITION_FRAMED_LINK],
 ]);
 
+let TRANSITION_TYPE_TO_TRANSITIONS_MAP = new Map();
+for (let [transition, transitionType] of TRANSITION_TO_TRANSITION_TYPES_MAP) {
+  TRANSITION_TYPE_TO_TRANSITIONS_MAP.set(transitionType, transition);
+}
+
 function getTransitionType(transition) {
   // cannot set a default value for the transition argument as the framework sets it to null
   transition = transition || "link";
   let transitionType = TRANSITION_TO_TRANSITION_TYPES_MAP.get(transition);
   if (!transitionType) {
     throw new Error(`|${transition}| is not a supported transition for history`);
   }
   return transitionType;
 }
 
+function getTransition(transitionType) {
+  return TRANSITION_TYPE_TO_TRANSITIONS_MAP.get(transitionType) || "link";
+}
+
 /*
- * Converts a nsINavHistoryResultNode into a plain object
+ * Converts a nsINavHistoryResultNode into a HistoryItem
  *
  * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
  */
-function convertNavHistoryResultNode(node) {
+function convertNodeToHistoryItem(node) {
   return {
     id: node.pageGuid,
     url: node.uri,
     title: node.title,
     lastVisitTime: PlacesUtils.toDate(node.time).getTime(),
     visitCount: node.accessCount,
   };
 }
 
 /*
+ * Converts a nsINavHistoryResultNode into a VisitItem
+ *
+ * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
+ */
+function convertNodeToVisitItem(node) {
+  return {
+    id: node.pageGuid,
+    visitId: node.visitId,
+    visitTime: PlacesUtils.toDate(node.time).getTime(),
+    referringVisitId: node.fromVisitId,
+    transition: getTransition(node.visitType),
+  };
+}
+
+/*
  * Converts a nsINavHistoryContainerResultNode into an array of objects
  *
  * https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryContainerResultNode
  */
-function convertNavHistoryContainerResultNode(container) {
+function convertNavHistoryContainerResultNode(container, converter) {
   let results = [];
   container.containerOpen = true;
   for (let i = 0; i < container.childCount; i++) {
     let node = container.getChild(i);
-    results.push(convertNavHistoryResultNode(node));
+    results.push(converter(node));
   }
   container.containerOpen = false;
   return results;
 }
 
 var _observer;
 
 function getObserver() {
@@ -158,17 +178,33 @@ extensions.registerSchemaAPI("history", 
         options.sortingMode = options.SORT_BY_DATE_DESCENDING;
         options.maxResults = query.maxResults || 100;
 
         let historyQuery = History.getNewQuery();
         historyQuery.searchTerms = query.text;
         historyQuery.beginTime = beginTime;
         historyQuery.endTime = endTime;
         let queryResult = History.executeQuery(historyQuery, options).root;
-        let results = convertNavHistoryContainerResultNode(queryResult);
+        let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToHistoryItem);
+        return Promise.resolve(results);
+      },
+      getVisits: function(details) {
+        let url = details.url;
+        if (!url) {
+          return Promise.reject({message: "A URL must be provided for getVisits"});
+        }
+
+        let options = History.getNewQueryOptions();
+        options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+        options.resultType = options.RESULTS_AS_VISIT;
+
+        let historyQuery = History.getNewQuery();
+        historyQuery.uri = NetUtil.newURI(url);
+        let queryResult = History.executeQuery(historyQuery, options).root;
+        let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem);
         return Promise.resolve(results);
       },
 
       onVisitRemoved: new SingletonEventManager(context, "history.onVisitRemoved", fire => {
         let listener = (event, data) => {
           context.runSafe(fire, data);
         };
 
--- a/browser/components/extensions/schemas/history.json
+++ b/browser/components/extensions/schemas/history.json
@@ -150,17 +150,16 @@
                 }
               }
             ]
           }
         ]
       },
       {
         "name": "getVisits",
-        "unsupported": true,
         "type": "function",
         "description": "Retrieves information about visits to a URL.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
--- a/browser/components/extensions/test/browser/browser_ext_history.js
+++ b/browser/components/extensions/test/browser/browser_ext_history.js
@@ -290,8 +290,73 @@ add_task(function* test_add_url() {
 
   for (let data of failTestData) {
     extension.sendMessage("expect-failure", data);
     yield extension.awaitMessage("add-failed");
   }
 
   yield extension.unload();
 });
+
+add_task(function* test_get_visits() {
+  function background() {
+    const TEST_DOMAIN = "http://example.com/";
+    const FIRST_DATE = Date.now();
+    const INITIAL_DETAILS = {
+      url: TEST_DOMAIN,
+      visitTime: FIRST_DATE,
+      transition: "link",
+    };
+
+    let visitIds = new Set();
+
+    function checkVisit(visit, expected) {
+      visitIds.add(visit.visitId);
+      browser.test.assertEq(expected.visitTime, visit.visitTime, "visit has the correct visitTime");
+      browser.test.assertEq(expected.transition, visit.transition, "visit has the correct transition");
+      browser.history.search({text: expected.url}).then(results => {
+        // all results will have the same id, so we only need to use the first one
+        browser.test.assertEq(results[0].id, visit.id, "visit has the correct id");
+      });
+    }
+
+    let details = Object.assign({}, INITIAL_DETAILS);
+
+    browser.history.addUrl(details).then(() => {
+      return browser.history.getVisits({url: details.url});
+    }).then(results => {
+      browser.test.assertEq(1, results.length, "the expected number of visits were returned");
+      checkVisit(results[0], details);
+      details.url = `${TEST_DOMAIN}/1/`;
+      return browser.history.addUrl(details);
+    }).then(() => {
+      return browser.history.getVisits({url: details.url});
+    }).then(results => {
+      browser.test.assertEq(1, results.length, "the expected number of visits were returned");
+      checkVisit(results[0], details);
+      details.visitTime = FIRST_DATE - 1000;
+      details.transition = "typed";
+      return browser.history.addUrl(details);
+    }).then(() => {
+      return browser.history.getVisits({url: details.url});
+    }).then(results => {
+      browser.test.assertEq(2, results.length, "the expected number of visits were returned");
+      checkVisit(results[0], INITIAL_DETAILS);
+      checkVisit(results[1], details);
+    }).then(() => {
+      browser.test.assertEq(3, visitIds.size, "each visit has a unique visitId");
+      browser.test.notifyPass("get-visits");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      permissions: ["history"],
+    },
+    background: `(${background})()`,
+  });
+
+  yield PlacesTestUtils.clearHistory();
+  yield extension.startup();
+
+  yield extension.awaitFinish("get-visits");
+  yield extension.unload();
+});
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -1235,17 +1235,17 @@ Toolbox.prototype = {
       // Prevent flicker while loading by waiting to make visible until now.
       iframe.style.visibility = "visible";
 
       // The build method should return a panel instance, so events can
       // be fired with the panel as an argument. However, in order to keep
       // backward compatibility with existing extensions do a check
       // for a promise return value.
       let built = definition.build(iframe.contentWindow, this);
-      if (!(built instanceof Promise)) {
+      if (!(typeof built.then == "function")) {
         let panel = built;
         iframe.panel = panel;
 
         // The panel instance is expected to fire (and listen to) various
         // framework events, so make sure it's properly decorated with
         // appropriate API (on, off, once, emit).
         // In this case we decorate panel instances directly returned by
         // the tool definition 'build' method.
--- a/devtools/client/netmonitor/netmonitor-controller.js
+++ b/devtools/client/netmonitor/netmonitor-controller.js
@@ -113,17 +113,17 @@ Cu.import("resource://devtools/client/sh
 Cu.import("resource://devtools/client/shared/widgets/VariablesViewController.jsm");
 
 const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
 const promise = require("promise");
 const Services = require("Services");
 const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
 const EventEmitter = require("devtools/shared/event-emitter");
 const Editor = require("devtools/client/sourceeditor/editor");
-const {TimelineFront} = require("devtools/server/actors/timeline");
+const {TimelineFront} = require("devtools/shared/fronts/timeline");
 const { Task } = require("devtools/shared/task");
 
 XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);
 XPCOMUtils.defineConstant(this, "ACTIVITY_TYPE", ACTIVITY_TYPE);
 XPCOMUtils.defineConstant(this, "Editor", Editor);
 
 XPCOMUtils.defineLazyModuleGetter(this, "Chart",
   "resource://devtools/client/shared/widgets/Chart.jsm");
--- a/devtools/client/performance/legacy/actors.js
+++ b/devtools/client/performance/legacy/actors.js
@@ -6,17 +6,17 @@
 const { Task } = require("devtools/shared/task");
 
 const promise = require("promise");
 const EventEmitter = require("devtools/shared/event-emitter");
 const { Poller } = require("devtools/client/shared/poller");
 
 const CompatUtils = require("devtools/client/performance/legacy/compatibility");
 const RecordingUtils = require("devtools/shared/performance/recording-utils");
-const { TimelineFront } = require("devtools/server/actors/timeline");
+const { TimelineFront } = require("devtools/shared/fronts/timeline");
 const { ProfilerFront } = require("devtools/server/actors/profiler");
 
 // how often do we check the status of the profiler's circular buffer
 const PROFILER_CHECK_TIMER = 5000; // ms
 
 const TIMELINE_ACTOR_METHODS = [
   "start", "stop",
 ];
--- a/devtools/client/shared/inplace-editor.js
+++ b/devtools/client/shared/inplace-editor.js
@@ -1426,17 +1426,17 @@ InplaceEditor.prototype = {
         input.setSelectionRange(query.length, query.length + item.length -
                                               startCheckQuery.length);
         this._updateSize();
       }
 
       // Display the list of suggestions if there are more than one.
       if (finalList.length > 1) {
         // Calculate the popup horizontal offset.
-        let indent = this.input.selectionStart - query.length;
+        let indent = this.input.selectionStart - startCheckQuery.length;
         let offset = indent * this.inputCharDimensions.width;
         offset = this._isSingleLine() ? offset : 0;
 
         // Select the most relevantItem if autoInsert is allowed
         let selectedIndex = autoInsert ? index : -1;
 
         // Open the suggestions popup.
         this.popup.setItems(finalList);
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -119,16 +119,17 @@ skip-if = e10s # Bug 1221911, bug 122228
 [browser_html_tooltip-04.js]
 [browser_html_tooltip-05.js]
 [browser_html_tooltip_arrow-01.js]
 [browser_html_tooltip_arrow-02.js]
 [browser_inplace-editor-01.js]
 [browser_inplace-editor-02.js]
 [browser_inplace-editor_autocomplete_01.js]
 [browser_inplace-editor_autocomplete_02.js]
+[browser_inplace-editor_autocomplete_offset.js]
 [browser_inplace-editor_maxwidth.js]
 [browser_key_shortcuts.js]
 [browser_layoutHelpers.js]
 skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_layoutHelpers-getBoxQuads.js]
 skip-if = e10s # Layouthelpers test should not run in a content page.
 [browser_mdn-docs-01.js]
 [browser_mdn-docs-02.js]
--- a/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_01.js
@@ -8,16 +8,23 @@
 const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
 const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
 loadHelperScript("helper_inplace_editor.js");
 
 // Test the inplace-editor autocomplete popup for CSS properties suggestions.
 // Using a mocked list of CSS properties to avoid test failures linked to
 // engine changes (new property, removed property, ...).
 
+// format :
+//  [
+//    what key to press,
+//    expected input box value after keypress,
+//    selected suggestion index (-1 if popup is hidden),
+//    number of suggestions in the popup (0 if popup is hidden),
+//  ]
 const testData = [
   ["b", "border", 1, 3],
   ["VK_DOWN", "box-sizing", 2, 3],
   ["VK_DOWN", "background", 0, 3],
   ["VK_DOWN", "border", 1, 3],
   ["VK_BACK_SPACE", "b", -1, 0],
   ["VK_BACK_SPACE", "", -1, 0],
   ["VK_DOWN", "background", 0, 6],
--- a/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_02.js
@@ -8,16 +8,23 @@
 const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
 const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
 loadHelperScript("helper_inplace_editor.js");
 
 // Test the inplace-editor autocomplete popup for CSS values suggestions.
 // Using a mocked list of CSS properties to avoid test failures linked to
 // engine changes (new property, removed property, ...).
 
+// format :
+//  [
+//    what key to press,
+//    expected input box value after keypress,
+//    selected suggestion index (-1 if popup is hidden),
+//    number of suggestions in the popup (0 if popup is hidden),
+//  ]
 const testData = [
   ["b", "block", -1, 0],
   ["VK_BACK_SPACE", "b", -1, 0],
   ["VK_BACK_SPACE", "", -1, 0],
   ["i", "inline", 0, 2],
   ["VK_DOWN", "inline-block", 1, 2],
   ["VK_DOWN", "inline", 0, 2],
   ["VK_LEFT", "inline", -1, 0],
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_inplace-editor_autocomplete_offset.js
@@ -0,0 +1,111 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+/* import-globals-from helper_inplace_editor.js */
+
+"use strict";
+
+const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
+const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
+loadHelperScript("helper_inplace_editor.js");
+
+// Test the inplace-editor autocomplete popup is aligned with the completed query.
+// Which means when completing "style=display:flex; color:" the popup will aim to be
+// aligned with the ":" next to "color".
+
+// format :
+//  [
+//    what key to press,
+//    expected input box value after keypress,
+//    selected suggestion index (-1 if popup is hidden),
+//    number of suggestions in the popup (0 if popup is hidden),
+//  ]
+// or
+//  ["checkPopupOffset"]
+// to measure and test the autocomplete popup left offset.
+const testData = [
+  ["VK_RIGHT", "style=", -1, 0],
+  ["d", "style=display", 1, 2],
+  ["checkPopupOffset"],
+  ["VK_RIGHT", "style=display", -1, 0],
+  [":", "style=display:block", 0, 3],
+  ["checkPopupOffset"],
+  ["f", "style=display:flex", -1, 0],
+  ["VK_RIGHT", "style=display:flex", -1, 0],
+  [";", "style=display:flex;", -1, 0],
+  ["c", "style=display:flex;color", 1, 2],
+  ["checkPopupOffset"],
+  ["VK_RIGHT", "style=display:flex;color", -1, 0],
+  [":", "style=display:flex;color:blue", 0, 2],
+  ["checkPopupOffset"],
+];
+
+const mockGetCSSPropertyList = function () {
+  return [
+    "clear",
+    "color",
+    "direction",
+    "display",
+  ];
+};
+
+const mockGetCSSValuesForPropertyName = function (propertyName) {
+  let values = {
+    "color": ["blue", "red"],
+    "display": ["block", "flex", "none"]
+  };
+  return values[propertyName] || [];
+};
+
+add_task(function* () {
+  yield addTab("data:text/html;charset=utf-8,inplace editor CSS value autocomplete");
+  let [host, win, doc] = yield createHost();
+
+  let xulDocument = win.top.document;
+  let popup = new AutocompletePopup(xulDocument, { autoSelect: true });
+
+  info("Create a CSS_MIXED type autocomplete");
+  yield new Promise(resolve => {
+    createInplaceEditorAndClick({
+      initial: "style=",
+      start: runAutocompletionTest,
+      contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+      done: resolve,
+      popup: popup
+    }, doc);
+  });
+
+  host.destroy();
+  gBrowser.removeCurrentTab();
+});
+
+let runAutocompletionTest = Task.async(function* (editor) {
+  info("Starting autocomplete test for inplace-editor popup offset");
+  editor._getCSSPropertyList = mockGetCSSPropertyList;
+  editor._getCSSValuesForPropertyName = mockGetCSSValuesForPropertyName;
+
+  let previousOffset = -1;
+  for (let data of testData) {
+    if (data[0] === "checkPopupOffset") {
+      info("Check the popup offset has been modified");
+      // We are not testing hard coded offset values here, which could be fragile. We only
+      // want to ensure the popup tries to match the position of the query in the editor
+      // input.
+      let offset = getPopupOffset(editor);
+      ok(offset > previousOffset, "New popup offset is greater than the previous one");
+      previousOffset = offset;
+    } else {
+      yield testCompletion(data, editor);
+    }
+  }
+
+  EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
+});
+
+/**
+ * Get the autocomplete panel left offset, relative to the provided input's left offset.
+ */
+function getPopupOffset({popup, input}) {
+  let popupQuads = popup._panel.getBoxQuads({relativeTo: input});
+  return popupQuads[0].bounds.left;
+}
--- a/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
+++ b/devtools/client/webconsole/test/browser_bug664688_sandbox_update_after_navigation.js
@@ -37,17 +37,17 @@ add_task(function* () {
         category: CATEGORY_OUTPUT,
       },
     ],
   };
 
   yield waitForMessages(msgForLocation1);
 
   // load second url
-  content.location = TEST_URI2;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
   yield loadBrowser(gBrowser.selectedBrowser);
 
   is(hud.outputNode.textContent.indexOf("Permission denied"), -1,
      "no permission denied errors");
 
   hud.jsterm.clearOutput();
   hud.jsterm.execute("window.location.href");
 
--- a/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
+++ b/devtools/client/webconsole/test/browser_netmonitor_shows_reqs_in_webconsole.js
@@ -51,17 +51,17 @@ add_task(function* () {
 
 function loadDocument(browser) {
   let deferred = promise.defer();
 
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     deferred.resolve();
   }, true);
-  content.location = TEST_PATH;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH);
 
   return deferred.promise;
 }
 
 function testNetmonitor(toolbox) {
   let monitor = toolbox.getCurrentPanel();
   let { RequestsMenu } = monitor.panelWin.NetMonitorView;
   RequestsMenu.lazyUpdate = false;
--- a/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_582201_duplicate_errors.js
@@ -18,17 +18,17 @@ add_task(function* () {
   let hud = yield openConsole();
 
   // On e10s, the exception is triggered in child process
   // and is ignored by test harness
   if (!Services.appinfo.browserTabsRemoteAutostart) {
     expectUncaughtException();
   }
 
-  content.location = TEST_URI;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
 
   yield waitForMessages({
     webconsole: hud,
     messages: [{
       text: "fooDuplicateError1",
       category: CATEGORY_JS,
       severity: SEVERITY_ERROR,
     },
--- a/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_595223_file_uri.js
@@ -23,17 +23,17 @@ add_task(function* () {
   let uri = Services.io.newFileURI(dir);
 
   let { browser } = yield loadTab(TEST_URI);
 
   hud = yield openConsole();
   hud.jsterm.clearOutput();
 
   let loaded = loadBrowser(browser);
-  content.location = uri.spec;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, uri.spec);
   yield loaded;
 
   yield testMessages();
 
   Services.prefs.clearUserPref(PREF);
   hud = null;
 });
 
--- a/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_595934_message_categories.js
@@ -174,17 +174,17 @@ function testNext() {
         }
       } else {
         pageError = true;
       }
 
       startNextTest();
     }, true);
 
-    content.location = testLocation;
+    BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testLocation);
   } else {
     testEnded = true;
     finishTest();
   }
 }
 
 function testEnd() {
   if (!testEnded) {
--- a/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_630733_response_redirect_headers.js
@@ -32,17 +32,17 @@ function consoleOpened(hud) {
   HUDService.lastFinishedRequest.callback = (aHttpRequest) => {
     let status = aHttpRequest.response.status;
     lastFinishedRequests[status] = aHttpRequest;
     if ("301" in lastFinishedRequests &&
         "404" in lastFinishedRequests) {
       deferred.resolve();
     }
   };
-  content.location = TEST_URI2;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
 
   return deferred.promise;
 }
 
 function getHeaders() {
   let deferred = promise.defer();
 
   HUDService.lastFinishedRequest.callback = null;
--- a/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_644419_log_limits.js
@@ -28,17 +28,17 @@ add_task(function* () {
   let loaded = loadBrowser(browser);
 
   // On e10s, the exception is triggered in child process
   // and is ignored by test harness
   if (!Services.appinfo.browserTabsRemoteAutostart) {
     expectUncaughtException();
   }
 
-  content.location = TEST_URI;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
   yield loaded;
 
   yield testWebDevLimits();
   yield testWebDevLimits2();
   yield testJsLimits();
   yield testJsLimits2();
 
   yield testNetLimits();
--- a/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_653531_highlighter_console_helper.js
@@ -99,11 +99,11 @@ function test() {
   waitForExplicitFinish();
 
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
     gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
     waitForFocus(createDocument, content);
   }, true);
 
-  content.location = "data:text/html;charset=utf-8,test for highlighter " +
-                     "helper in web console";
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser,
+    "data:text/html;charset=utf-8,test for highlighter helper in web console");
 }
--- a/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_737873_mixedcontent.js
@@ -25,17 +25,17 @@ add_task(function* () {
 
   yield testMixedContent(hud);
 
   Services.prefs.clearUserPref("security.mixed_content.block_display_content");
   Services.prefs.clearUserPref("security.mixed_content.block_active_content");
 });
 
 var testMixedContent = Task.async(function* (hud) {
-  content.location = TEST_HTTPS_URI;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_HTTPS_URI);
 
   let results = yield waitForMessages({
     webconsole: hud,
     messages: [{
       text: "example.com",
       category: CATEGORY_NETWORK,
       severity: SEVERITY_WARNING,
     }],
--- a/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js
+++ b/devtools/client/webconsole/test/browser_webconsole_bug_770099_violation.js
@@ -18,17 +18,17 @@ const CSP_VIOLATION_MSG = "Content Secur
 add_task(function* () {
   let { browser } = yield loadTab(TEST_URI);
 
   let hud = yield openConsole();
 
   hud.jsterm.clearOutput();
 
   let loaded = loadBrowser(browser);
-  content.location = TEST_VIOLATION;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_VIOLATION);
   yield loaded;
 
   yield waitForSuccess({
     name: "CSP policy URI warning displayed successfully",
     validator: function () {
       return hud.outputNode.textContent.indexOf(CSP_VIOLATION_MSG) > -1;
     }
   });
--- a/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js
+++ b/devtools/client/webconsole/test/browser_webconsole_certificate_messages.js
@@ -45,20 +45,20 @@ function runTestLoop(theHud) {
   if (!gHud) {
     gHud = theHud;
   }
   gHud.jsterm.clearOutput();
   gContentBrowser.addEventListener("load", onLoad, true);
   if (gCurrentTest.pref) {
     SpecialPowers.pushPrefEnv({"set": gCurrentTest.pref},
       function () {
-        content.location = gCurrentTest.url;
+        BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.url);
       });
   } else {
-    content.location = gCurrentTest.url;
+    BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.url);
   }
 }
 
 function onLoad() {
   gContentBrowser.removeEventListener("load", onLoad, true);
 
   waitForSuccess({
     name: gCurrentTest.name,
--- a/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js
+++ b/devtools/client/webconsole/test/browser_webconsole_dont_navigate_on_doubleclick.js
@@ -21,17 +21,17 @@ function test() {
   function* runner() {
     const TEST_PAGE_URI = "http://example.com/browser/devtools/client/" +
                           "webconsole/test/test-console.html" + "?_uniq=" +
                           Date.now();
 
     const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello</p>");
     const hud = yield openConsole(tab);
 
-    content.location = TEST_PAGE_URI;
+    BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PAGE_URI);
 
     let messages = yield waitForMessages({
       webconsole: hud,
       messages: [{
         name: "Network request message",
         url: TEST_PAGE_URI,
         category: CATEGORY_NETWORK
       }]
--- a/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js
+++ b/devtools/client/webconsole/test/browser_webconsole_hsts_invalid-headers.js
@@ -61,17 +61,17 @@ add_task(function* () {
     text: "Strict-Transport-Security: The site specified a header that " +
           "included multiple \u2018max-age\u2019 directives."
   }, hud);
 });
 
 function* checkForMessage(curTest, hud) {
   hud.jsterm.clearOutput();
 
-  content.location = curTest.url;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, curTest.url);
 
   let results = yield waitForMessages({
     webconsole: hud,
     messages: [
       {
         name: curTest.name,
         text: curTest.text,
         category: CATEGORY_SECURITY,
--- a/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
+++ b/devtools/client/webconsole/test/browser_webconsole_netlogging_reset_filter.js
@@ -21,17 +21,17 @@ add_task(function* () {
 
   yield pushPrefEnv();
   hud = yield openConsole();
   hud.jsterm.clearOutput();
 
   HUDService.lastFinishedRequest.callback = request => requests.push(request);
 
   let loaded = loadBrowser(browser);
-  content.location = TEST_FILE_URI;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_FILE_URI);
   yield loaded;
 
   yield testMessages();
   let htmlRequest = requests.find(e => e.request.url.endsWith("html"));
   ok(htmlRequest, "htmlRequest was a html");
 
   yield hud.ui.openNetworkPanel(htmlRequest.actor);
   let toolbox = gDevTools.getToolbox(hud.target);
--- a/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js
+++ b/devtools/client/webconsole/test/browser_webconsole_shows_reqs_in_netmonitor.js
@@ -51,17 +51,17 @@ add_task(function* () {
 
 function loadDocument(browser) {
   let deferred = promise.defer();
 
   browser.addEventListener("load", function onLoad() {
     browser.removeEventListener("load", onLoad, true);
     deferred.resolve();
   }, true);
-  content.location = TEST_PATH;
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH);
 
   return deferred.promise;
 }
 
 function testNetmonitor(toolbox) {
   let monitor = toolbox.getCurrentPanel();
   let { RequestsMenu } = monitor.panelWin.NetMonitorView;
   RequestsMenu.lazyUpdate = false;
--- a/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js
+++ b/devtools/client/webconsole/test/browser_webconsole_strict_mode_errors.js
@@ -26,49 +26,52 @@ add_task(function* () {
         severity: SEVERITY_ERROR,
       },
     ],
   });
 
   if (!Services.appinfo.browserTabsRemoteAutostart) {
     expectUncaughtException();
   }
-  content.location = "data:text/html;charset=utf8,<script>'use strict';function f(a, a) {};</script>";
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "data:text/html;charset="
+    + "utf8,<script>'use strict';function f(a, a) {};</script>");
 
   yield waitForMessages({
     webconsole: hud,
     messages: [
       {
         text: "SyntaxError: duplicate formal argument a",
         category: CATEGORY_JS,
         severity: SEVERITY_ERROR,
       },
     ],
   });
 
   if (!Services.appinfo.browserTabsRemoteAutostart) {
     expectUncaughtException();
   }
-  content.location = "data:text/html;charset=utf8,<script>'use strict';var o = {get p() {}};o.p = 1;</script>";
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "data:text/html;charset="
+    + "utf8,<script>'use strict';var o = {get p() {}};o.p = 1;</script>");
 
   yield waitForMessages({
     webconsole: hud,
     messages: [
       {
         text: "TypeError: setting a property that has only a getter",
         category: CATEGORY_JS,
         severity: SEVERITY_ERROR,
       },
     ],
   });
 
   if (!Services.appinfo.browserTabsRemoteAutostart) {
     expectUncaughtException();
   }
-  content.location = "data:text/html;charset=utf8,<script>'use strict';v = 1;</script>";
+  BrowserTestUtils.loadURI(gBrowser.selectedBrowser,
+    "data:text/html;charset=utf8,<script>'use strict';v = 1;</script>");
 
   yield waitForMessages({
     webconsole: hud,
     messages: [
       {
         text: "ReferenceError: assignment to undeclared variable v",
         category: CATEGORY_JS,
         severity: SEVERITY_ERROR,
--- a/devtools/server/actors/common.js
+++ b/devtools/server/actors/common.js
@@ -503,8 +503,19 @@ exports.expectState = expectState;
  * @see Framerate actor definition: devtools/server/actors/framerate.js
  */
 function actorBridge(methodName, definition = {}) {
   return method(function () {
     return this.bridge[methodName].apply(this.bridge, arguments);
   }, definition);
 }
 exports.actorBridge = actorBridge;
+
+/**
+ * Like `actorBridge`, but without a spec definition, for when the actor is
+ * created with `ActorClassWithSpec` rather than vanilla `ActorClass`.
+ */
+function actorBridgeWithSpec (methodName) {
+  return method(function () {
+    return this.bridge[methodName].apply(this.bridge, arguments);
+  });
+}
+exports.actorBridgeWithSpec = actorBridgeWithSpec;
--- a/devtools/server/actors/csscoverage.js
+++ b/devtools/server/actors/csscoverage.js
@@ -692,25 +692,31 @@ function getTestSelector(selector) {
  */
 exports.SEL_ALL = [
   SEL_EXTERNAL, SEL_FORM, SEL_ELEMENT, SEL_STRUCTURAL, SEL_SEMI,
   SEL_COMBINING, SEL_MEDIA
 ].reduce(function (prev, curr) { return prev.concat(curr); }, []);
 
 /**
  * Find a URL for a given stylesheet
+ * @param stylesheet {StyleSheet|StyleSheetActor}
  */
 const sheetToUrl = exports.sheetToUrl = function (stylesheet) {
   // For <link> elements
   if (stylesheet.href) {
     return stylesheet.href;
   }
 
   // For <style> elements
   if (stylesheet.ownerNode) {
     let document = stylesheet.ownerNode.ownerDocument;
     let sheets = [...document.querySelectorAll("style")];
     let index = sheets.indexOf(stylesheet.ownerNode);
     return getURL(document) + " → <style> index " + index;
   }
 
+  // When `stylesheet` is a StyleSheetActor, we don't have access to ownerNode
+  if (stylesheet.nodeHref) {
+    return stylesheet.nodeHref + " → <style> index " + stylesheet.styleSheetIndex;
+  }
+
   throw new Error("Unknown sheet source");
 };
--- a/devtools/server/actors/timeline.js
+++ b/devtools/server/actors/timeline.js
@@ -12,112 +12,42 @@
  * Most of the logic is handled in devtools/server/performance/timeline.js
  * This just wraps that module up and exposes it via RDP.
  *
  * For more documentation:
  * @see devtools/server/performance/timeline.js
  */
 
 const protocol = require("devtools/shared/protocol");
-const { method, Arg, RetVal, Option } = protocol;
-const events = require("sdk/event/core");
+const { Option, RetVal } = protocol;
+const { actorBridgeWithSpec } = require("devtools/server/actors/common");
 const { Timeline } = require("devtools/server/performance/timeline");
-const { actorBridge } = require("devtools/server/actors/common");
-
-/**
- * Type representing an array of numbers as strings, serialized fast(er).
- * http://jsperf.com/json-stringify-parse-vs-array-join-split/3
- *
- * XXX: It would be nice if on local connections (only), we could just *give*
- * the array directly to the front, instead of going through all this
- * serialization redundancy.
- */
-protocol.types.addType("array-of-numbers-as-strings", {
-  write: (v) => v.join(","),
-  // In Gecko <= 37, `v` is an array; do not transform in this case.
-  read: (v) => typeof v === "string" ? v.split(",") : v
-});
+const { timelineSpec } = require("devtools/shared/specs/timeline");
+const events = require("sdk/event/core");
 
 /**
  * The timeline actor pops and forwards timeline markers registered in docshells.
  */
-var TimelineActor = exports.TimelineActor = protocol.ActorClass({
-  typeName: "timeline",
-
-  events: {
-    /**
-     * Events emitted when "DOMContentLoaded" and "Load" markers are received.
-     */
-    "doc-loading" : {
-      type: "doc-loading",
-      marker: Arg(0, "json"),
-      endTime: Arg(0, "number")
-    },
-
-    /**
-     * The "markers" events emitted every DEFAULT_TIMELINE_DATA_PULL_TIMEOUT ms
-     * at most, when profile markers are found. The timestamps on each marker
-     * are relative to when recording was started.
-     */
-    "markers" : {
-      type: "markers",
-      markers: Arg(0, "json"),
-      endTime: Arg(1, "number")
-    },
-
-    /**
-     * The "memory" events emitted in tandem with "markers", if this was enabled
-     * when the recording started. The `delta` timestamp on this measurement is
-     * relative to when recording was started.
-     */
-    "memory" : {
-      type: "memory",
-      delta: Arg(0, "number"),
-      measurement: Arg(1, "json")
-    },
-
-    /**
-     * The "ticks" events (from the refresh driver) emitted in tandem with
-     * "markers", if this was enabled when the recording started. All ticks
-     * are timestamps with a zero epoch.
-     */
-    "ticks" : {
-      type: "ticks",
-      delta: Arg(0, "number"),
-      timestamps: Arg(1, "array-of-numbers-as-strings")
-    },
-
-    /**
-     * The "frames" events emitted in tandem with "markers", containing
-     * JS stack frames. The `delta` timestamp on this frames packet is
-     * relative to when recording was started.
-     */
-    "frames" : {
-      type: "frames",
-      delta: Arg(0, "number"),
-      frames: Arg(1, "json")
-    }
-  },
-
+var TimelineActor = exports.TimelineActor = protocol.ActorClassWithSpec(timelineSpec, {
   /**
    * Initializes this actor with the provided connection and tab actor.
    */
   initialize: function (conn, tabActor) {
     protocol.Actor.prototype.initialize.call(this, conn);
     this.tabActor = tabActor;
     this.bridge = new Timeline(tabActor);
 
     this._onTimelineEvent = this._onTimelineEvent.bind(this);
     events.on(this.bridge, "*", this._onTimelineEvent);
   },
 
   /**
-   * The timeline actor is the first (and last) in its hierarchy to use protocol.js
-   * so it doesn't have a parent protocol actor that takes care of its lifetime.
-   * So it needs a disconnect method to cleanup.
+   * The timeline actor is the first (and last) in its hierarchy to use
+   * protocol.js so it doesn't have a parent protocol actor that takes care of
+   * its lifetime. So it needs a disconnect method to cleanup.
    */
   disconnect: function () {
     this.destroy();
   },
 
   /**
    * Destroys this actor, stopping recording first.
    */
@@ -125,56 +55,44 @@ var TimelineActor = exports.TimelineActo
     events.off(this.bridge, "*", this._onTimelineEvent);
     this.bridge.destroy();
     this.bridge = null;
     this.tabActor = null;
     protocol.Actor.prototype.destroy.call(this);
   },
 
   /**
-   * Propagate events from the Timeline module over
-   * RDP if the event is defined here.
+   * Propagate events from the Timeline module over RDP if the event is defined
+   * here.
    */
   _onTimelineEvent: function (eventName, ...args) {
-    if (this.events[eventName]) {
-      events.emit(this, eventName, ...args);
-    }
+    events.emit(this, eventName, ...args);
   },
 
-  isRecording: actorBridge("isRecording", {
+  isRecording: actorBridgeWithSpec("isRecording", {
     request: {},
     response: {
       value: RetVal("boolean")
     }
   }),
 
-  start: actorBridge("start", {
+  start: actorBridgeWithSpec("start", {
     request: {
       withMarkers: Option(0, "boolean"),
       withTicks: Option(0, "boolean"),
       withMemory: Option(0, "boolean"),
       withFrames: Option(0, "boolean"),
       withGCEvents: Option(0, "boolean"),
       withDocLoadingEvents: Option(0, "boolean")
     },
     response: {
       value: RetVal("number")
     }
   }),
 
-  stop: actorBridge("stop", {
+  stop: actorBridgeWithSpec("stop", {
     response: {
       // Set as possibly nullable due to the end time possibly being
       // undefined during destruction
       value: RetVal("nullable:number")
     }
   }),
 });
-
-exports.TimelineFront = protocol.FrontClass(TimelineActor, {
-  initialize: function (client, {timelineActor}) {
-    protocol.Front.prototype.initialize.call(this, client, {actor: timelineActor});
-    this.manage(this);
-  },
-  destroy: function () {
-    protocol.Front.prototype.destroy.call(this);
-  },
-});
--- a/devtools/server/tests/browser/browser_markers-docloading-01.js
+++ b/devtools/server/tests/browser/browser_markers-docloading-01.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Test that we get DOMContentLoaded and Load markers
  */
 
-const { TimelineFront } = require("devtools/server/actors/timeline");
+const { TimelineFront } = require("devtools/shared/fronts/timeline");
 const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"];
 
 add_task(function* () {
   let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html");
   let doc = browser.contentDocument;
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
--- a/devtools/server/tests/browser/browser_markers-docloading-02.js
+++ b/devtools/server/tests/browser/browser_markers-docloading-02.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Test that we get DOMContentLoaded and Load markers
  */
 
-const { TimelineFront } = require("devtools/server/actors/timeline");
+const { TimelineFront } = require("devtools/shared/fronts/timeline");
 const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"];
 
 add_task(function* () {
   let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html");
   let doc = browser.contentDocument;
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
--- a/devtools/server/tests/browser/browser_markers-docloading-03.js
+++ b/devtools/server/tests/browser/browser_markers-docloading-03.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
  * Test that we get DOMContentLoaded and Load markers
  */
 
-const { TimelineFront } = require("devtools/server/actors/timeline");
+const { TimelineFront } = require("devtools/shared/fronts/timeline");
 const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"];
 
 add_task(function* () {
   let browser = yield addTab(MAIN_DOMAIN + "doc_innerHTML.html");
   let doc = browser.contentDocument;
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
--- a/devtools/server/tests/browser/browser_timeline.js
+++ b/devtools/server/tests/browser/browser_timeline.js
@@ -5,17 +5,17 @@
 "use strict";
 
 // Test that the timeline front's start/stop/isRecording methods work in a
 // simple use case, and that markers events are sent when operations occur.
 // Note that this test isn't concerned with which markers are actually recorded,
 // just that markers are recorded at all.
 // Trying to check marker types here may lead to intermittents, see bug 1066474.
 
-const {TimelineFront} = require("devtools/server/actors/timeline");
+const {TimelineFront} = require("devtools/shared/fronts/timeline");
 
 add_task(function* () {
   let browser = yield addTab("data:text/html;charset=utf-8,mop");
   let doc = browser.contentDocument;
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
   let form = yield connectDebuggerClient(client);
--- a/devtools/server/tests/browser/browser_timeline_actors.js
+++ b/devtools/server/tests/browser/browser_timeline_actors.js
@@ -2,17 +2,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test that the timeline can also record data from the memory and framerate
 // actors, emitted as events in tadem with the markers.
 
-const {TimelineFront} = require("devtools/server/actors/timeline");
+const {TimelineFront} = require("devtools/shared/fronts/timeline");
 
 add_task(function* () {
   let browser = yield addTab("data:text/html;charset=utf-8,mop");
   let doc = browser.contentDocument;
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
   let form = yield connectDebuggerClient(client);
--- a/devtools/server/tests/browser/browser_timeline_iframes.js
+++ b/devtools/server/tests/browser/browser_timeline_iframes.js
@@ -2,17 +2,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 // Test the timeline front receives markers events for operations that occur in
 // iframes.
 
-const {TimelineFront} = require("devtools/server/actors/timeline");
+const {TimelineFront} = require("devtools/shared/fronts/timeline");
 
 add_task(function* () {
   let browser = yield addTab(MAIN_DOMAIN + "timeline-iframe-parent.html");
   let doc = browser.contentDocument;
 
   initDebuggerServer();
   let client = new DebuggerClient(DebuggerServer.connectPipe());
   let form = yield connectDebuggerClient(client);
--- a/devtools/shared/event-emitter.js
+++ b/devtools/shared/event-emitter.js
@@ -1,24 +1,24 @@
 /* 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/. */
 
-/**
- * EventEmitter.
- */
+"use strict";
 
-(function (factory) { // Module boilerplate
-  if (this.module && module.id.indexOf("event-emitter") >= 0) { // require
+(function (factory) {
+  if (this.module && module.id.indexOf("event-emitter") >= 0) {
+    // require
     factory.call(this, require, exports, module);
-  } else { // Cu.import
+  } else {
+    // Cu.import
     this.isWorker = false;
-      // Bug 1259045: This module is loaded early in firefox startup as a JSM,
-      // but it doesn't depends on any real module. We can save a few cycles
-      // and bytes by not loading Loader.jsm.
+    // Bug 1259045: This module is loaded early in firefox startup as a JSM,
+    // but it doesn't depends on any real module. We can save a few cycles
+    // and bytes by not loading Loader.jsm.
     let require = function (module) {
       const Cu = Components.utils;
       switch (module) {
         case "promise":
           return Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
         case "Services":
           return Cu.import("resource://gre/modules/Services.jsm", {}).Services;
         case "chrome":
@@ -28,156 +28,156 @@
           };
       }
       return null;
     };
     factory.call(this, require, this, { exports: this });
     this.EXPORTED_SYMBOLS = ["EventEmitter"];
   }
 }).call(this, function (require, exports, module) {
-
-  this.EventEmitter = function EventEmitter() {};
+  let EventEmitter = this.EventEmitter = function () {};
   module.exports = EventEmitter;
 
-// See comment in JSM module boilerplate when adding a new dependency.
-  const { Cu, components } = require("chrome");
+  // See comment in JSM module boilerplate when adding a new dependency.
+  const { components } = require("chrome");
   const Services = require("Services");
   const promise = require("promise");
-  var loggingEnabled = true;
+  let loggingEnabled = true;
 
   if (!isWorker) {
     loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
     Services.prefs.addObserver("devtools.dump.emit", {
       observe: () => {
         loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
       }
     }, false);
   }
 
-/**
- * Decorate an object with event emitter functionality.
- *
- * @param Object aObjectToDecorate
- *        Bind all public methods of EventEmitter to
- *        the aObjectToDecorate object.
- */
-  EventEmitter.decorate = function EventEmitter_decorate(aObjectToDecorate) {
+  /**
+   * Decorate an object with event emitter functionality.
+   *
+   * @param Object objectToDecorate
+   *        Bind all public methods of EventEmitter to
+   *        the objectToDecorate object.
+   */
+  EventEmitter.decorate = function (objectToDecorate) {
     let emitter = new EventEmitter();
-    aObjectToDecorate.on = emitter.on.bind(emitter);
-    aObjectToDecorate.off = emitter.off.bind(emitter);
-    aObjectToDecorate.once = emitter.once.bind(emitter);
-    aObjectToDecorate.emit = emitter.emit.bind(emitter);
+    objectToDecorate.on = emitter.on.bind(emitter);
+    objectToDecorate.off = emitter.off.bind(emitter);
+    objectToDecorate.once = emitter.once.bind(emitter);
+    objectToDecorate.emit = emitter.emit.bind(emitter);
   };
 
   EventEmitter.prototype = {
-  /**
-   * Connect a listener.
-   *
-   * @param string aEvent
-   *        The event name to which we're connecting.
-   * @param function aListener
-   *        Called when the event is fired.
-   */
-    on: function EventEmitter_on(aEvent, aListener) {
-      if (!this._eventEmitterListeners)
+    /**
+     * Connect a listener.
+     *
+     * @param string event
+     *        The event name to which we're connecting.
+     * @param function listener
+     *        Called when the event is fired.
+     */
+    on(event, listener) {
+      if (!this._eventEmitterListeners) {
         this._eventEmitterListeners = new Map();
-      if (!this._eventEmitterListeners.has(aEvent)) {
-        this._eventEmitterListeners.set(aEvent, []);
       }
-      this._eventEmitterListeners.get(aEvent).push(aListener);
+      if (!this._eventEmitterListeners.has(event)) {
+        this._eventEmitterListeners.set(event, []);
+      }
+      this._eventEmitterListeners.get(event).push(listener);
     },
 
-  /**
-   * Listen for the next time an event is fired.
-   *
-   * @param string aEvent
-   *        The event name to which we're connecting.
-   * @param function aListener
-   *        (Optional) Called when the event is fired. Will be called at most
-   *        one time.
-   * @return promise
-   *        A promise which is resolved when the event next happens. The
-   *        resolution value of the promise is the first event argument. If
-   *        you need access to second or subsequent event arguments (it's rare
-   *        that this is needed) then use aListener
-   */
-    once: function EventEmitter_once(aEvent, aListener) {
+    /**
+     * Listen for the next time an event is fired.
+     *
+     * @param string event
+     *        The event name to which we're connecting.
+     * @param function listener
+     *        (Optional) Called when the event is fired. Will be called at most
+     *        one time.
+     * @return promise
+     *        A promise which is resolved when the event next happens. The
+     *        resolution value of the promise is the first event argument. If
+     *        you need access to second or subsequent event arguments (it's rare
+     *        that this is needed) then use listener
+     */
+    once(event, listener) {
       let deferred = promise.defer();
 
-      let handler = (aEvent, aFirstArg, ...aRest) => {
-        this.off(aEvent, handler);
-        if (aListener) {
-          aListener.apply(null, [aEvent, aFirstArg, ...aRest]);
+      let handler = (_, first, ...rest) => {
+        this.off(event, handler);
+        if (listener) {
+          listener.apply(null, [event, first, ...rest]);
         }
-        deferred.resolve(aFirstArg);
+        deferred.resolve(first);
       };
 
-      handler._originalListener = aListener;
-      this.on(aEvent, handler);
+      handler._originalListener = listener;
+      this.on(event, handler);
 
       return deferred.promise;
     },
 
-  /**
-   * Remove a previously-registered event listener.  Works for events
-   * registered with either on or once.
-   *
-   * @param string aEvent
-   *        The event name whose listener we're disconnecting.
-   * @param function aListener
-   *        The listener to remove.
-   */
-    off: function EventEmitter_off(aEvent, aListener) {
-      if (!this._eventEmitterListeners)
+    /**
+     * Remove a previously-registered event listener.  Works for events
+     * registered with either on or once.
+     *
+     * @param string event
+     *        The event name whose listener we're disconnecting.
+     * @param function listener
+     *        The listener to remove.
+     */
+    off(event, listener) {
+      if (!this._eventEmitterListeners) {
         return;
-      let listeners = this._eventEmitterListeners.get(aEvent);
+      }
+      let listeners = this._eventEmitterListeners.get(event);
       if (listeners) {
-        this._eventEmitterListeners.set(aEvent, listeners.filter(l => {
-          return l !== aListener && l._originalListener !== aListener;
+        this._eventEmitterListeners.set(event, listeners.filter(l => {
+          return l !== listener && l._originalListener !== listener;
         }));
       }
     },
 
-  /**
-   * Emit an event.  All arguments to this method will
-   * be sent to listener functions.
-   */
-    emit: function EventEmitter_emit(aEvent) {
-      this.logEvent(aEvent, arguments);
+    /**
+     * Emit an event.  All arguments to this method will
+     * be sent to listener functions.
+     */
+    emit(event) {
+      this.logEvent(event, arguments);
 
-      if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(aEvent)) {
+      if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(event)) {
         return;
       }
 
-      let originalListeners = this._eventEmitterListeners.get(aEvent);
-      for (let listener of this._eventEmitterListeners.get(aEvent)) {
-      // If the object was destroyed during event emission, stop
-      // emitting.
+      let originalListeners = this._eventEmitterListeners.get(event);
+      for (let listener of this._eventEmitterListeners.get(event)) {
+        // If the object was destroyed during event emission, stop
+        // emitting.
         if (!this._eventEmitterListeners) {
           break;
         }
 
-      // If listeners were removed during emission, make sure the
-      // event handler we're going to fire wasn't removed.
-        if (originalListeners === this._eventEmitterListeners.get(aEvent) ||
-          this._eventEmitterListeners.get(aEvent).some(l => l === listener)) {
+        // If listeners were removed during emission, make sure the
+        // event handler we're going to fire wasn't removed.
+        if (originalListeners === this._eventEmitterListeners.get(event) ||
+          this._eventEmitterListeners.get(event).some(l => l === listener)) {
           try {
             listener.apply(null, arguments);
+          } catch (ex) {
+            // Prevent a bad listener from interfering with the others.
+            let msg = ex + ": " + ex.stack;
+            console.error(msg);
+            dump(msg + "\n");
           }
-        catch (ex) {
-          // Prevent a bad listener from interfering with the others.
-          let msg = ex + ": " + ex.stack;
-          console.error(msg);
-          dump(msg + "\n");
-        }
         }
       }
     },
 
-    logEvent: function (aEvent, args) {
+    logEvent(event, args) {
       if (!loggingEnabled) {
         return;
       }
 
       let caller, func, path;
       if (!isWorker) {
         caller = components.stack.caller.caller;
         func = caller.name;
@@ -185,26 +185,26 @@
         if (file.includes(" -> ")) {
           file = caller.filename.split(/ -> /)[1];
         }
         path = file + ":" + caller.lineNumber;
       }
 
       let argOut = "(";
       if (args.length === 1) {
-        argOut += aEvent;
+        argOut += event;
       }
 
       let out = "EMITTING: ";
 
-    // We need this try / catch to prevent any dead object errors.
+      // We need this try / catch to prevent any dead object errors.
       try {
         for (let i = 1; i < args.length; i++) {
           if (i === 1) {
-            argOut = "(" + aEvent + ", ";
+            argOut = "(" + event + ", ";
           } else {
             argOut += ", ";
           }
 
           let arg = args[i];
           argOut += arg;
 
           if (arg && arg.nodeName) {
@@ -214,20 +214,19 @@
             }
             if (arg.className) {
               argOut += "." + arg.className;
             }
             argOut += ")";
           }
         }
       } catch (e) {
-      // Object is dead so the toolbox is most likely shutting down,
-      // do nothing.
+        // Object is dead so the toolbox is most likely shutting down,
+        // do nothing.
       }
 
       argOut += ")";
       out += "emit" + argOut + " from " + func + "() -> " + path + "\n";
 
       dump(out);
     },
   };
-
 });
--- a/devtools/shared/fronts/moz.build
+++ b/devtools/shared/fronts/moz.build
@@ -19,11 +19,12 @@ DevToolsModules(
     'inspector.js',
     'preference.js',
     'promises.js',
     'settings.js',
     'storage.js',
     'string.js',
     'styles.js',
     'stylesheets.js',
+    'timeline.js',
     'webaudio.js',
     'webgl.js'
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/fronts/timeline.js
@@ -0,0 +1,25 @@
+/* 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/. */
+"use strict";
+
+const {
+  Front,
+  FrontClassWithSpec,
+} = require("devtools/shared/protocol");
+const { timelineSpec } = require("devtools/shared/specs/timeline");
+
+/**
+ * TimelineFront, the front for the TimelineActor.
+ */
+const TimelineFront = FrontClassWithSpec(timelineSpec, {
+  initialize: function (client, { timelineActor }) {
+    Front.prototype.initialize.call(this, client, { actor: timelineActor });
+    this.manage(this);
+  },
+  destroy: function () {
+    Front.prototype.destroy.call(this);
+  },
+});
+
+exports.TimelineFront = TimelineFront;
--- a/devtools/shared/specs/moz.build
+++ b/devtools/shared/specs/moz.build
@@ -27,12 +27,13 @@ DevToolsModules(
     'script.js',
     'settings.js',
     'source.js',
     'storage.js',
     'string.js',
     'styleeditor.js',
     'styles.js',
     'stylesheets.js',
+    'timeline.js',
     'webaudio.js',
     'webgl.js',
     'worker.js'
 )
new file mode 100644
--- /dev/null
+++ b/devtools/shared/specs/timeline.js
@@ -0,0 +1,118 @@
+/* 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/. */
+"use strict";
+
+const {
+  Arg,
+  RetVal,
+  Option,
+  generateActorSpec,
+  types
+} = require("devtools/shared/protocol");
+
+/**
+ * Type representing an array of numbers as strings, serialized fast(er).
+ * http://jsperf.com/json-stringify-parse-vs-array-join-split/3
+ *
+ * XXX: It would be nice if on local connections (only), we could just *give*
+ * the array directly to the front, instead of going through all this
+ * serialization redundancy.
+ */
+types.addType("array-of-numbers-as-strings", {
+  write: (v) => v.join(","),
+  // In Gecko <= 37, `v` is an array; do not transform in this case.
+  read: (v) => typeof v === "string" ? v.split(",") : v
+});
+
+const timelineSpec = generateActorSpec({
+  typeName: "timeline",
+
+  events: {
+    /**
+     * Events emitted when "DOMContentLoaded" and "Load" markers are received.
+     */
+    "doc-loading": {
+      type: "doc-loading",
+      marker: Arg(0, "json"),
+      endTime: Arg(0, "number")
+    },
+
+    /**
+     * The "markers" events emitted every DEFAULT_TIMELINE_DATA_PULL_TIMEOUT ms
+     * at most, when profile markers are found. The timestamps on each marker
+     * are relative to when recording was started.
+     */
+    "markers": {
+      type: "markers",
+      markers: Arg(0, "json"),
+      endTime: Arg(1, "number")
+    },
+
+    /**
+     * The "memory" events emitted in tandem with "markers", if this was enabled
+     * when the recording started. The `delta` timestamp on this measurement is
+     * relative to when recording was started.
+     */
+    "memory": {
+      type: "memory",
+      delta: Arg(0, "number"),
+      measurement: Arg(1, "json")
+    },
+
+    /**
+     * The "ticks" events (from the refresh driver) emitted in tandem with
+     * "markers", if this was enabled when the recording started. All ticks
+     * are timestamps with a zero epoch.
+     */
+    "ticks": {
+      type: "ticks",
+      delta: Arg(0, "number"),
+      timestamps: Arg(1, "array-of-numbers-as-strings")
+    },
+
+    /**
+     * The "frames" events emitted in tandem with "markers", containing
+     * JS stack frames. The `delta` timestamp on this frames packet is
+     * relative to when recording was started.
+     */
+    "frames": {
+      type: "frames",
+      delta: Arg(0, "number"),
+      frames: Arg(1, "json")
+    }
+  },
+
+  methods: {
+    isRecording: {
+      request: {},
+      response: {
+        value: RetVal("boolean")
+      }
+    },
+
+    start: {
+      request: {
+        withMarkers: Option(0, "boolean"),
+        withTicks: Option(0, "boolean"),
+        withMemory: Option(0, "boolean"),
+        withFrames: Option(0, "boolean"),
+        withGCEvents: Option(0, "boolean"),
+        withDocLoadingEvents: Option(0, "boolean")
+      },
+      response: {
+        value: RetVal("number")
+      }
+    },
+
+    stop: {
+      response: {
+        // Set as possibly nullable due to the end time possibly being
+        // undefined during destruction
+        value: RetVal("nullable:number")
+      }
+    },
+  },
+});
+
+exports.timelineSpec = timelineSpec;
--- a/devtools/shared/worker/worker.js
+++ b/devtools/shared/worker/worker.js
@@ -1,146 +1,151 @@
 /* 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/. */
+
 "use strict";
 
-(function (factory) { // Module boilerplate
-  if (this.module && module.id.indexOf("worker") >= 0) { // require
+/* global ChromeWorker */
+
+(function (factory) {
+  if (this.module && module.id.indexOf("worker") >= 0) {
+    // require
     const { Cc, Ci, Cu, ChromeWorker } = require("chrome");
     const dumpn = require("devtools/shared/DevToolsUtils").dumpn;
     factory.call(this, require, exports, module, { Cc, Ci, Cu }, ChromeWorker, dumpn);
-  } else { // Cu.import
+  } else {
+    // Cu.import
     const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
     const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
     this.isWorker = false;
     this.Promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
     this.console = Cu.import("resource://gre/modules/Console.jsm", {}).console;
     factory.call(
       this, require, this, { exports: this },
       { Cc, Ci, Cu }, ChromeWorker, null
     );
     this.EXPORTED_SYMBOLS = ["DevToolsWorker"];
   }
 }).call(this, function (require, exports, module, { Ci, Cc }, ChromeWorker, dumpn) {
-
-  var MESSAGE_COUNTER = 0;
+  let MESSAGE_COUNTER = 0;
 
-/**
- * Creates a wrapper around a ChromeWorker, providing easy
- * communication to offload demanding tasks. The corresponding URL
- * must implement the interface provided by `devtools/shared/worker/helper`.
- *
- * @see `./devtools/client/shared/widgets/GraphsWorker.js`
- *
- * @param {string} url
- *        The URL of the worker.
- * @param Object opts
- *        An option with the following optional fields:
- *        - name: a name that will be printed with logs
- *        - verbose: log incoming and outgoing messages
- */
+  /**
+   * Creates a wrapper around a ChromeWorker, providing easy
+   * communication to offload demanding tasks. The corresponding URL
+   * must implement the interface provided by `devtools/shared/worker/helper`.
+   *
+   * @see `./devtools/client/shared/widgets/GraphsWorker.js`
+   *
+   * @param {string} url
+   *        The URL of the worker.
+   * @param Object opts
+   *        An option with the following optional fields:
+   *        - name: a name that will be printed with logs
+   *        - verbose: log incoming and outgoing messages
+   */
   function DevToolsWorker(url, opts) {
     opts = opts || {};
     this._worker = new ChromeWorker(url);
     this._verbose = opts.verbose;
     this._name = opts.name;
 
     this._worker.addEventListener("error", this.onError, false);
   }
   exports.DevToolsWorker = DevToolsWorker;
 
-/**
- * Performs the given task in a chrome worker, passing in data.
- * Returns a promise that resolves when the task is completed, resulting in
- * the return value of the task.
- *
- * @param {string} task
- *        The name of the task to execute in the worker.
- * @param {any} data
- *        Data to be passed into the task implemented by the worker.
- * @return {Promise}
- */
+  /**
+   * Performs the given task in a chrome worker, passing in data.
+   * Returns a promise that resolves when the task is completed, resulting in
+   * the return value of the task.
+   *
+   * @param {string} task
+   *        The name of the task to execute in the worker.
+   * @param {any} data
+   *        Data to be passed into the task implemented by the worker.
+   * @return {Promise}
+   */
   DevToolsWorker.prototype.performTask = function (task, data) {
     if (this._destroyed) {
       return Promise.reject("Cannot call performTask on a destroyed DevToolsWorker");
     }
     let worker = this._worker;
     let id = ++MESSAGE_COUNTER;
     let payload = { task, id, data };
 
     if (this._verbose && dumpn) {
       dumpn("Sending message to worker" +
-          (this._name ? (" (" + this._name + ")") : "") +
-          ": " +
-          JSON.stringify(payload, null, 2));
+            (this._name ? (" (" + this._name + ")") : "") +
+            ": " +
+            JSON.stringify(payload, null, 2));
     }
     worker.postMessage(payload);
 
     return new Promise((resolve, reject) => {
-      let listener = ({ data }) => {
+      let listener = ({ data: result }) => {
         if (this._verbose && dumpn) {
           dumpn("Received message from worker" +
-              (this._name ? (" (" + this._name + ")") : "") +
-              ": " +
-              JSON.stringify(data, null, 2));
+                (this._name ? (" (" + this._name + ")") : "") +
+                ": " +
+                JSON.stringify(result, null, 2));
         }
 
-        if (data.id !== id) {
+        if (result.id !== id) {
           return;
         }
         worker.removeEventListener("message", listener);
-        if (data.error) {
-          reject(data.error);
+        if (result.error) {
+          reject(result.error);
         } else {
-          resolve(data.response);
+          resolve(result.response);
         }
       };
 
       worker.addEventListener("message", listener);
     });
   };
 
-/**
- * Terminates the underlying worker. Use when no longer needing the worker.
- */
+  /**
+   * Terminates the underlying worker. Use when no longer needing the worker.
+   */
   DevToolsWorker.prototype.destroy = function () {
     this._worker.terminate();
     this._worker = null;
     this._destroyed = true;
   };
 
   DevToolsWorker.prototype.onError = function ({ message, filename, lineno }) {
     dump(new Error(message + " @ " + filename + ":" + lineno) + "\n");
   };
 
-/**
- * Takes a function and returns a Worker-wrapped version of the same function.
- * Returns a promise upon resolution.
- * @see `./devtools/shared/shared/tests/browser/browser_devtools-worker-03.js
- *
- * * * * ! ! ! This should only be used for tests or A/B testing performance ! ! ! * * * * * *
- *
- * The original function must:
- *
- * Be a pure function, that is, not use any variables not declared within the
- * function, or its arguments.
- *
- * Return a value or a promise.
- *
- * Note any state change in the worker will not affect the callee's context.
- *
- * @param {function} fn
- * @return {function}
- */
+  /**
+   * Takes a function and returns a Worker-wrapped version of the same function.
+   * Returns a promise upon resolution.
+   * @see `./devtools/shared/shared/tests/browser/browser_devtools-worker-03.js
+   *
+   * ⚠ This should only be used for tests or A/B testing performance ⚠
+   *
+   * The original function must:
+   *
+   * Be a pure function, that is, not use any variables not declared within the
+   * function, or its arguments.
+   *
+   * Return a value or a promise.
+   *
+   * Note any state change in the worker will not affect the callee's context.
+   *
+   * @param {function} fn
+   * @return {function}
+   */
   function workerify(fn) {
-    console.warn(`\`workerify\` should only be used in tests or measuring performance.
-  This creates an object URL on the browser window, and should not be used in production.`);
-  // Fetch via window/utils here as we don't want to include
-  // this module normally.
+    console.warn("`workerify` should only be used in tests or measuring performance. " +
+                 "This creates an object URL on the browser window, and should not be " +
+                 "used in production.");
+    // Fetch via window/utils here as we don't want to include
+    // this module normally.
     let { getMostRecentBrowserWindow } = require("sdk/window/utils");
     let { URL, Blob } = getMostRecentBrowserWindow();
     let stringifiedFn = createWorkerString(fn);
     let blob = new Blob([stringifiedFn]);
     let url = URL.createObjectURL(blob);
     let worker = new DevToolsWorker(url);
 
     let wrapperFn = data => worker.performTask("workerifiedTask", data);
@@ -149,20 +154,18 @@
       URL.revokeObjectURL(url);
       worker.destroy();
     };
 
     return wrapperFn;
   }
   exports.workerify = workerify;
 
-/**
- * Takes a function, and stringifies it, attaching the worker-helper.js
- * boilerplate hooks.
- */
+  /**
+   * Takes a function, and stringifies it, attaching the worker-helper.js
+   * boilerplate hooks.
+   */
   function createWorkerString(fn) {
     return `importScripts("resource://gre/modules/workers/require.js");
-    const { createTask } = require("resource://devtools/shared/worker/helper.js");
-    createTask(self, "workerifiedTask", ${fn.toString()});
-  `;
+            const { createTask } = require("resource://devtools/shared/worker/helper.js");
+            createTask(self, "workerifiedTask", ${fn.toString()});`;
   }
-
 });
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -136,17 +136,17 @@ pref("browser.sessionhistory.max_total_v
 pref("browser.sessionhistory.max_entries", 50);
 pref("browser.sessionhistory.contentViewerTimeout", 360);
 pref("browser.sessionhistory.bfcacheIgnoreMemoryPressure", false);
 
 /* session store */
 pref("browser.sessionstore.resume_session_once", false);
 pref("browser.sessionstore.resume_from_crash", true);
 pref("browser.sessionstore.interval", 10000); // milliseconds
-pref("browser.sessionstore.max_tabs_undo", 5);
+pref("browser.sessionstore.max_tabs_undo", 10);
 pref("browser.sessionstore.max_resumed_crashes", 1);
 pref("browser.sessionstore.privacy_level", 0); // saving data: 0 = all, 1 = unencrypted sites, 2 = never
 pref("browser.sessionstore.debug_logging", false);
 
 /* these should help performance */
 pref("mozilla.widget.force-24bpp", true);
 pref("mozilla.widget.use-buffer-pixmap", true);
 pref("mozilla.widget.disable-native-theme", true);
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -5,16 +5,17 @@
 
 package org.mozilla.gecko;
 
 import android.Manifest;
 import android.app.DownloadManager;
 import android.os.Environment;
 import android.support.annotation.CheckResult;
 import android.support.annotation.NonNull;
+
 import org.json.JSONArray;
 import org.mozilla.gecko.adjust.AdjustHelperInterface;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
@@ -2523,16 +2524,22 @@ public class BrowserApp extends GeckoApp
                 // to panel BAR, the history URL still contains FOO, and we restore to FOO. In most
                 // cases however we aren't supplying a panel ID in the URL so this code still works
                 // for most cases.
                 // We can't fix this directly since we can't ignore the panelId if we're explicitly
                 // loading a specific panel, and we currently can't distinguish between loading
                 // history, and loading new pages, see Bug 1268887
                 panelId = tab.getMostRecentHomePanel();
                 panelRestoreData = tab.getMostRecentHomePanelData();
+            } else if (panelId.equals(HomeConfig.getIdForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS))) {
+                // Redirect to the Combined History panel.
+                panelId = HomeConfig.getIdForBuiltinPanelType(PanelType.COMBINED_HISTORY);
+                panelRestoreData = new Bundle();
+                // Jump directly to the Recent Tabs subview of the Combined History panel.
+                panelRestoreData.putBoolean("goToRecentTabs", true);
             }
             showHomePager(panelId, panelRestoreData);
 
             if (mDynamicToolbar.isEnabled()) {
                 mDynamicToolbar.setVisible(true, VisibilityTransition.ANIMATE);
             }
         } else {
             hideHomePager();
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1448,24 +1448,27 @@ public abstract class GeckoApp
         mDoorHangerPopup = new DoorHangerPopup(this);
         mPluginContainer = (AbsoluteLayout) findViewById(R.id.plugin_container);
         mFormAssistPopup = (FormAssistPopup) findViewById(R.id.form_assist_popup);
     }
 
     /**
      * Loads the initial tab at Fennec startup. If we don't restore tabs, this
      * tab will be about:home, or the homepage if the user has set one.
-     * If we've temporarily disabled restoring to break out of a crash loop, we'll
-     * show the recent tabs panel so the user can manually restore tabs as needed.
+     * If we've temporarily disabled restoring to break out of a crash loop, we'll show
+     * the Recent Tabs folder of the Combined History panel, so the user can manually
+     * restore tabs as needed.
      * If we restore tabs, we don't need to create a new tab.
      */
     protected void loadStartupTab(final int flags) {
         if (!mShouldRestore) {
             if (mLastSessionCrashed) {
-                Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(PanelType.RECENT_TABS), flags);
+                // The Recent Tabs panel no longer exists, but BrowserApp will redirect us
+                // to the Recent Tabs folder of the Combined History panel.
+                Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS), flags);
             } else {
                 final String homepage = getHomepage();
                 Tabs.getInstance().loadUrl(!TextUtils.isEmpty(homepage) ? homepage : AboutPages.HOME, flags);
             }
         }
     }
 
     /**
--- a/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/ClientsAdapter.java
@@ -140,16 +140,20 @@ public class ClientsAdapter extends Recy
         }
     }
 
     @Override
     public int getItemViewType(int position) {
         return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
     }
 
+    public int getClientsCount() {
+        return hiddenClients.size() + visibleClients.size();
+    }
+
     @UiThread
     public void setClients(List<RemoteClient> clients) {
         adapterList.clear();
         adapterList.add(null);
 
         hiddenClients.clear();
         visibleClients.clear();
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
@@ -12,17 +12,18 @@ import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 
 public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
-    private static final int SYNCED_DEVICES_SMARTFOLDER_INDEX = 0;
+    private static final int RECENT_TABS_SMARTFOLDER_INDEX = 0;
+    private static final int SYNCED_DEVICES_SMARTFOLDER_INDEX = 1;
 
     // Array for the time ranges in milliseconds covered by each section.
     static final HistorySectionsHelper.SectionDateRange[] sectionDateRangeArray = new HistorySectionsHelper.SectionDateRange[SectionHeader.values().length];
 
     // Semantic names for the time covered by each section
     public enum SectionHeader {
         TODAY,
         YESTERDAY,
@@ -34,16 +35,18 @@ public class CombinedHistoryAdapter exte
         FOUR_MONTHS_AGO,
         FIVE_MONTHS_AGO,
         OLDER_THAN_SIX_MONTHS
     }
 
     private Cursor historyCursor;
     private DevicesUpdateHandler devicesUpdateHandler;
     private int deviceCount = 0;
+    private RecentTabsUpdateHandler recentTabsUpdateHandler;
+    private int recentTabsCount = 0;
 
     // We use a sparse array to store each section header's position in the panel [more cheaply than a HashMap].
     private final SparseArray<SectionHeader> sectionHeaders;
 
     public CombinedHistoryAdapter(Resources resources) {
         super();
         sectionHeaders = new SparseArray<>();
         HistorySectionsHelper.updateRecentSectionOffset(resources, sectionDateRangeArray);
@@ -60,31 +63,49 @@ public class CombinedHistoryAdapter exte
     }
 
     public DevicesUpdateHandler getDeviceUpdateHandler() {
         if (devicesUpdateHandler == null) {
             devicesUpdateHandler = new DevicesUpdateHandler() {
                 @Override
                 public void onDeviceCountUpdated(int count) {
                     deviceCount = count;
-                    notifyItemChanged(0);
+                    notifyItemChanged(SYNCED_DEVICES_SMARTFOLDER_INDEX);
                 }
             };
         }
         return devicesUpdateHandler;
     }
 
+    public interface RecentTabsUpdateHandler {
+        void onRecentTabsCountUpdated(int count);
+    }
+
+    public RecentTabsUpdateHandler getRecentTabsUpdateHandler() {
+        if (recentTabsUpdateHandler == null) {
+            recentTabsUpdateHandler = new RecentTabsUpdateHandler() {
+                @Override
+                public void onRecentTabsCountUpdated(int count) {
+                    recentTabsCount = count;
+                    notifyItemChanged(RECENT_TABS_SMARTFOLDER_INDEX);
+                }
+            };
+        }
+        return recentTabsUpdateHandler;
+    }
+
     @Override
     public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
         final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
         final View view;
 
         final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
 
         switch (itemType) {
+            case RECENT_TABS:
             case SYNCED_DEVICES:
                 view = inflater.inflate(R.layout.home_smartfolder, viewGroup, false);
                 return new CombinedHistoryItem.SmartFolder(view);
 
             case SECTION_HEADER:
                 view = inflater.inflate(R.layout.home_header_row, viewGroup, false);
                 return new CombinedHistoryItem.BasicItem(view);
 
@@ -97,16 +118,20 @@ public class CombinedHistoryAdapter exte
     }
 
     @Override
     public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {
         final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
         final int localPosition = transformAdapterPositionForDataStructure(itemType, position);
 
         switch (itemType) {
+            case RECENT_TABS:
+                ((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.icon_recent, R.string.home_closed_tabs_title2, R.string.home_closed_tabs_one, R.string.home_closed_tabs_number, recentTabsCount);
+                break;
+
             case SYNCED_DEVICES:
                 ((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.cloud, R.string.home_synced_devices_smartfolder, R.string.home_synced_devices_one, R.string.home_synced_devices_number, deviceCount);
                 break;
 
             case SECTION_HEADER:
                 ((TextView) viewHolder.itemView).setText(getSectionHeaderTitle(sectionHeaders.get(localPosition)));
                 break;
 
@@ -135,16 +160,19 @@ public class CombinedHistoryAdapter exte
         } else if (type == CombinedHistoryItem.ItemType.HISTORY) {
             return position - getHeadersBefore(position) - CombinedHistoryPanel.NUM_SMART_FOLDERS;
         } else {
             return position;
         }
     }
 
     private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
+        if (position == RECENT_TABS_SMARTFOLDER_INDEX) {
+            return CombinedHistoryItem.ItemType.RECENT_TABS;
+        }
         if (position == SYNCED_DEVICES_SMARTFOLDER_INDEX) {
             return CombinedHistoryItem.ItemType.SYNCED_DEVICES;
         }
         final int sectionPosition = transformAdapterPositionForDataStructure(CombinedHistoryItem.ItemType.SECTION_HEADER, position);
         if (sectionHeaders.get(sectionPosition) != null) {
             return CombinedHistoryItem.ItemType.SECTION_HEADER;
         }
         return CombinedHistoryItem.ItemType.HISTORY;
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
@@ -10,26 +10,28 @@ import android.support.v4.content.Contex
 import android.support.v7.widget.RecyclerView;
 import android.util.Log;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.TextView;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.RemoteClient;
 import org.mozilla.gecko.db.RemoteTab;
+import org.mozilla.gecko.home.RecentTabsAdapter.ClosedTab;
 
 public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
     private static final String LOGTAG = "CombinedHistoryItem";
 
     public CombinedHistoryItem(View view) {
         super(view);
     }
 
     public enum ItemType {
-        CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD, SYNCED_DEVICES;
+        CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD, SYNCED_DEVICES,
+        RECENT_TABS, CLOSED_TAB;
 
         public static ItemType viewTypeToItemType(int viewType) {
             if (viewType >= ItemType.values().length) {
                 Log.e(LOGTAG, "No corresponding ItemType!");
             }
             return ItemType.values()[viewType];
         }
 
@@ -78,16 +80,22 @@ public abstract class CombinedHistoryIte
             pageRow.updateFromCursor(historyCursor);
         }
 
         public void bind(RemoteTab remoteTab) {
             final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView;
             childPageRow.setShowIcons(true);
             childPageRow.update(remoteTab.title, remoteTab.url);
         }
+
+        public void bind(ClosedTab closedTab) {
+            final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView;
+            childPageRow.setShowIcons(false);
+            childPageRow.update(closedTab.title, closedTab.url);
+        }
     }
 
     public static class ClientItem extends CombinedHistoryItem {
         final TextView nameView;
         final ImageView deviceTypeView;
         final TextView lastModifiedView;
         final ImageView deviceExpanded;
 
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
@@ -39,16 +39,17 @@ import org.json.JSONObject;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.RemoteClientsDialogFragment;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.FxAccountConstants;
 import org.mozilla.gecko.fxa.SyncStatusListener;
+import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel;
 import org.mozilla.gecko.restrictions.Restrictions;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.RemoteClient;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.widget.DividerItemDecoration;
 
@@ -63,55 +64,66 @@ public class CombinedHistoryPanel extend
     private final int LOADER_ID_HISTORY = 0;
     private final int LOADER_ID_REMOTE = 1;
 
     // String placeholders to mark formatting.
     private final static String FORMAT_S1 = "%1$s";
     private final static String FORMAT_S2 = "%2$s";
 
     // Number of smart folders for determining practical empty state.
-    public static final int NUM_SMART_FOLDERS = 1;
+    public static final int NUM_SMART_FOLDERS = 2;
 
     private CombinedHistoryRecyclerView mRecyclerView;
     private CombinedHistoryAdapter mHistoryAdapter;
     private ClientsAdapter mClientsAdapter;
+    private RecentTabsAdapter mRecentTabsAdapter;
     private CursorLoaderCallbacks mCursorLoaderCallbacks;
 
-    private OnPanelLevelChangeListener.PanelLevel mPanelLevel;
+    private Bundle mSavedRestoreBundle;
+
+    private PanelLevel mPanelLevel;
     private Button mPanelFooterButton;
 
+    private PanelStateUpdateHandler mPanelStateUpdateHandler;
+
     // Child refresh layout view.
     protected SwipeRefreshLayout mRefreshLayout;
 
     // Sync listener that stops refreshing when a sync is completed.
     protected RemoteTabsSyncListener mSyncStatusListener;
 
     // Reference to the View to display when there are no results.
     private View mHistoryEmptyView;
     private View mClientsEmptyView;
+    private View mRecentTabsEmptyView;
 
     public interface OnPanelLevelChangeListener {
         enum PanelLevel {
-        PARENT, CHILD
+        PARENT, CHILD_SYNC, CHILD_RECENT_TABS
     }
 
         /**
          * Propagates level changes.
          * @param level
          * @return true if level changed, false otherwise.
          */
         boolean changeLevel(PanelLevel level);
     }
 
     @Override
     public void onCreate(Bundle savedInstance) {
         super.onCreate(savedInstance);
 
         mHistoryAdapter = new CombinedHistoryAdapter(getResources());
         mClientsAdapter = new ClientsAdapter(getContext());
+        // The RecentTabsAdapter doesn't use a cursor and therefore can't use the CursorLoader's
+        // onLoadFinished() callback for updating the panel state when the closed tab count changes.
+        // Instead, we provide it with independent callbacks as necessary.
+        mRecentTabsAdapter = new RecentTabsAdapter(getContext(),
+                mHistoryAdapter.getRecentTabsUpdateHandler(), getPanelStateUpdateHandler());
 
         mSyncStatusListener = new RemoteTabsSyncListener();
         FirefoxAccounts.addSyncStatusListener(mSyncStatusListener);
     }
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
         return inflater.inflate(R.layout.home_combined_history_panel, container, false);
@@ -124,28 +136,38 @@ public class CombinedHistoryPanel extend
         mRecyclerView = (CombinedHistoryRecyclerView) view.findViewById(R.id.combined_recycler_view);
         setUpRecyclerView();
 
         mRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.refresh_layout);
         setUpRefreshLayout();
 
         mClientsEmptyView = view.findViewById(R.id.home_clients_empty_view);
         mHistoryEmptyView = view.findViewById(R.id.home_history_empty_view);
+        mRecentTabsEmptyView = view.findViewById(R.id.home_recent_tabs_empty_view);
         setUpEmptyViews();
 
-        mPanelFooterButton = (Button) view.findViewById(R.id.clear_history_button);
+        mPanelFooterButton = (Button) view.findViewById(R.id.history_panel_footer_button);
+        mPanelFooterButton.setText(R.string.home_clear_history_button);
         mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
+
+        mRecentTabsAdapter.startListeningForClosedTabs();
+
+        if (mSavedRestoreBundle != null) {
+            setPanelStateFromBundle(mSavedRestoreBundle);
+            mSavedRestoreBundle = null;
+        }
     }
 
     private void setUpRecyclerView() {
         if (mPanelLevel == null) {
-            mPanelLevel = OnPanelLevelChangeListener.PanelLevel.PARENT;
+            mPanelLevel = PanelLevel.PARENT;
         }
 
-        mRecyclerView.setAdapter(mPanelLevel == OnPanelLevelChangeListener.PanelLevel.PARENT ? mHistoryAdapter : mClientsAdapter);
+        mRecyclerView.setAdapter(mPanelLevel == PanelLevel.PARENT ? mHistoryAdapter :
+                mPanelLevel == PanelLevel.CHILD_SYNC ? mClientsAdapter : mRecentTabsAdapter);
 
         final RecyclerView.ItemAnimator animator = new DefaultItemAnimator();
         animator.setAddDuration(100);
         animator.setChangeDuration(100);
         animator.setMoveDuration(100);
         animator.setRemoveDuration(100);
         mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
         mRecyclerView.setItemAnimator(animator);
@@ -153,63 +175,89 @@ public class CombinedHistoryPanel extend
         mRecyclerView.setOnHistoryClickedListener(mUrlOpenListener);
         mRecyclerView.setOnPanelLevelChangeListener(new OnLevelChangeListener());
         mRecyclerView.setHiddenClientsDialogBuilder(new HiddenClientsHelper());
         mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
             @Override
             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                 super.onScrolled(recyclerView, dx, dy);
                 final LinearLayoutManager llm = (LinearLayoutManager) recyclerView.getLayoutManager();
-                if ((mPanelLevel == OnPanelLevelChangeListener.PanelLevel.PARENT) && (llm.findLastCompletelyVisibleItemPosition() == HistoryCursorLoader.HISTORY_LIMIT)) {
+                if ((mPanelLevel == PanelLevel.PARENT) && (llm.findLastCompletelyVisibleItemPosition() == HistoryCursorLoader.HISTORY_LIMIT)) {
                     Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.LIST, "history_scroll_max");
                 }
 
             }
         });
         registerForContextMenu(mRecyclerView);
     }
 
     private void setUpRefreshLayout() {
         mRefreshLayout.setColorSchemeResources(R.color.fennec_ui_orange, R.color.action_orange);
         mRefreshLayout.setOnRefreshListener(new RemoteTabsRefreshListener());
+        mRefreshLayout.setEnabled(false);
     }
 
     private void setUpEmptyViews() {
         // Set up history empty view.
-        final ImageView emptyIcon = (ImageView) mHistoryEmptyView.findViewById(R.id.home_empty_image);
-        emptyIcon.setVisibility(View.GONE);
+        final ImageView historyIcon = (ImageView) mHistoryEmptyView.findViewById(R.id.home_empty_image);
+        historyIcon.setVisibility(View.GONE);
 
-        final TextView emptyText = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_text);
-        emptyText.setText(R.string.home_most_recent_empty);
+        final TextView historyText = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_text);
+        historyText.setText(R.string.home_most_recent_empty);
 
-        final TextView emptyHint = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_hint);
+        final TextView historyHint = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_hint);
 
         if (!Restrictions.isAllowed(getActivity(), Restrictable.PRIVATE_BROWSING)) {
-            emptyHint.setVisibility(View.GONE);
+            historyHint.setVisibility(View.GONE);
         } else {
             final String hintText = getResources().getString(R.string.home_most_recent_emptyhint);
             final SpannableStringBuilder hintBuilder = formatHintText(hintText);
             if (hintBuilder != null) {
-                emptyHint.setText(hintBuilder);
-                emptyHint.setMovementMethod(LinkMovementMethod.getInstance());
-                emptyHint.setVisibility(View.VISIBLE);
+                historyHint.setText(hintBuilder);
+                historyHint.setMovementMethod(LinkMovementMethod.getInstance());
+                historyHint.setVisibility(View.VISIBLE);
             }
         }
 
         // Set up Clients empty view.
         final Button syncSetupButton = (Button) mClientsEmptyView.findViewById(R.id.sync_setup_button);
         syncSetupButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View view) {
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "history_syncsetup");
                 // This Activity will redirect to the correct Activity as needed.
                 final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
                 startActivity(intent);
             }
         });
+
+        // Set up Recent Tabs empty view.
+        final ImageView recentTabsIcon = (ImageView) mRecentTabsEmptyView.findViewById(R.id.home_empty_image);
+        recentTabsIcon.setImageResource(R.drawable.icon_remote_tabs_empty);
+
+        final TextView recentTabsText = (TextView) mRecentTabsEmptyView.findViewById(R.id.home_empty_text);
+        recentTabsText.setText(R.string.home_last_tabs_empty);
+    }
+
+    @Override
+    public void restoreData(Bundle data) {
+        if (mRecyclerView != null) {
+            setPanelStateFromBundle(data);
+        } else {
+            mSavedRestoreBundle = data;
+        }
+    }
+
+    private void setPanelStateFromBundle(Bundle data) {
+        if (data != null && data.getBoolean("goToRecentTabs", false) && mPanelLevel != PanelLevel.CHILD_RECENT_TABS) {
+            mPanelLevel = PanelLevel.CHILD_RECENT_TABS;
+            mRecyclerView.swapAdapter(mRecentTabsAdapter, true);
+            updateEmptyView();
+            updateButtonFromLevel();
+        }
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
         mCursorLoaderCallbacks = new CursorLoaderCallbacks();
     }
 
@@ -277,123 +325,169 @@ public class CombinedHistoryPanel extend
                 case LOADER_ID_HISTORY:
                     mHistoryAdapter.setHistory(c);
                     break;
 
                 case LOADER_ID_REMOTE:
                     final List<RemoteClient> clients = mDB.getTabsAccessor().getClientsFromCursor(c);
                     mHistoryAdapter.getDeviceUpdateHandler().onDeviceCountUpdated(clients.size());
                     mClientsAdapter.setClients(clients);
-                    mRefreshLayout.setEnabled(clients.size() > 0);
                     break;
             }
 
             updateEmptyView();
             updateButtonFromLevel();
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
             mClientsAdapter.setClients(Collections.<RemoteClient>emptyList());
             mHistoryAdapter.setHistory(null);
         }
     }
 
+    public interface PanelStateUpdateHandler {
+        void onPanelStateUpdated();
+    }
+
+    public PanelStateUpdateHandler getPanelStateUpdateHandler() {
+        if (mPanelStateUpdateHandler == null) {
+            mPanelStateUpdateHandler = new PanelStateUpdateHandler() {
+                @Override
+                public void onPanelStateUpdated() {
+                    updateEmptyView();
+                    updateButtonFromLevel();
+                }
+            };
+        }
+        return mPanelStateUpdateHandler;
+    }
+
     protected class OnLevelChangeListener implements OnPanelLevelChangeListener {
         @Override
         public boolean changeLevel(PanelLevel level) {
             if (level == mPanelLevel) {
                 return false;
             }
 
             mPanelLevel = level;
             switch (level) {
                 case PARENT:
                     mRecyclerView.swapAdapter(mHistoryAdapter, true);
+                    mRefreshLayout.setEnabled(false);
                     break;
-                case CHILD:
+                case CHILD_SYNC:
                     mRecyclerView.swapAdapter(mClientsAdapter, true);
+                    mRefreshLayout.setEnabled(mClientsAdapter.getClientsCount() > 0);
+                    break;
+                case CHILD_RECENT_TABS:
+                    mRecyclerView.swapAdapter(mRecentTabsAdapter, true);
                     break;
             }
 
             updateEmptyView();
             updateButtonFromLevel();
             return true;
         }
     }
 
     private void updateButtonFromLevel() {
         switch (mPanelLevel) {
             case PARENT:
                 final boolean historyRestricted = !Restrictions.isAllowed(getActivity(), Restrictable.CLEAR_HISTORY);
                 if (historyRestricted || mHistoryAdapter.getItemCount() == NUM_SMART_FOLDERS) {
                     mPanelFooterButton.setVisibility(View.GONE);
                 } else {
+                    mPanelFooterButton.setText(R.string.home_clear_history_button);
                     mPanelFooterButton.setVisibility(View.VISIBLE);
                 }
                 break;
-            case CHILD:
+            case CHILD_RECENT_TABS:
+                if (mRecentTabsAdapter.getClosedTabsCount() > 1) {
+                    mPanelFooterButton.setText(R.string.home_restore_all);
+                    mPanelFooterButton.setVisibility(View.VISIBLE);
+                } else {
+                    mPanelFooterButton.setVisibility(View.GONE);
+                }
+                break;
+            case CHILD_SYNC:
                 mPanelFooterButton.setVisibility(View.GONE);
                 break;
         }
     }
 
     private class OnFooterButtonClickListener implements View.OnClickListener {
         @Override
         public void onClick(View view) {
+            switch (mPanelLevel) {
+                case PARENT:
+                    final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
+                    dialogBuilder.setMessage(R.string.home_clear_history_confirm);
+                    dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
+                        @Override
+                        public void onClick(final DialogInterface dialog, final int which) {
+                            dialog.dismiss();
+                        }
+                    });
 
-            final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
-            dialogBuilder.setMessage(R.string.home_clear_history_confirm);
-            dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
-                @Override
-                public void onClick(final DialogInterface dialog, final int which) {
-                    dialog.dismiss();
-                }
-            });
-
-            dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
-                @Override
-                public void onClick(final DialogInterface dialog, final int which) {
-                    dialog.dismiss();
+                    dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
+                        @Override
+                        public void onClick(final DialogInterface dialog, final int which) {
+                            dialog.dismiss();
 
-                    // Send message to Java to clear history.
-                    final JSONObject json = new JSONObject();
-                    try {
-                        json.put("history", true);
-                    } catch (JSONException e) {
-                        Log.e(LOGTAG, "JSON error", e);
+                            // Send message to Java to clear history.
+                            final JSONObject json = new JSONObject();
+                            try {
+                                json.put("history", true);
+                            } catch (JSONException e) {
+                                Log.e(LOGTAG, "JSON error", e);
+                            }
+
+                            GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
+                    mRecentTabsAdapter.clearLastSessionData();
+                            Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
+                        }
+                    });
+
+                    dialogBuilder.show();
+                    break;
+                case CHILD_RECENT_TABS:
+                    final String telemetryExtra = mRecentTabsAdapter.restoreAllTabs();
+                    if (telemetryExtra != null) {
+                        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.BUTTON, telemetryExtra);
                     }
-
-                    GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
-                    Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
-                }
-            });
-
-            dialogBuilder.show();
+                    break;
+            }
         }
     }
 
     private void updateEmptyView() {
         boolean showEmptyHistoryView = false;
         boolean showEmptyClientsView = false;
+        boolean showEmptyRecentTabsView = false;
         switch (mPanelLevel) {
             case PARENT:
                 showEmptyHistoryView = mHistoryAdapter.getItemCount() == NUM_SMART_FOLDERS;
                 break;
 
-            case CHILD:
+            case CHILD_SYNC:
                 showEmptyClientsView = mClientsAdapter.getItemCount() == 1;
                 break;
+
+            case CHILD_RECENT_TABS:
+                showEmptyRecentTabsView = mRecentTabsAdapter.getClosedTabsCount() == 0;
+                break;
         }
 
-        final boolean showEmptyView = showEmptyClientsView || showEmptyHistoryView;
+        final boolean showEmptyView = showEmptyClientsView || showEmptyHistoryView || showEmptyRecentTabsView;
         mRecyclerView.setOverScrollMode(showEmptyView ? View.OVER_SCROLL_NEVER : View.OVER_SCROLL_IF_CONTENT_SCROLLS);
 
         mClientsEmptyView.setVisibility(showEmptyClientsView ? View.VISIBLE : View.GONE);
         mHistoryEmptyView.setVisibility(showEmptyHistoryView ? View.VISIBLE : View.GONE);
+        mRecentTabsEmptyView.setVisibility(showEmptyRecentTabsView ? View.VISIBLE : View.GONE);
     }
 
     /**
      * Make Span that is clickable, and underlined
      * between the string markers <code>FORMAT_S1</code> and
      * <code>FORMAT_S2</code>.
      *
      * @param text String to format
@@ -551,16 +645,23 @@ public class CombinedHistoryPanel extend
 
         @Override
         public void onSyncFinished() {
             mRefreshLayout.setRefreshing(false);
         }
     }
 
     @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+
+        mRecentTabsAdapter.stopListeningForClosedTabs();
+    }
+
+    @Override
     public void onDestroy() {
         super.onDestroy();
         if (mSyncStatusListener != null) {
             FirefoxAccounts.removeSyncStatusListener(mSyncStatusListener);
             mSyncStatusListener = null;
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
@@ -14,17 +14,18 @@ import android.view.View;
 import org.mozilla.gecko.db.RemoteClient;
 import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.widget.RecyclerViewClickSupport;
 
 import java.util.EnumSet;
 
-import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD;
+import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS;
+import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_SYNC;
 import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.PARENT;
 
 public class CombinedHistoryRecyclerView extends RecyclerView
         implements RecyclerViewClickSupport.OnItemClickListener, RecyclerViewClickSupport.OnItemLongClickListener {
     public static String LOGTAG = "CombinedHistoryRecycView";
 
     protected interface AdapterContextMenuBuilder {
         HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position);
@@ -84,20 +85,25 @@ public class CombinedHistoryRecyclerView
     public void setHiddenClientsDialogBuilder(CombinedHistoryPanel.DialogBuilder<RemoteClient> builder) {
         mDialogBuilder = builder;
     }
 
     @Override
     public void onItemClicked(RecyclerView recyclerView, int position, View v) {
         final int viewType = getAdapter().getItemViewType(position);
         final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
+        final String telemetryExtra;
 
         switch (itemType) {
+            case RECENT_TABS:
+                mOnPanelLevelChangeListener.changeLevel(CHILD_RECENT_TABS);
+                break;
+
             case SYNCED_DEVICES:
-                mOnPanelLevelChangeListener.changeLevel(CHILD);
+                mOnPanelLevelChangeListener.changeLevel(CHILD_SYNC);
                 break;
 
             case CLIENT:
                 ((ClientsAdapter) getAdapter()).toggleClient(position);
                 break;
 
             case HIDDEN_DEVICES:
                 if (mDialogBuilder != null) {
@@ -112,16 +118,21 @@ public class CombinedHistoryRecyclerView
             case CHILD:
             case HISTORY:
                 if (mOnUrlOpenListener != null) {
                     final TwoLinePageRow historyItem = (TwoLinePageRow) v;
                     Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "history");
                     mOnUrlOpenListener.onUrlOpen(historyItem.getUrl(), EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
                 }
                 break;
+
+            case CLOSED_TAB:
+                telemetryExtra = ((RecentTabsAdapter) getAdapter()).restoreTabFromPosition(position);
+                Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, telemetryExtra);
+                break;
         }
     }
 
     @Override
     public boolean onItemLongClicked(RecyclerView recyclerView, int position, View v) {
         mContextMenuInfo = ((AdapterContextMenuBuilder) getAdapter()).makeContextMenuInfoFromPosition(v, position);
         return showContextMenuForChild(this);
     }
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfig.java
@@ -38,23 +38,23 @@ public final class HomeConfig {
      * to a default set of built-in panels. The DYNAMIC panel type is used by
      * third-party services to create panels with varying types of content.
      */
     @RobocopTarget
     public static enum PanelType implements Parcelable {
         TOP_SITES("top_sites", TopSitesPanel.class),
         BOOKMARKS("bookmarks", BookmarksPanel.class),
         COMBINED_HISTORY("combined_history", CombinedHistoryPanel.class),
-        RECENT_TABS("recent_tabs", RecentTabsPanel.class),
         DYNAMIC("dynamic", DynamicPanel.class),
         // Deprecated panels that should no longer exist but are kept around for
         // migration code. Class references have been replaced with new version of the panel.
         DEPRECATED_REMOTE_TABS("remote_tabs", CombinedHistoryPanel.class),
         DEPRECATED_HISTORY("history", CombinedHistoryPanel.class),
-        DEPRECATED_READING_LIST("reading_list", BookmarksPanel.class);
+        DEPRECATED_READING_LIST("reading_list", BookmarksPanel.class),
+        DEPRECATED_RECENT_TABS("recent_tabs", CombinedHistoryPanel.class);
 
         private final String mId;
         private final Class<?> mPanelClass;
 
         PanelType(String id, Class<?> panelClass) {
             mId = id;
             mPanelClass = panelClass;
         }
@@ -1639,22 +1639,20 @@ public final class HomeConfig {
             return R.string.home_top_sites_title;
 
         case BOOKMARKS:
         case DEPRECATED_READING_LIST:
             return R.string.bookmarks_title;
 
         case DEPRECATED_HISTORY:
         case DEPRECATED_REMOTE_TABS:
+        case DEPRECATED_RECENT_TABS:
         case COMBINED_HISTORY:
             return R.string.home_history_title;
 
-        case RECENT_TABS:
-            return R.string.recent_tabs_title;
-
         default:
             throw new IllegalArgumentException("Only for built-in panel types: " + panelType);
         }
     }
 
     public static String getIdForBuiltinPanelType(PanelType panelType) {
         switch (panelType) {
         case TOP_SITES:
@@ -1670,17 +1668,17 @@ public final class HomeConfig {
             return COMBINED_HISTORY_PANEL_ID;
 
         case DEPRECATED_REMOTE_TABS:
             return REMOTE_TABS_PANEL_ID;
 
         case DEPRECATED_READING_LIST:
             return DEPRECATED_READING_LIST_PANEL_ID;
 
-        case RECENT_TABS:
+        case DEPRECATED_RECENT_TABS:
             return RECENT_TABS_PANEL_ID;
 
         default:
             throw new IllegalArgumentException("Only for built-in panel types: " + panelType);
         }
     }
 
     public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType, EnumSet<PanelConfig.Flags> flags) {
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
@@ -30,17 +30,17 @@ import android.content.SharedPreferences
 import android.support.v4.content.LocalBroadcastManager;
 import android.text.TextUtils;
 import android.util.Log;
 
 public class HomeConfigPrefsBackend implements HomeConfigBackend {
     private static final String LOGTAG = "GeckoHomeConfigBackend";
 
     // Increment this to trigger a migration.
-    private static final int VERSION = 6;
+    private static final int VERSION = 7;
 
     // This key was originally used to store only an array of panel configs.
     public static final String PREFS_CONFIG_KEY_OLD = "home_panels";
 
     // This key is now used to store a version number with the array of panel configs.
     public static final String PREFS_CONFIG_KEY = "home_panels_with_version";
 
     // Keys used with JSON object stored in prefs.
@@ -70,18 +70,16 @@ public class HomeConfigPrefsBackend impl
 
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.TOP_SITES,
                                                   EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)));
 
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.BOOKMARKS));
         panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.COMBINED_HISTORY));
 
 
-        panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.RECENT_TABS));
-
         return new State(panelConfigs, true);
     }
 
     /**
      * Iterate through the panels to check if they are all disabled.
      */
     private static boolean allPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
         final int count = jsonPanels.length();
@@ -228,51 +226,69 @@ public class HomeConfigPrefsBackend impl
         }
 
         // Make the History panel default. We can't modify existing PanelConfigs, so make a new one.
         final PanelConfig historyPanelConfig = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL));
         jsonPanels.put(historyIndex, historyPanelConfig.toJSON());
     }
 
     /**
-     * Remove the reading list panel.
-     * If the reading list panel used to be the default panel, we make bookmarks the new default.
+     * Removes a panel from the home panel config.
+     * If the removed panel was set as the default home panel, we provide a replacement for it.
+     *
+     * @param context Android context
+     * @param jsonPanels array of original JSON panels
+     * @param panelToRemove The home panel to be removed.
+     * @param replacementPanel The panel which will replace it if the removed panel
+     *                         was the default home panel.
+     * @param alwaysUnhide If true, the replacement panel will always be unhidden,
+     *                     otherwise only if we turn it into the new default panel.
+     * @return new array of updated JSON panels
+     * @throws JSONException
      */
-    private static JSONArray removeReadingListPanel(Context context, JSONArray jsonPanels) throws JSONException {
+    private static JSONArray removePanel(Context context, JSONArray jsonPanels,
+                                         PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide) throws JSONException {
         boolean wasDefault = false;
-        int bookmarksIndex = -1;
+        int replacementPanelIndex = -1;
 
         // JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all
         // the items we don't want deleted into a new array.
         final JSONArray newJSONPanels = new JSONArray();
 
         for (int i = 0; i < jsonPanels.length(); i++) {
             final JSONObject panelJSON = jsonPanels.getJSONObject(i);
             final PanelConfig panelConfig = new PanelConfig(panelJSON);
 
-            if (panelConfig.getType() == PanelType.DEPRECATED_READING_LIST) {
+            if (panelConfig.getType() == panelToRemove) {
                 // If this panel was the default we'll need to assign a new default:
                 wasDefault = panelConfig.isDefault();
             } else {
-                if (panelConfig.getType() == PanelType.BOOKMARKS) {
-                    bookmarksIndex = newJSONPanels.length();
+                if (panelConfig.getType() == replacementPanel) {
+                    replacementPanelIndex = newJSONPanels.length();
                 }
 
                 newJSONPanels.put(panelJSON);
             }
         }
 
-        if (wasDefault) {
-            // This will make the bookmarks panel visible if it was previously hidden - this is desired
-            // since this will make the new equivalent of the reading list visible by default.
-            final JSONObject bookmarksPanelConfig = createBuiltinPanelConfig(context, PanelType.BOOKMARKS, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
-            if (bookmarksIndex != -1) {
-                newJSONPanels.put(bookmarksIndex, bookmarksPanelConfig);
+        // Unless alwaysUnhide is true, we make the replacement panel visible only if it is going
+        // to be the new default panel, since a hidden default panel doesn't make sense.
+        // This is to allow preserving the behaviour of the original reading list migration function.
+        if (wasDefault || alwaysUnhide) {
+            final JSONObject replacementPanelConfig;
+            if (wasDefault) {
+                replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
             } else {
-                newJSONPanels.put(bookmarksPanelConfig);
+                replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel).toJSON();
+            }
+
+            if (replacementPanelIndex != -1) {
+                newJSONPanels.put(replacementPanelIndex, replacementPanelConfig);
+            } else {
+                newJSONPanels.put(replacementPanelConfig);
             }
         }
 
         return newJSONPanels;
     }
 
     /**
      * Checks to see if the reading list panel already exists.
@@ -341,17 +357,17 @@ public class HomeConfigPrefsBackend impl
 
         for (int v = version + 1; v <= VERSION; v++) {
             Log.d(LOGTAG, "Migrating to version = " + v);
 
             switch (v) {
                 case 1:
                     // Add "Recent Tabs" panel.
                     addBuiltinPanelConfig(context, jsonPanels,
-                            PanelType.RECENT_TABS, Position.FRONT, Position.BACK);
+                            PanelType.DEPRECATED_RECENT_TABS, Position.FRONT, Position.BACK);
 
                     // Remove the old pref key.
                     prefsEditor.remove(PREFS_CONFIG_KEY_OLD);
                     break;
 
                 case 2:
                     // Add "Remote Tabs"/"Synced Tabs" panel.
                     addBuiltinPanelConfig(context, jsonPanels,
@@ -379,17 +395,23 @@ public class HomeConfigPrefsBackend impl
                     break;
 
                 case 5:
                     // This is the fix for bug 1264136 where we lost track of the default panel during some migrations.
                     ensureDefaultPanelForV5(context, jsonPanels);
                     break;
 
                 case 6:
-                    jsonPanels = removeReadingListPanel(context, jsonPanels);
+                    jsonPanels = removePanel(context, jsonPanels,
+                            PanelType.DEPRECATED_READING_LIST, PanelType.BOOKMARKS, false);
+                    break;
+
+                case 7:
+                    jsonPanels = removePanel(context, jsonPanels,
+                            PanelType.DEPRECATED_RECENT_TABS, PanelType.COMBINED_HISTORY, true);
                     break;
             }
         }
 
         // Save the new panel config and the new version number.
         final JSONObject newJson = new JSONObject();
         newJson.put(JSON_KEY_PANELS, jsonPanels);
         newJson.put(JSON_KEY_VERSION, VERSION);
new file mode 100755
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
@@ -0,0 +1,423 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.home;
+
+import android.content.Context;
+import android.support.v7.widget.RecyclerView;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.EventDispatcher;
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.SessionParser;
+import org.mozilla.gecko.home.CombinedHistoryAdapter.RecentTabsUpdateHandler;
+import org.mozilla.gecko.home.CombinedHistoryPanel.PanelStateUpdateHandler;
+import org.mozilla.gecko.util.EventCallback;
+import org.mozilla.gecko.util.NativeEventListener;
+import org.mozilla.gecko.util.NativeJSObject;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType;
+
+public class RecentTabsAdapter extends RecyclerView.Adapter<CombinedHistoryItem>
+                               implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder, NativeEventListener {
+    private static final String LOGTAG = "GeckoRecentTabsAdapter";
+
+    private static final int NAVIGATION_BACK_BUTTON_INDEX = 0;
+
+    private static final String TELEMETRY_EXTRA_LAST_TIME = "recent_tabs_last_time";
+    private static final String TELEMETRY_EXTRA_RECENTY_CLOSED = "recent_closed_tabs";
+    private static final String TELEMETRY_EXTRA_MIXED = "recent_tabs_mixed";
+
+    // Recently closed tabs from Gecko.
+    private ClosedTab[] recentlyClosedTabs;
+
+    // "Tabs from last time".
+    private ClosedTab[] lastSessionTabs;
+
+    public static final class ClosedTab {
+        public final String url;
+        public final String title;
+        public final String data;
+
+        public ClosedTab(String url, String title, String data) {
+            this.url = url;
+            this.title = title;
+            this.data = data;
+        }
+    }
+
+    private final Context context;
+    private final RecentTabsUpdateHandler recentTabsUpdateHandler;
+    private final PanelStateUpdateHandler panelStateUpdateHandler;
+
+    public RecentTabsAdapter(Context context,
+                             RecentTabsUpdateHandler recentTabsUpdateHandler,
+                             PanelStateUpdateHandler panelStateUpdateHandler) {
+        this.context = context;
+        this.recentTabsUpdateHandler = recentTabsUpdateHandler;
+        this.panelStateUpdateHandler = panelStateUpdateHandler;
+        recentlyClosedTabs = new ClosedTab[0];
+        lastSessionTabs = new ClosedTab[0];
+
+        readPreviousSessionData();
+    }
+
+    public void startListeningForClosedTabs() {
+        EventDispatcher.getInstance().registerGeckoThreadListener(this, "ClosedTabs:Data");
+        GeckoAppShell.notifyObservers("ClosedTabs:StartNotifications", null);
+    }
+
+    public void stopListeningForClosedTabs() {
+        GeckoAppShell.notifyObservers("ClosedTabs:StopNotifications", null);
+        EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "ClosedTabs:Data");
+    }
+
+    @Override
+    public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
+        final NativeJSObject[] tabs = message.getObjectArray("tabs");
+        final int length = tabs.length;
+
+        final ClosedTab[] closedTabs = new ClosedTab[length];
+        for (int i = 0; i < length; i++) {
+            final NativeJSObject tab = tabs[i];
+            closedTabs[i] = new ClosedTab(tab.getString("url"), tab.getString("title"), tab.getObject("data").toString());
+        }
+
+        // Only modify recentlyClosedTabs on the UI thread.
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // Save some data about the old panel state, so we can be
+                // smarter about notifying the recycler view which bits changed.
+                int prevClosedTabsCount = recentlyClosedTabs.length;
+                boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
+                int prevSectionHeaderIndex = getSectionHeaderIndex();
+
+                recentlyClosedTabs = closedTabs;
+                recentTabsUpdateHandler.onRecentTabsCountUpdated(getClosedTabsCount());
+                panelStateUpdateHandler.onPanelStateUpdated();
+
+                // Handle the section header hiding/unhiding.
+                updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
+
+                // Update the "Recently closed" part of the tab list.
+                updateTabsList(prevClosedTabsCount, recentlyClosedTabs.length, getFirstRecentTabIndex(), getLastRecentTabIndex());
+            }
+        });
+    }
+
+    private void readPreviousSessionData() {
+        // Make sure that the start up code has had a chance to update sessionstore.bak as necessary.
+        GeckoProfile.get(context).waitForOldSessionDataProcessing();
+
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                final String jsonString = GeckoProfile.get(context).readSessionFile(true);
+                if (jsonString == null) {
+                    // No previous session data.
+                    return;
+                }
+
+                final List<ClosedTab> parsedTabs = new ArrayList<>();
+
+                new SessionParser() {
+                    @Override
+                    public void onTabRead(SessionTab tab) {
+                        final String url = tab.getUrl();
+
+                        // Don't show last tabs for about:home
+                        if (AboutPages.isAboutHome(url)) {
+                            return;
+                        }
+
+                        parsedTabs.add(new ClosedTab(url, tab.getTitle(), tab.getTabObject().toString()));
+                    }
+                }.parse(jsonString);
+
+                final ClosedTab[] closedTabs = parsedTabs.toArray(new ClosedTab[parsedTabs.size()]);
+
+                // Only modify lastSessionTabs on the UI thread.
+                ThreadUtils.postToUiThread(new Runnable() {
+                    @Override
+                    public void run() {
+                        // Save some data about the old panel state, so we can be
+                        // smarter about notifying the recycler view which bits changed.
+                        int prevClosedTabsCount = lastSessionTabs.length;
+                        boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
+                        int prevSectionHeaderIndex = getSectionHeaderIndex();
+
+                        lastSessionTabs = closedTabs;
+                        recentTabsUpdateHandler.onRecentTabsCountUpdated(getClosedTabsCount());
+                        panelStateUpdateHandler.onPanelStateUpdated();
+
+                        // Handle the section header hiding/unhiding.
+                        updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
+
+                        // Update the "Tabs from last time" part of the tab list.
+                        updateTabsList(prevClosedTabsCount, lastSessionTabs.length, getFirstLastSessionTabIndex(), getLastLastSessionTabIndex());
+                    }
+                });
+            }
+        });
+    }
+
+    public void clearLastSessionData() {
+        final ClosedTab[] emptyLastSessionTabs = new ClosedTab[0];
+
+        // Only modify mLastSessionTabs on the UI thread.
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // Save some data about the old panel state, so we can be
+                // smarter about notifying the recycler view which bits changed.
+                int prevClosedTabsCount = lastSessionTabs.length;
+                boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
+                int prevSectionHeaderIndex = getSectionHeaderIndex();
+
+                lastSessionTabs = emptyLastSessionTabs;
+                recentTabsUpdateHandler.onRecentTabsCountUpdated(getClosedTabsCount());
+                panelStateUpdateHandler.onPanelStateUpdated();
+
+                // Handle the section header hiding.
+                updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
+
+                // Handle the "tabs from last time" being cleared.
+                if (prevClosedTabsCount > 0) {
+                    notifyItemRangeRemoved(getFirstLastSessionTabIndex(), prevClosedTabsCount);
+                }
+            }
+        });
+    }
+
+    private void updateHeaderVisibility(boolean prevSectionHeaderVisibility, int prevSectionHeaderIndex) {
+        if (prevSectionHeaderVisibility && !isSectionHeaderVisible()) {
+            notifyItemRemoved(prevSectionHeaderIndex);
+        } else if (!prevSectionHeaderVisibility && isSectionHeaderVisible()) {
+            notifyItemInserted(getSectionHeaderIndex());
+        }
+    }
+
+    /**
+     * Updates the tab list as necessary to account for any changes in tab count in a particular data source.
+     *
+     * Since the session store only sends out full updates, we don't know for sure what has changed compared
+     * to the last data set, so we can only animate if the tab count actually changes.
+     *
+     * @param prevClosedTabsCount The previous number of closed tabs from that data source.
+     * @param closedTabsCount The current number of closed tabs contained in that data source.
+     * @param firstTabListIndex The current position of that data source's first item in the RecyclerView.
+     * @param lastTabListIndex The current position of that data source's last item in the RecyclerView.
+     */
+    private void updateTabsList(int prevClosedTabsCount, int closedTabsCount, int firstTabListIndex, int lastTabListIndex) {
+        final int closedTabsCountChange = closedTabsCount - prevClosedTabsCount;
+
+        if (closedTabsCountChange <= 0) {
+            notifyItemRangeRemoved(lastTabListIndex + 1, -closedTabsCountChange); // Remove tabs from the bottom of the list.
+            notifyItemRangeChanged(firstTabListIndex, closedTabsCount); // Update the contents of the remaining items.
+        } else { // closedTabsCountChange > 0
+            notifyItemRangeInserted(firstTabListIndex, closedTabsCountChange); // Add additional tabs at the top of the list.
+            notifyItemRangeChanged(firstTabListIndex + closedTabsCountChange, prevClosedTabsCount); // Update any previous list items.
+        }
+    }
+
+    public String restoreTabFromPosition(int position) {
+        final List<String> dataList = new ArrayList<>(1);
+        dataList.add(getClosedTabForPosition(position).data);
+
+        final String telemetryExtra =
+                position > getLastRecentTabIndex() ? TELEMETRY_EXTRA_LAST_TIME : TELEMETRY_EXTRA_RECENTY_CLOSED;
+
+        restoreSessionWithHistory(dataList);
+
+        return telemetryExtra;
+    }
+
+    public String restoreAllTabs() {
+        if (recentlyClosedTabs.length == 0 && lastSessionTabs.length == 0) {
+            return null;
+        }
+
+        final List<String> dataList = new ArrayList<>(getClosedTabsCount());
+        addTabDataToList(dataList, recentlyClosedTabs);
+        addTabDataToList(dataList, lastSessionTabs);
+
+        final String telemetryExtra = recentlyClosedTabs.length > 0 && lastSessionTabs.length > 0 ? TELEMETRY_EXTRA_MIXED :
+                recentlyClosedTabs.length > 0 ? TELEMETRY_EXTRA_RECENTY_CLOSED : TELEMETRY_EXTRA_LAST_TIME;
+
+        restoreSessionWithHistory(dataList);
+
+        return telemetryExtra;
+    }
+
+    private void addTabDataToList(List<String> dataList, ClosedTab[] closedTabs) {
+        for (ClosedTab closedTab : closedTabs) {
+            dataList.add(closedTab.data);
+        }
+    }
+
+    private static void restoreSessionWithHistory(List<String> dataList) {
+        final JSONObject json = new JSONObject();
+        try {
+            json.put("tabs", new JSONArray(dataList));
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "JSON error", e);
+        }
+
+        GeckoAppShell.notifyObservers("Session:RestoreRecentTabs", json.toString());
+    }
+
+    @Override
+    public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
+        final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
+        final View view;
+
+        final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
+
+        switch (itemType) {
+            case NAVIGATION_BACK:
+                view = inflater.inflate(R.layout.home_combined_back_item, parent, false);
+                return new CombinedHistoryItem.HistoryItem(view);
+
+            case SECTION_HEADER:
+                view = inflater.inflate(R.layout.home_header_row, parent, false);
+                return new CombinedHistoryItem.BasicItem(view);
+
+            case CLOSED_TAB:
+                view = inflater.inflate(R.layout.home_item_row, parent, false);
+                return new CombinedHistoryItem.HistoryItem(view);
+        }
+        return null;
+    }
+
+    @Override
+    public void onBindViewHolder(CombinedHistoryItem holder, final int position) {
+        final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
+
+        switch (itemType) {
+            case SECTION_HEADER:
+                ((TextView) holder.itemView).setText(context.getString(R.string.home_closed_tabs_title2));
+                break;
+
+            case CLOSED_TAB:
+                final ClosedTab closedTab = getClosedTabForPosition(position);
+                ((CombinedHistoryItem.HistoryItem) holder).bind(closedTab);
+                break;
+        }
+    }
+
+    @Override
+    public int getItemCount() {
+        int itemCount = 1; // NAVIGATION_BACK button is always visible.
+
+        if (isSectionHeaderVisible()) {
+            itemCount += 1;
+        }
+
+        itemCount += getClosedTabsCount();
+
+        return itemCount;
+    }
+
+    private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
+        if (position == NAVIGATION_BACK_BUTTON_INDEX) {
+            return ItemType.NAVIGATION_BACK;
+        }
+
+        if (position == getSectionHeaderIndex() && isSectionHeaderVisible()) {
+            return ItemType.SECTION_HEADER;
+        }
+
+        return ItemType.CLOSED_TAB;
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
+    }
+
+    public int getClosedTabsCount() {
+        return recentlyClosedTabs.length + lastSessionTabs.length;
+    }
+
+    private boolean isSectionHeaderVisible() {
+        return recentlyClosedTabs.length > 0 || lastSessionTabs.length > 0;
+    }
+
+    private int getSectionHeaderIndex() {
+        return isSectionHeaderVisible() ?
+                NAVIGATION_BACK_BUTTON_INDEX + 1 :
+                NAVIGATION_BACK_BUTTON_INDEX;
+    }
+
+    private int getFirstRecentTabIndex() {
+        return getSectionHeaderIndex() + 1;
+    }
+
+    private int getLastRecentTabIndex() {
+        return getSectionHeaderIndex() + recentlyClosedTabs.length;
+    }
+
+    private int getFirstLastSessionTabIndex() {
+        return getLastRecentTabIndex() + 1;
+    }
+
+    private int getLastLastSessionTabIndex() {
+        return getLastRecentTabIndex() + lastSessionTabs.length;
+    }
+
+    /**
+     * Get the closed tab corresponding to a RecyclerView list item.
+     *
+     * The Recent Tab folder combines two data sources, so if we want to get the ClosedTab object
+     * behind a certain list item, we need to route this request to the corresponding data source
+     * and also transform the global list position into a local array index.
+     */
+    private ClosedTab getClosedTabForPosition(int position) {
+        final ClosedTab closedTab;
+        if (position <= getLastRecentTabIndex()) { // Upper part of the list is "Recently closed tabs".
+            closedTab = recentlyClosedTabs[position - getFirstRecentTabIndex()];
+        } else { // Lower part is "Tabs from last time".
+            closedTab = lastSessionTabs[position - getFirstLastSessionTabIndex()];
+        }
+
+        return closedTab;
+    }
+
+    @Override
+    public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
+        final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
+        final HomeContextMenuInfo info;
+
+        switch (itemType) {
+            case CLOSED_TAB:
+                info = new HomeContextMenuInfo(view, position, -1);
+                ClosedTab closedTab = getClosedTabForPosition(position);
+                return populateChildInfoFromTab(info, closedTab);
+        }
+
+        return null;
+    }
+
+    protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, ClosedTab tab) {
+        info.url = tab.url;
+        info.title = tab.title;
+        return info;
+    }
+}
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/home/RecentTabsPanel.java
+++ /dev/null
@@ -1,443 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko.home;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.mozilla.gecko.AboutPages;
-import org.mozilla.gecko.EventDispatcher;
-import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.GeckoProfile;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.SessionParser;
-import org.mozilla.gecko.Telemetry;
-import org.mozilla.gecko.TelemetryContract;
-import org.mozilla.gecko.db.BrowserContract.CommonColumns;
-import org.mozilla.gecko.db.BrowserContract.URLColumns;
-import org.mozilla.gecko.reader.SavedReaderViewHelper;
-import org.mozilla.gecko.util.EventCallback;
-import org.mozilla.gecko.util.NativeEventListener;
-import org.mozilla.gecko.util.NativeJSObject;
-import org.mozilla.gecko.util.ThreadUtils;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.database.MatrixCursor.RowBuilder;
-import android.os.Bundle;
-import android.support.v4.app.LoaderManager;
-import android.support.v4.content.Loader;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewStub;
-import android.widget.AdapterView;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-/**
- * Fragment that displays tabs from last session in a ListView.
- */
-public class RecentTabsPanel extends HomeFragment
-                             implements NativeEventListener {
-    // Logging tag name
-    @SuppressWarnings("unused")
-    private static final String LOGTAG = "GeckoRecentTabsPanel";
-
-    // Cursor loader ID for the loader that loads recent tabs
-    private static final int LOADER_ID_RECENT_TABS = 0;
-
-    private static final String TELEMETRY_EXTRA_LAST_TIME = "recent_tabs_last_time";
-    private static final String TELEMETRY_EXTRA_CLOSED = "recent_closed_tabs";
-
-    // Adapter for the list of recent tabs.
-    private RecentTabsAdapter mAdapter;
-
-    // The view shown by the fragment.
-    private HomeListView mList;
-
-    // Reference to the View to display when there are no results.
-    private View mEmptyView;
-
-    // Callbacks used for the search and favicon cursor loaders
-    private CursorLoaderCallbacks mCursorLoaderCallbacks;
-
-    // Recently closed tabs from gecko
-    private ClosedTab[] mClosedTabs;
-
-    private void restoreSessionWithHistory(List<String> dataList) {
-        JSONObject json = new JSONObject();
-        try {
-            json.put("tabs", new JSONArray(dataList));
-        } catch (JSONException e) {
-            Log.e(LOGTAG, "JSON error", e);
-        }
-
-        GeckoAppShell.notifyObservers("Session:RestoreRecentTabs", json.toString());
-    }
-
-    private static final class ClosedTab {
-        public final String url;
-        public final String title;
-        public final String data;
-
-        public ClosedTab(String url, String title, String data) {
-            this.url = url;
-            this.title = title;
-            this.data = data;
-        }
-    }
-
-    public static final class RecentTabs implements URLColumns, CommonColumns {
-        public static final String TYPE = "type";
-        public static final String DATA = "data";
-
-        public static final int TYPE_HEADER = 0;
-        public static final int TYPE_LAST_TIME = 1;
-        public static final int TYPE_CLOSED = 2;
-        public static final int TYPE_OPEN_ALL_LAST_TIME = 3;
-        public static final int TYPE_OPEN_ALL_CLOSED = 4;
-    }
-
-    @Override
-    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-        return inflater.inflate(R.layout.home_list_panel, container, false);
-    }
-
-    @Override
-    public void onViewCreated(View view, Bundle savedInstanceState) {
-        mList = (HomeListView) view.findViewById(R.id.list);
-        mList.setTag(HomePager.LIST_TAG_RECENT_TABS);
-
-        mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
-            @Override
-            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-                final Cursor c = mAdapter.getCursor();
-                if (c == null || !c.moveToPosition(position)) {
-                    return;
-                }
-
-                final int itemType = c.getInt(c.getColumnIndexOrThrow(RecentTabs.TYPE));
-
-                if (itemType == RecentTabs.TYPE_OPEN_ALL_LAST_TIME) {
-                    openTabsWithType(RecentTabs.TYPE_LAST_TIME);
-                    return;
-                }
-
-                if (itemType == RecentTabs.TYPE_OPEN_ALL_CLOSED) {
-                    openTabsWithType(RecentTabs.TYPE_CLOSED);
-                    return;
-                }
-
-                final String extras = (itemType == RecentTabs.TYPE_CLOSED) ? TELEMETRY_EXTRA_CLOSED : TELEMETRY_EXTRA_LAST_TIME;
-                Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, extras);
-
-                final List<String> dataList = new ArrayList<>();
-                dataList.add(c.getString(c.getColumnIndexOrThrow(RecentTabs.DATA)));
-                restoreSessionWithHistory(dataList);
-            }
-        });
-
-        mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
-            @Override
-            public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
-                // Don't show context menus for the "Open all" rows.
-                final int itemType = cursor.getInt(cursor.getColumnIndexOrThrow(RecentTabs.TYPE));
-                if (itemType == RecentTabs.TYPE_OPEN_ALL_LAST_TIME || itemType == RecentTabs.TYPE_OPEN_ALL_CLOSED) {
-                    return null;
-                }
-
-                final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
-                info.url = cursor.getString(cursor.getColumnIndexOrThrow(RecentTabs.URL));
-                info.title = cursor.getString(cursor.getColumnIndexOrThrow(RecentTabs.TITLE));
-                return info;
-            }
-        });
-
-        registerForContextMenu(mList);
-
-        EventDispatcher.getInstance().registerGeckoThreadListener(this, "ClosedTabs:Data");
-        GeckoAppShell.notifyObservers("ClosedTabs:StartNotifications", null);
-    }
-
-    @Override
-    public void onDestroyView() {
-        super.onDestroyView();
-
-        // Discard any additional item clicks on the list as the
-        // panel is getting destroyed (bug 1210243).
-        mList.setOnItemClickListener(null);
-
-        mList = null;
-        mEmptyView = null;
-
-        EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "ClosedTabs:Data");
-        GeckoAppShell.notifyObservers("ClosedTabs:StopNotifications", null);
-    }
-
-    @Override
-    public void onActivityCreated(Bundle savedInstanceState) {
-        super.onActivityCreated(savedInstanceState);
-
-        // Intialize adapter
-        mAdapter = new RecentTabsAdapter(getActivity());
-        mList.setAdapter(mAdapter);
-
-        // Create callbacks before the initial loader is started
-        mCursorLoaderCallbacks = new CursorLoaderCallbacks();
-        loadIfVisible();
-    }
-
-    private void updateUiFromCursor(Cursor c) {
-        if (c != null && c.getCount() > 0) {
-            return;
-        }
-
-        if (mEmptyView == null) {
-            // Set empty panel view. We delay this so that the empty view won't flash.
-            final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
-            mEmptyView = emptyViewStub.inflate();
-
-            final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image);
-            emptyIcon.setImageResource(R.drawable.icon_remote_tabs_empty);
-
-            final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
-            emptyText.setText(R.string.home_last_tabs_empty);
-
-            mList.setEmptyView(mEmptyView);
-        }
-    }
-
-    @Override
-    protected void load() {
-        getLoaderManager().initLoader(LOADER_ID_RECENT_TABS, null, mCursorLoaderCallbacks);
-    }
-
-    @Override
-    public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
-        final NativeJSObject[] tabs = message.getObjectArray("tabs");
-        final int length = tabs.length;
-
-        final ClosedTab[] closedTabs = new ClosedTab[length];
-        for (int i = 0; i < length; i++) {
-            final NativeJSObject tab = tabs[i];
-            closedTabs[i] = new ClosedTab(tab.getString("url"), tab.getString("title"), tab.getObject("data").toString());
-        }
-
-        // Only modify mClosedTabs on the UI thread
-        ThreadUtils.postToUiThread(new Runnable() {
-            @Override
-            public void run() {
-                mClosedTabs = closedTabs;
-
-                // The fragment might have been detached before this code
-                // runs in the UI thread.
-                if (getActivity() != null) {
-                    // Reload the cursor to show recently closed tabs.
-                    getLoaderManager().restartLoader(LOADER_ID_RECENT_TABS, null, mCursorLoaderCallbacks);
-                }
-            }
-        });
-    }
-
-    private void openTabsWithType(int type) {
-        final Cursor c = mAdapter.getCursor();
-        if (c == null || !c.moveToFirst()) {
-            return;
-        }
-
-        final List<String> dataList = new ArrayList<String>();
-        do {
-            if (c.getInt(c.getColumnIndexOrThrow(RecentTabs.TYPE)) == type) {
-                dataList.add(c.getString(c.getColumnIndexOrThrow(RecentTabs.DATA)));
-            }
-        } while (c.moveToNext());
-
-        final String extras = (type == RecentTabs.TYPE_CLOSED) ? TELEMETRY_EXTRA_CLOSED : TELEMETRY_EXTRA_LAST_TIME;
-        Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.BUTTON, extras);
-
-        restoreSessionWithHistory(dataList);
-    }
-
-    private static class RecentTabsCursorLoader extends SimpleCursorLoader {
-        private final ClosedTab[] closedTabs;
-
-        public RecentTabsCursorLoader(Context context, ClosedTab[] closedTabs) {
-            super(context);
-            this.closedTabs = closedTabs;
-        }
-
-        private void addRow(MatrixCursor c, String url, String title, int type, String data) {
-            final RowBuilder row = c.newRow();
-            row.add(-1);
-            row.add(url);
-            row.add(title);
-            row.add(type);
-            row.add(data);
-        }
-
-        @Override
-        public Cursor loadCursor() {
-            // TwoLinePageRow requires the SavedReaderViewHelper to be initialised. Usually this is
-            // done as part of BrowserDatabaseHelper.onOpen(), however we don't actually access
-            // the DB when showing the Recent Tabs panel, hence it's possible that the SavedReaderViewHelper
-            // isn't loaded. Therefore we need to explicitly force loading here.
-            // Note: loadCursor is run on a background thread, hence it's safe to do this here.
-            // (loading time is a few ms, and hence shouldn't impact overall loading time for this
-            // panel in any significant way).
-            SavedReaderViewHelper.getSavedReaderViewHelper(getContext()).loadItems();
-
-            final Context context = getContext();
-
-            final MatrixCursor c = new MatrixCursor(new String[] { RecentTabs._ID,
-                                                                   RecentTabs.URL,
-                                                                   RecentTabs.TITLE,
-                                                                   RecentTabs.TYPE,
-                                                                   RecentTabs.DATA});
-
-            if (closedTabs != null && closedTabs.length > 0) {
-                // How many closed tabs are actually displayed.
-                int visibleClosedTabs = 0;
-
-                final int length = closedTabs.length;
-                for (int i = 0; i < length; i++) {
-                    final String url = closedTabs[i].url;
-
-                    // Don't show recent tabs for about:home or about:privatebrowsing.
-                    if (!AboutPages.isTitlelessAboutPage(url)) {
-                        // If this is the first closed tab we're adding, add a header for the section.
-                        if (visibleClosedTabs == 0) {
-                            addRow(c, null, context.getString(R.string.home_closed_tabs_title), RecentTabs.TYPE_HEADER, null);
-                        }
-                        addRow(c, url, closedTabs[i].title, RecentTabs.TYPE_CLOSED, closedTabs[i].data);
-                        visibleClosedTabs++;
-                    }
-                }
-
-                // Add an "Open all" button if more than 2 tabs were added to the list.
-                if (visibleClosedTabs > 1) {
-                    addRow(c, null, null, RecentTabs.TYPE_OPEN_ALL_CLOSED, null);
-                }
-            }
-
-            // We need to ensure that the session restore code has updated sessionstore.bak as necessary.
-            GeckoProfile.get(context).waitForOldSessionDataProcessing();
-
-            final String jsonString = GeckoProfile.get(context).readSessionFile(true);
-            if (jsonString == null) {
-                // No previous session data
-                return c;
-            }
-
-            final int count = c.getCount();
-
-            new SessionParser() {
-                @Override
-                public void onTabRead(SessionTab tab) {
-                    final String url = tab.getUrl();
-
-                    // Don't show last tabs for about:home
-                    if (AboutPages.isAboutHome(url)) {
-                        return;
-                    }
-
-                    // If this is the first tab we're reading, add a header.
-                    if (c.getCount() == count) {
-                        addRow(c, null, context.getString(R.string.home_last_tabs_title), RecentTabs.TYPE_HEADER, null);
-                    }
-
-                    addRow(c, url, tab.getTitle(), RecentTabs.TYPE_LAST_TIME, tab.getTabObject().toString());
-                }
-            }.parse(jsonString);
-
-            // Add an "Open all" button if more than 2 tabs were added to the list (account for the header)
-            if (c.getCount() - count > 2) {
-                addRow(c, null, null, RecentTabs.TYPE_OPEN_ALL_LAST_TIME, null);
-            }
-
-            return c;
-        }
-    }
-
-    private static class RecentTabsAdapter extends MultiTypeCursorAdapter {
-        private static final int ROW_HEADER = 0;
-        private static final int ROW_STANDARD = 1;
-        private static final int ROW_OPEN_ALL = 2;
-
-        private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER, ROW_OPEN_ALL };
-        private static final int[] LAYOUT_TYPES =
-            new int[] { R.layout.home_item_row, R.layout.home_header_row, R.layout.home_open_all_row };
-
-        public RecentTabsAdapter(Context context) {
-            super(context, null, VIEW_TYPES, LAYOUT_TYPES);
-        }
-
-        @Override
-        public int getItemViewType(int position) {
-            final Cursor c = getCursor(position);
-            final int type = c.getInt(c.getColumnIndexOrThrow(RecentTabs.TYPE));
-
-            if (type == RecentTabs.TYPE_HEADER) {
-                return ROW_HEADER;
-            }
-
-            if (type == RecentTabs.TYPE_OPEN_ALL_LAST_TIME || type == RecentTabs.TYPE_OPEN_ALL_CLOSED) {
-                return ROW_OPEN_ALL;
-            }
-
-            return ROW_STANDARD;
-         }
-
-        @Override
-        public boolean isEnabled(int position) {
-            return (getItemViewType(position) != ROW_HEADER);
-        }
-
-        @Override
-        public void bindView(View view, Context context, int position) {
-            final int itemType = getItemViewType(position);
-            if (itemType == ROW_OPEN_ALL) {
-                return;
-            }
-
-            final Cursor c = getCursor(position);
-
-            if (itemType == ROW_HEADER) {
-                final String title = c.getString(c.getColumnIndexOrThrow(RecentTabs.TITLE));
-                final TextView textView = (TextView) view;
-                textView.setText(title);
-            } else if (itemType == ROW_STANDARD) {
-                final TwoLinePageRow pageRow = (TwoLinePageRow) view;
-                pageRow.setShowIcons(false);
-                pageRow.updateFromCursor(c);
-            }
-         }
-    }
-
-    private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
-        @Override
-        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-            return new RecentTabsCursorLoader(getActivity(), mClosedTabs);
-        }
-
-        @Override
-        public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
-            mAdapter.swapCursor(c);
-            updateUiFromCursor(c);
-        }
-
-        @Override
-        public void onLoaderReset(Loader<Cursor> loader) {
-            mAdapter.swapCursor(null);
-        }
-    }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
@@ -12,16 +12,18 @@ import android.util.Log;
 import ch.boye.httpclientandroidlib.HttpHeaders;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.restrictions.Restrictable;
+import org.mozilla.gecko.restrictions.Restrictions;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.Resource;
 import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
 import org.mozilla.gecko.util.DateUtil;
 import org.mozilla.gecko.util.NetworkUtils;
 import org.mozilla.gecko.util.StringUtils;
@@ -197,16 +199,22 @@ public class TelemetryUploadService exte
             return false;
         }
 
         if (!GeckoPreferences.getBooleanPref(context, GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true)) {
             Log.d(LOGTAG, "Telemetry upload opt-out");
             return false;
         }
 
+        if (Restrictions.isRestrictedProfile(context) &&
+                !Restrictions.isAllowed(context, Restrictable.HEALTH_REPORT)) {
+            Log.d(LOGTAG, "Telemetry upload feature disabled by admin profile");
+            return false;
+        }
+
         return true;
     }
 
     /**
      * Determines if the telemetry upload feature is enabled via profile & application level configurations. This is the
      * preferred method.
      *
      * You may wish to also check if the network is connected when calling this method.
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -39,17 +39,16 @@
 <!ENTITY firstrun_account_message "Have &brandShortName; on another device?">
 
 <!ENTITY  onboard_start_restricted1 "Stay safe and in control with this simplified version of &brandShortName;.">
 
 <!-- Localization note: These are used as the titles of different pages on the home screen.
      They are automatically converted to all caps by the Android platform. -->
 <!ENTITY  bookmarks_title "Bookmarks">
 <!ENTITY  history_title "History">
-<!ENTITY  recent_tabs_title "Recent Tabs">
 
 <!ENTITY  switch_to_tab "Switch to tab">
 
 <!ENTITY  crash_reporter_title "&brandShortName; Crash Reporter">
 <!ENTITY  crash_message2 "&brandShortName; had a problem and crashed. Your tabs should be listed on the &brandShortName; Start page when you restart.">
 <!ENTITY  crash_send_report_message3 "Tell &vendorShortName; about this crash so they can fix it">
 <!ENTITY  crash_include_url2 "Include the address of the page I was on">
 <!ENTITY  crash_sorry "We\'re sorry">
@@ -565,20 +564,22 @@ size. -->
 <!ENTITY home_synced_devices_smartfolder "Synced devices">
 <!ENTITY home_synced_devices_number "&formatD; devices">
 <!-- Localization note (home_synced_devices_one_device): This is the singular version of home_synced_devices_number, referring to the number of devices a user has synced. -->
 <!ENTITY home_synced_devices_one "1 device">
 <!ENTITY home_history_back_to2 "Back to full History">
 <!ENTITY home_clear_history_button "Clear browsing history">
 <!ENTITY home_clear_history_confirm "Are you sure you want to clear your history?">
 <!ENTITY home_bookmarks_empty "Bookmarks you save show up here.">
-<!ENTITY home_closed_tabs_title "Recently closed tabs">
-<!ENTITY home_last_tabs_title "Tabs from last time">
+<!ENTITY home_closed_tabs_title2 "Recently closed">
 <!ENTITY home_last_tabs_empty "Your recent tabs show up here.">
-<!ENTITY home_open_all "Open all">
+<!ENTITY home_restore_all "Restore all">
+<!ENTITY home_closed_tabs_number "&formatD; tabs">
+<!-- Localization note (home_closed_tabs_one): This is the singular version of home_closed_tabs_number, referring to the number of recently closed tabs available. -->
+<!ENTITY home_closed_tabs_one "1 tab">
 <!ENTITY home_most_recent_empty "Websites you visited most recently show up here.">
 <!-- Localization note (home_most_recent_emptyhint2): "Psst" is a sound that might be used to attract someone's attention unobtrusively, and intended to hint at Private Browsing to the user.
      The placeholders &formatS1; and &formatS2; are used to mark the location of text underlining. -->
 <!ENTITY home_most_recent_emptyhint2 "Psst: using a &formatS1;New Private Tab&formatS2; won\'t save your history.">
 
 <!-- Localization note (home_default_empty): This string is used as the default text when there
      is no data to show in an about:home panel that was created by an add-on. -->
 <!ENTITY home_default_empty "No content could be found for this panel.">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -426,17 +426,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'home/PanelLayout.java',
     'home/PanelListView.java',
     'home/PanelRecyclerView.java',
     'home/PanelRecyclerViewAdapter.java',
     'home/PanelRefreshLayout.java',
     'home/PanelViewAdapter.java',
     'home/PanelViewItemHandler.java',
     'home/PinSiteDialog.java',
-    'home/RecentTabsPanel.java',
+    'home/RecentTabsAdapter.java',
     'home/RemoteTabsExpandableListState.java',
     'home/SearchEngine.java',
     'home/SearchEngineAdapter.java',
     'home/SearchEngineBar.java',
     'home/SearchEngineRow.java',
     'home/SearchLoader.java',
     'home/SimpleCursorLoader.java',
     'home/SpacingDecoration.java',
new file mode 100755
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d93658f08917bc75aa15258f6380ffab2815856a
GIT binary patch
literal 774
zc%17D@N?(olHy`uVBq!ia0vp^W+2SL3?z5Yp7kC`F%}28J29*~C-V}>nH=B~;tHhy
zgTa0<ICA93i4!Ld9z1yX@L?buC<0_3I&=ugH~<DfE`)>tAR8hDC*dlQ0Ukj#DIoXy
z((8AD&N3(o@(X5QWM<*v6<1VK*U`0iaP;&F4UbCCt*LG3oibzY+HLz!-@0?}{-eh)
zU%mPK<=f9+{}w;wzX0^+Q%@Ji5Rc<?!)}%}83@dc2<ZyY_F!}@)b0{J`@cR`ev?1~
zljTnT#wYW9(%$JXI8O+jGkyB5%vatsk83lrDby7%m%YPX-{pBo*tLMmy))i@)xnS6
z-cfQdIQYEhh+ZiXDQ(!eP2Z~VdYQwlw^x{->u}xqd53q=n>GLLpRF=!E%ml3J?@>y
zwLX2}or)!X_l4AJPrU7$+gNkGu5oYm#_8{a|7;Smt682KaXs|w-BSv+-1XZE1wK~g
zO_A2}(tcX}S>)s7TLR@Pbrwts`dY30?s1i(Q=n$gMFml-WPZ+8^Ai@m9Q#`s7+SLb
zUHts8fsI?PU6_$U-c)e0r*SQV#tySNnX?Xm4Pp?OciP^-x4GrHb))G18^#MO|9s#|
zT32bXknw!GMA5brXEG$M=Q^Jhn<%N9wA_2gCf5Wv?i(+)3qJ1F@O@Bzwtkll?}9|`
z^eJk^2NUP_E;xHOqu9~dXp@jz9hdS}y@T1B*0mSe8CrQ7%<gEU?AZFqN%82?sYY#K
zr<4kXgrjC#+%j=puw>;n4TXU2rgbces(<}89d|XEzj-~kX}R|LDB;S=t0JH6ejSau
zZ1ro6<Ld*`q2FU87H`Vw-sXE_+tb6x3l0>_dC7RVXwF8tc{%Q;w+~*Z;@K)&bzAVa
dl~vMzW_61z>ED0s`2kEU44$rjF6*2UngFeuf@J^z
deleted file mode 100644
--- a/mobile/android/base/resources/layout-large-land-v11/home_list.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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/. -->
-
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <ViewStub android:id="@+id/home_empty_view_stub"
-              android:layout="@layout/home_empty_panel"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"/>
-
-    <org.mozilla.gecko.home.HomeListView
-            android:id="@+id/list"
-            style="@style/Widget.Home.HomeList"
-            android:layout_width="match_parent"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
-
-</merge>
deleted file mode 100644
--- a/mobile/android/base/resources/layout-xlarge-v11/home_list.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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/. -->
-
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <ViewStub android:id="@+id/home_empty_view_stub"
-              android:layout="@layout/home_empty_panel"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"/>
-
-    <org.mozilla.gecko.home.HomeListView
-            android:id="@+id/list"
-            style="@style/Widget.Home.HomeList"
-            android:layout_width="match_parent"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
-
-</merge>
--- a/mobile/android/base/resources/layout/home_combined_history_panel.xml
+++ b/mobile/android/base/resources/layout/home_combined_history_panel.xml
@@ -7,17 +7,17 @@
               android:layout_width="match_parent"
               android:layout_height="match_parent"
               android:orientation="vertical">
 
     <android.support.v4.widget.SwipeRefreshLayout
             android:id="@+id/refresh_layout"
             android:layout_width="match_parent"
             android:layout_height="0dp"
-            android:layout_weight="1">
+            android:layout_weight="1.2">
 
         <org.mozilla.gecko.home.CombinedHistoryRecyclerView
                 android:id="@+id/combined_recycler_view"
                 android:layout_width="match_parent"
                 android:layout_height="match_parent"
                 android:scrollbars="vertical"/>
 
     </android.support.v4.widget.SwipeRefreshLayout>
@@ -31,16 +31,22 @@
 
     <include android:id="@+id/home_clients_empty_view"
               layout="@layout/history_sync_setup"
               android:layout_width="match_parent"
               android:layout_height="0dp"
               android:layout_weight="3"
               android:visibility="gone"/>
 
-    <Button android:id="@+id/clear_history_button"
+    <include android:id="@+id/home_recent_tabs_empty_view"
+              layout="@layout/home_empty_panel"
+              android:layout_width="match_parent"
+              android:layout_height="0dp"
+              android:layout_weight="3"
+              android:visibility="gone"/>
+
+    <Button android:id="@+id/history_panel_footer_button"
             style="@style/Widget.Home.ActionButton"
-            android:text="@string/home_clear_history_button"
             android:layout_width="match_parent"
             android:layout_height="48dp"
             android:visibility="gone" />
 
 </LinearLayout>
deleted file mode 100644
--- a/mobile/android/base/resources/layout/home_list.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?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/. -->
-
-<merge xmlns:android="http://schemas.android.com/apk/res/android">
-
-    <ViewStub android:id="@+id/home_empty_view_stub"
-              android:layout="@layout/home_empty_panel"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"/>
-
-    <org.mozilla.gecko.home.HomeListView
-            android:id="@+id/list"
-            style="@style/Widget.Home.HomeList"
-            android:layout_width="match_parent"
-            android:layout_height="0dp"
-            android:layout_weight="1"/>
-
-</merge>
deleted file mode 100644
--- a/mobile/android/base/resources/layout/home_list_panel.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-<?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/. -->
-
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              android:layout_width="match_parent"
-              android:layout_height="match_parent"
-              android:orientation="vertical">
-
-    <include layout="@layout/home_list"/>
-
-</LinearLayout>
deleted file mode 100644
--- a/mobile/android/base/resources/layout/home_open_all_row.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-<?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/. -->
-
-<TextView xmlns:android="http://schemas.android.com/apk/res/android"
-          android:text="@string/home_open_all"
-          style="@style/Widget.Home.ActionItem"/>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -60,17 +60,16 @@
 
   <string name="firstrun_account_title">&firstrun_account_title;</string>
   <string name="firstrun_account_message">&firstrun_account_message;</string>
 
   <string name="firstrun_welcome_restricted">&onboard_start_restricted1;</string>
 
   <string name="bookmarks_title">&bookmarks_title;</string>
   <string name="history_title">&history_title;</string>
-  <string name="recent_tabs_title">&recent_tabs_title;</string>
 
   <string name="switch_to_tab">&switch_to_tab;</string>
 
   <string name="crash_reporter_title">&crash_reporter_title;</string>
   <string name="crash_message2">&crash_message2;</string>
   <string name="crash_send_report_message3">&crash_send_report_message3;</string>
   <string name="crash_include_url2">&crash_include_url2;</string>
   <string name="crash_sorry">&crash_sorry;</string>
@@ -447,20 +446,21 @@
   <string name="home_history_title">&home_history_title;</string>
   <string name="home_synced_devices_smartfolder">&home_synced_devices_smartfolder;</string>
   <string name="home_synced_devices_number">&home_synced_devices_number;</string>
   <string name="home_synced_devices_one">&home_synced_devices_one;</string>
   <string name="home_history_back_to">&home_history_back_to2;</string>
   <string name="home_clear_history_button">&home_clear_history_button;</string>
   <string name="home_clear_history_confirm">&home_clear_history_confirm;</string>
   <string name="home_bookmarks_empty">&home_bookmarks_empty;</string>
-  <string name="home_closed_tabs_title">&home_closed_tabs_title;</string>
-  <string name="home_last_tabs_title">&home_last_tabs_title;</string>
+  <string name="home_closed_tabs_title2">&home_closed_tabs_title2;</string>
   <string name="home_last_tabs_empty">&home_last_tabs_empty;</string>
-  <string name="home_open_all">&home_open_all;</string>
+  <string name="home_restore_all">&home_restore_all;</string>
+  <string name="home_closed_tabs_number">&home_closed_tabs_number;</string>
+  <string name="home_closed_tabs_one">&home_closed_tabs_one;</string>
   <string name="home_most_recent_empty">&home_most_recent_empty;</string>
   <string name="home_most_recent_emptyhint">&home_most_recent_emptyhint2;</string>
   <string name="home_default_empty">&home_default_empty;</string>
   <string name="home_move_back_to_filter">&home_move_back_to_filter;</string>
   <string name="home_remote_tabs_many_hidden_devices">&home_remote_tabs_many_hidden_devices;</string>
   <string name="home_remote_tabs_hidden_devices_title">&home_remote_tabs_hidden_devices_title;</string>
   <string name="home_remote_tabs_unhide_selected_devices">&home_remote_tabs_unhide_selected_devices;</string>
   <string name="pin_site_dialog_hint">&pin_site_dialog_hint;</string>
--- a/mobile/android/components/extensions/ext-pageAction.js
+++ b/mobile/android/components/extensions/ext-pageAction.js
@@ -1,15 +1,18 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
                                   "resource://devtools/shared/event-emitter.js");
 
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+
 // Import the android PageActions module.
 XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
                                   "resource://gre/modules/PageActions.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 
 var {
   SingletonEventManager,
@@ -18,22 +21,32 @@ var {
 // WeakMap[Extension -> PageAction]
 var pageActionMap = new WeakMap();
 
 function PageAction(options, extension) {
   this.id = null;
 
   let DEFAULT_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
 
+  this.popupUrl = options.default_popup;
+
   this.options = {
     title: options.default_title || extension.name,
     icon: DEFAULT_ICON,
     id: extension.id,
     clickCallback: () => {
-      this.emit("click");
+      if (this.popupUrl) {
+        let win = Services.wm.getMostRecentWindow("navigator:browser");
+        win.BrowserApp.addTab(this.popupUrl, {
+          selected: true,
+          parentId: win.BrowserApp.selectedTab.id,
+        });
+      } else {
+        this.emit("click");
+      }
     },
   };
 
   EventEmitter.decorate(this);
 }
 
 PageAction.prototype = {
   show(tabId) {
@@ -45,16 +58,26 @@ PageAction.prototype = {
 
   hide(tabId) {
     if (this.id) {
       PageActions.remove(this.id);
       this.id = null;
     }
   },
 
+  setPopup(tab, url) {
+    // TODO: Only set the popup for the specified tab once we have Tabs API support.
+    this.popupUrl = url;
+  },
+
+  getPopup(tab) {
+    // TODO: Only return the popup for the specified tab once we have Tabs API support.
+    return this.popupUrl;
+  },
+
   shutdown() {
     this.hide();
   },
 };
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("manifest_page_action", (type, directive, extension, manifest) => {
   let pageAction = new PageAction(manifest.page_action, extension);
@@ -80,14 +103,29 @@ extensions.registerSchemaAPI("pageAction
         return () => {
           pageActionMap.get(extension).off("click", listener);
         };
       }).api(),
 
       show(tabId) {
         pageActionMap.get(extension).show(tabId);
       },
+
       hide(tabId) {
         pageActionMap.get(extension).hide(tabId);
       },
+
+      setPopup(details) {
+        // TODO: Use the Tabs API to get the tab from details.tabId.
+        let tab = null;
+        let url = details.popup && context.uri.resolve(details.popup);
+        pageActionMap.get(extension).setPopup(tab, url);
+      },
+
+      getPopup(details) {
+        // TODO: Use the Tabs API to get the tab from details.tabId.
+        let tab = null;
+        let popup = pageActionMap.get(extension).getPopup(tab);
+        return Promise.resolve(popup);
+      },
     },
   };
 });
--- a/mobile/android/components/extensions/schemas/page_action.json
+++ b/mobile/android/components/extensions/schemas/page_action.json
@@ -19,17 +19,16 @@
                 "preprocess": "localize"
               },
               "default_icon": {
                 "unsupported": true,
                 "$ref": "IconPath",
                 "optional": true
               },
               "default_popup": {
-                "unsupported": true,
                 "type": "string",
                 "format": "relativeUrl",
                 "optional": true,
                 "preprocess": "localize"
               },
               "browser_style": {
                 "type": "boolean",
                 "optional": true
@@ -156,17 +155,16 @@
             "name": "callback",
             "optional": true,
             "parameters": []
           }
         ]
       },
       {
         "name": "setPopup",
-        "unsupported": true,
         "type": "function",
         "description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
               "tabId": {"type": "integer", "minimum": 0, "description": "The id of the tab for which you want to modify the page action."},
@@ -175,17 +173,16 @@
                 "description": "The html file to show in a popup.  If set to the empty string (''), no popup is shown."
               }
             }
           }
         ]
       },
       {
         "name": "getPopup",
-        "unsupported": true,
         "type": "function",
         "description": "Gets the html document set as the popup for this page action.",
         "async": "callback",
         "parameters": [
           {
             "name": "details",
             "type": "object",
             "properties": {
--- a/mobile/android/components/extensions/test/mochitest/chrome.ini
+++ b/mobile/android/components/extensions/test/mochitest/chrome.ini
@@ -1,5 +1,6 @@
 [DEFAULT]
 support-files =
   head.js
 
-[test_ext_pageAction.html]
\ No newline at end of file
+[test_ext_pageAction.html]
+[test_ext_pageAction_popup.html]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/extensions/test/mochitest/test_ext_pageAction_popup.html
@@ -0,0 +1,153 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>PageAction Test</title>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
+  <script type="text/javascript" src="head.js"></script>
+  <link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<script type="text/javascript">
+"use strict";
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+add_task(function* test_contentscript() {
+  function backgroundScript() {
+    // TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
+    let tabId = 1;
+    let onClickedListenerEnabled = false;
+
+    browser.test.onMessage.addListener((msg, details) => {
+      if (msg === "page-action-show") {
+        // TODO: switch to using .show(tabId).then(...) once bug 1270742 lands.
+        browser.pageAction.show(tabId);
+        browser.test.sendMessage("page-action-shown");
+      } else if (msg == "page-action-set-popup") {
+        browser.pageAction.setPopup({popup: details.name, tabId: tabId});
+        browser.test.sendMessage("page-action-popup-set");
+      } else if (msg == "page-action-get-popup") {
+        browser.pageAction.getPopup({tabId: tabId}).then(url => {
+          browser.test.sendMessage("page-action-got-popup", url);
+        });
+      } else if (msg == "page-action-enable-onClicked-listener") {
+        onClickedListenerEnabled = true;
+        browser.test.sendMessage("page-action-onClicked-listener-enabled");
+      } else if (msg == "page-action-disable-onClicked-listener") {
+        onClickedListenerEnabled = false;
+        browser.test.sendMessage("page-action-onClicked-listener-disabled");
+      }
+    });
+
+    browser.pageAction.onClicked.addListener(tab => {
+      browser.test.assertTrue(onClickedListenerEnabled, "The onClicked listener should only fire when it is enabled.");
+      browser.test.sendMessage("page-action-onClicked-fired");
+    });
+
+    browser.test.sendMessage("ready");
+  }
+
+  function popupScript() {
+    window.onload = () => {
+      browser.test.sendMessage("page-action-from-popup", location.href);
+    };
+    browser.test.onMessage.addListener((msg, details) => {
+      if (msg == "page-action-close-popup") {
+        if (details.location == location.href) {
+          window.close();
+        }
+      }
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript}())`,
+    manifest: {
+      "name": "PageAction Extension",
+      "page_action": {
+        "default_title": "Page Action",
+        "default_popup": "default.html",
+      },
+    },
+    files: {
+      "default.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
+      "a.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
+      "b.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
+      "popup.js": `(${popupScript})()`,
+    },
+  });
+
+  let tabClosedPromise = () => {
+    return new Promise(resolve => {
+      let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+      let BrowserApp = chromeWin.BrowserApp;
+
+      let tabCloseListener = (event) => {
+        BrowserApp.deck.removeEventListener("TabClose", tabCloseListener, false);
+        let browser = event.target;
+        let url = browser.currentURI.spec;
+        resolve(url);
+      };
+
+      BrowserApp.deck.addEventListener("TabClose", tabCloseListener, false);
+    });
+  };
+
+  function* testPopup(name) {
+    // We don't need to set the popup when testing default_popup.
+    if (name != "default.html") {
+      extension.sendMessage("page-action-set-popup", {name});
+      yield extension.awaitMessage("page-action-popup-set");
+    }
+
+    extension.sendMessage("page-action-get-popup");
+    let url = yield extension.awaitMessage("page-action-got-popup");
+
+    if (name == "") {
+      ok(url == name, "Calling pageAction.getPopup should return an empty string when the popup is not set.");
+
+      // The onClicked listener should get called when the popup is set to an empty string.
+      extension.sendMessage("page-action-enable-onClicked-listener");
+      yield extension.awaitMessage("page-action-onClicked-listener-enabled");
+
+      clickPageAction(extension.id);
+      yield extension.awaitMessage("page-action-onClicked-fired");
+
+      extension.sendMessage("page-action-disable-onClicked-listener");
+      yield extension.awaitMessage("page-action-onClicked-listener-disabled");
+    } else {
+      ok(url.includes(name), "Calling pageAction.getPopup should return the correct popup URL when the popup is set.");
+
+      clickPageAction(extension.id);
+      let location = yield extension.awaitMessage("page-action-from-popup");
+      ok(location.includes(name), "The popup with the correct URL should be shown.");
+
+      extension.sendMessage("page-action-close-popup", {location});
+
+      url = yield tabClosedPromise();
+      ok(url.includes(name), "The tab for the popup should be closed.");
+    }
+  }
+
+  yield extension.startup();
+  yield extension.awaitMessage("ready");
+
+  extension.sendMessage("page-action-show");
+  yield extension.awaitMessage("page-action-shown");
+  ok(isPageActionShown(extension.id), "The PageAction should be shown.");
+
+  yield testPopup("default.html");
+  yield testPopup("a.html");
+  yield testPopup("");
+  yield testPopup("b.html");
+
+  yield extension.unload();
+  ok(!isPageActionShown(extension.id), "The PageAction should be removed after unload.");
+});
+</script>
+
+</body>
+</html>
--- a/mobile/android/modules/Sanitizer.jsm
+++ b/mobile/android/modules/Sanitizer.jsm
@@ -81,42 +81,51 @@ Sanitizer.prototype = {
 
       get canClear()
       {
         return true;
       }
     },
 
     siteSettings: {
-      clear: function ()
-      {
-        return new Promise(function(resolve, reject) {
-          // Clear site-specific permissions like "Allow this site to open popups"
-          Services.perms.removeAll();
+      clear: Task.async(function* () {
+        // Clear site-specific permissions like "Allow this site to open popups"
+        Services.perms.removeAll();
 
-          // Clear site-specific settings like page-zoom level
-          Cc["@mozilla.org/content-pref/service;1"]
-            .getService(Ci.nsIContentPrefService2)
-            .removeAllDomains(null);
+        // Clear site-specific settings like page-zoom level
+        Cc["@mozilla.org/content-pref/service;1"]
+          .getService(Ci.nsIContentPrefService2)
+          .removeAllDomains(null);
+
+        // Clear "Never remember passwords for this site", which is not handled by
+        // the permission manager
+        var hosts = Services.logins.getAllDisabledHosts({})
+        for (var host of hosts) {
+          Services.logins.setLoginSavingEnabled(host, true);
+        }
 
-          // Clear "Never remember passwords for this site", which is not handled by
-          // the permission manager
-          var hosts = Services.logins.getAllDisabledHosts({})
-          for (var host of hosts) {
-            Services.logins.setLoginSavingEnabled(host, true);
-          }
+        // Clear site security settings
+        var sss = Cc["@mozilla.org/ssservice;1"]
+                    .getService(Ci.nsISiteSecurityService);
+        sss.clearAll();
 
-          // Clear site security settings
-          var sss = Cc["@mozilla.org/ssservice;1"]
-                      .getService(Ci.nsISiteSecurityService);
-          sss.clearAll();
-
-          resolve();
+        // Clear push subscriptions
+        yield new Promise((resolve, reject) => {
+          let push = Cc["@mozilla.org/push/Service;1"]
+                       .getService(Ci.nsIPushService);
+          push.clearForDomain("*", status => {
+            if (Components.isSuccessCode(status)) {
+              resolve();
+            } else {
+              reject(new Error("Error clearing push subscriptions: " +
+                               status));
+            }
+          });
         });
-      },
+      }),
 
       get canClear()
       {
         return true;
       }
     },
 
     offlineApps: {
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/components/AboutHomeComponent.java
@@ -29,18 +29,17 @@ import com.robotium.solo.Solo;
  * A class representing any interactions that take place on the Awesomescreen.
  */
 public class AboutHomeComponent extends BaseComponent {
     private static final String LOGTAG = AboutHomeComponent.class.getSimpleName();
 
     private static final List<PanelType> PANEL_ORDERING = Arrays.asList(
             PanelType.TOP_SITES,
             PanelType.BOOKMARKS,
-            PanelType.COMBINED_HISTORY,
-            PanelType.RECENT_TABS
+            PanelType.COMBINED_HISTORY
     );
 
     // The percentage of the panel to swipe between 0 and 1. This value was set through
     // testing: 0.55f was tested on try and fails on armv6 devices.
     private static final float SWIPE_PERCENTAGE = 0.70f;
 
     public AboutHomeComponent(final UITestContext testContext) {
         super(testContext);
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testAboutHomePageNavigation.java
@@ -5,18 +5,16 @@
 package org.mozilla.gecko.tests;
 
 import org.mozilla.gecko.home.HomeConfig.PanelType;
 import org.mozilla.gecko.tests.helpers.DeviceHelper;
 import org.mozilla.gecko.tests.helpers.GeckoHelper;
 
 /**
  * Tests functionality related to navigating between the various about:home panels.
- *
- * TODO: Update this test to account for recent tabs panel (bug 1028727).
  */
 public class testAboutHomePageNavigation extends UITest {
     // TODO: Define this test dynamically by creating dynamic representations of the Page
     // enum for both phone and tablet, then swiping through the panels. This will also
     // benefit having a HomePager with custom panels.
     public void testAboutHomePageNavigation() {
         GeckoHelper.blockForDelayedStartup();
 
--- a/toolkit/components/downloads/nsDownloadManager.cpp
+++ b/toolkit/components/downloads/nsDownloadManager.cpp
@@ -2323,17 +2323,17 @@ nsDownloadManager::OnEndUpdateBatch()
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDownloadManager::OnVisit(nsIURI *aURI, int64_t aVisitID, PRTime aTime,
                            int64_t aSessionID, int64_t aReferringID,
                            uint32_t aTransitionType, const nsACString& aGUID,
-                           bool aHidden)
+                           bool aHidden, uint32_t aVisitCount, uint32_t aTyped)
 {
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDownloadManager::OnTitleChanged(nsIURI *aURI,
                                   const nsAString &aPageTitle,
                                   const nsACString &aGUID)
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -1032,16 +1032,18 @@ Database::InitFunctions()
   rv = CalculateFrecencyFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = GenerateGUIDFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = FixupURLFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = FrecencyNotificationFunction::create(mMainConn);
   NS_ENSURE_SUCCESS(rv, rv);
+  rv = StoreLastInsertedIdFunction::create(mMainConn);
+  NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 nsresult
 Database::InitTempEntities()
 {
   MOZ_ASSERT(NS_IsMainThread());
--- a/toolkit/components/places/History.cpp
+++ b/toolkit/components/places/History.cpp
@@ -68,36 +68,46 @@ namespace places {
 ////////////////////////////////////////////////////////////////////////////////
 //// VisitData
 
 struct VisitData {
   VisitData()
   : placeId(0)
   , visitId(0)
   , hidden(true)
+  , shouldUpdateHidden(true)
   , typed(false)
   , transitionType(UINT32_MAX)
   , visitTime(0)
   , frecency(-1)
+  , lastVisitId(0)
+  , lastVisitTime(0)
+  , visitCount(0)
+  , referrerVisitId(0)
   , titleChanged(false)
   , shouldUpdateFrecency(true)
   {
     guid.SetIsVoid(true);
     title.SetIsVoid(true);
   }
 
   explicit VisitData(nsIURI* aURI,
                      nsIURI* aReferrer = nullptr)
   : placeId(0)
   , visitId(0)
   , hidden(true)
+  , shouldUpdateHidden(true)
   , typed(false)
   , transitionType(UINT32_MAX)
   , visitTime(0)
   , frecency(-1)
+  , lastVisitId(0)
+  , lastVisitTime(0)
+  , visitCount(0)
+  , referrerVisitId(0)
   , titleChanged(false)
   , shouldUpdateFrecency(true)
   {
     MOZ_ASSERT(aURI);
     if (aURI) {
       (void)aURI->GetSpec(spec);
       (void)GetReversedHostname(aURI, revHost);
     }
@@ -116,56 +126,41 @@ struct VisitData {
    *        TRANSITION_ constants on nsINavHistoryService.
    */
   void SetTransitionType(uint32_t aTransitionType)
   {
     typed = aTransitionType == nsINavHistoryService::TRANSITION_TYPED;
     transitionType = aTransitionType;
   }
 
-  /**
-   * Determines if this refers to the same url as aOther, and updates aOther
-   * with missing information if so.
-   *
-   * @param aOther
-   *        The other place to check against.
-   * @return true if this is a visit for the same place as aOther, false
-   *         otherwise.
-   */
-  bool IsSamePlaceAs(VisitData& aOther)
-  {
-    if (!spec.Equals(aOther.spec)) {
-      return false;
-    }
-
-    aOther.placeId = placeId;
-    aOther.guid = guid;
-    return true;
-  }
-
   int64_t placeId;
   nsCString guid;
   int64_t visitId;
   nsCString spec;
   nsString revHost;
   bool hidden;
+  bool shouldUpdateHidden;
   bool typed;
   uint32_t transitionType;
   PRTime visitTime;
   int32_t frecency;
+  int64_t lastVisitId;
+  PRTime lastVisitTime;
+  uint32_t visitCount;
 
   /**
    * Stores the title.  If this is empty (IsEmpty() returns true), then the
    * title should be removed from the Place.  If the title is void (IsVoid()
    * returns true), then no title has been set on this object, and titleChanged
    * should remain false.
    */
   nsString title;
 
   nsCString referrerSpec;
+  int64_t referrerVisitId;
 
   // TODO bug 626836 hook up hidden and typed change tracking too!
   bool titleChanged;
 
   // Indicates whether frecency should be updated for this visit.
   bool shouldUpdateFrecency;
 };
 
@@ -618,20 +613,18 @@ NS_IMPL_ISUPPORTS_INHERITED(
 )
 
 /**
  * Notifies observers about a visit.
  */
 class NotifyVisitObservers : public Runnable
 {
 public:
-  NotifyVisitObservers(VisitData& aPlace,
-                       VisitData& aReferrer)
+  explicit NotifyVisitObservers(VisitData& aPlace)
   : mPlace(aPlace)
-  , mReferrer(aReferrer)
   , mHistory(History::GetService())
   {
   }
 
   NS_IMETHOD Run()
   {
     MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
 
@@ -652,18 +645,20 @@ public:
     if (!uri) {
       return NS_ERROR_UNEXPECTED;
     }
 
     // Notify the visit.  Note that TRANSITION_EMBED visits are never added
     // to the database, thus cannot be queried and we don't notify them.
     if (mPlace.transitionType != nsINavHistoryService::TRANSITION_EMBED) {
       navHistory->NotifyOnVisit(uri, mPlace.visitId, mPlace.visitTime,
-                                mReferrer.visitId, mPlace.transitionType,
-                                mPlace.guid, mPlace.hidden);
+                                mPlace.referrerVisitId, mPlace.transitionType,
+                                mPlace.guid, mPlace.hidden,
+                                mPlace.visitCount + 1, // Add current visit.
+                                static_cast<uint32_t>(mPlace.typed));
     }
 
     nsCOMPtr<nsIObserverService> obsService =
       mozilla::services::GetObserverService();
     if (obsService) {
       DebugOnly<nsresult> rv =
         obsService->NotifyObservers(uri, URI_VISIT_SAVED, nullptr);
       NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), "Could not notify observers");
@@ -673,17 +668,16 @@ public:
     NS_ENSURE_STATE(history);
     history->AppendToRecentlyVisitedURIs(uri);
     history->NotifyVisited(uri);
 
     return NS_OK;
   }
 private:
   VisitData mPlace;
-  VisitData mReferrer;
   RefPtr<History> mHistory;
 };
 
 /**
  * Notifies observers about a pages title changing.
  */
 class NotifyTitleObservers : public Runnable
 {
@@ -922,61 +916,76 @@ public:
     }
 
     mozStorageTransaction transaction(mDBConn, false,
                                       mozIStorageConnection::TRANSACTION_IMMEDIATE);
 
     VisitData* lastFetchedPlace = nullptr;
     for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
       VisitData& place = mPlaces.ElementAt(i);
-      VisitData& referrer = mReferrers.ElementAt(i);
 
       // Fetching from the database can overwrite this information, so save it
       // apart.
       bool typed = place.typed;
       bool hidden = place.hidden;
 
       // We can avoid a database lookup if it's the same place as the last
       // visit we added.
-      bool known = lastFetchedPlace && lastFetchedPlace->IsSamePlaceAs(place);
+      bool known = lastFetchedPlace && lastFetchedPlace->spec.Equals(place.spec);
       if (!known) {
         nsresult rv = mHistory->FetchPageInfo(place, &known);
         if (NS_FAILED(rv)) {
           if (!!mCallback) {
             nsCOMPtr<nsIRunnable> event =
               new NotifyPlaceInfoCallback(mCallback, place, true, rv);
             return NS_DispatchToMainThread(event);
           }
           return NS_OK;
         }
         lastFetchedPlace = &mPlaces.ElementAt(i);
+      } else {
+        // Copy over the data from the already known place.
+        place.placeId = lastFetchedPlace->placeId;
+        place.guid = lastFetchedPlace->guid;
+        place.lastVisitId = lastFetchedPlace->visitId;
+        place.lastVisitTime = lastFetchedPlace->visitTime;
+        place.titleChanged = !lastFetchedPlace->title.Equals(place.title);
+        place.frecency = lastFetchedPlace->frecency;
+        // Add one visit for the previous loop.
+        place.visitCount = ++(*lastFetchedPlace).visitCount;
       }
 
       // If any transition is typed, ensure the page is marked as typed.
       if (typed != lastFetchedPlace->typed) {
         place.typed = true;
       }
 
       // If any transition is visible, ensure the page is marked as visible.
       if (hidden != lastFetchedPlace->hidden) {
         place.hidden = false;
       }
 
-      FetchReferrerInfo(referrer, place);
-
-      nsresult rv = DoDatabaseInserts(known, place, referrer);
+      // If this is a new page, or the existing page was already visible,
+      // there's no need to try to unhide it.
+      if (!known || !lastFetchedPlace->hidden) {
+        place.shouldUpdateHidden = false;
+      }
+
+      FetchReferrerInfo(place);
+
+      nsresult rv = DoDatabaseInserts(known, place);
       if (!!mCallback) {
         nsCOMPtr<nsIRunnable> event =
           new NotifyPlaceInfoCallback(mCallback, place, true, rv);
         nsresult rv2 = NS_DispatchToMainThread(event);
         NS_ENSURE_SUCCESS(rv2, rv2);
       }
       NS_ENSURE_SUCCESS(rv, rv);
 
-      nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(place, referrer);
+      nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(place);
       rv = NS_DispatchToMainThread(event);
       NS_ENSURE_SUCCESS(rv, rv);
 
       // Notify about title change if needed.
       if ((!known && !place.title.IsVoid()) || place.titleChanged) {
         event = new NotifyTitleObservers(place.spec, place.title, place.guid);
         rv = NS_DispatchToMainThread(event);
         NS_ENSURE_SUCCESS(rv, rv);
@@ -994,325 +1003,211 @@ private:
                     const nsMainThreadPtrHandle<mozIVisitInfoCallback>& aCallback)
   : mDBConn(aConnection)
   , mCallback(aCallback)
   , mHistory(History::GetService())
   {
     MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
 
     mPlaces.SwapElements(aPlaces);
-    mReferrers.SetLength(mPlaces.Length());
-
-    for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
-      mReferrers[i].spec = mPlaces[i].referrerSpec;
 
 #ifdef DEBUG
+    for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
       nsCOMPtr<nsIURI> uri;
       MOZ_ASSERT(NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), mPlaces[i].spec)));
-      NS_ASSERTION(CanAddURI(uri),
-                   "Passed a VisitData with a URI we cannot add to history!");
+      MOZ_ASSERT(CanAddURI(uri),
+                 "Passed a VisitData with a URI we cannot add to history!");
+    }
 #endif
-    }
   }
 
   /**
    * Inserts or updates the entry in moz_places for this visit, adds the visit,
    * and updates the frecency of the place.
    *
    * @param aKnown
    *        True if we already have an entry for this place in moz_places, false
    *        otherwise.
    * @param aPlace
    *        The place we are adding a visit for.
-   * @param aReferrer
-   *        The referrer for aPlace.
    */
   nsresult DoDatabaseInserts(bool aKnown,
-                             VisitData& aPlace,
-                             VisitData& aReferrer)
+                             VisitData& aPlace)
   {
     MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread");
 
     // If the page was in moz_places, we need to update the entry.
     nsresult rv;
     if (aKnown) {
       rv = mHistory->UpdatePlace(aPlace);
       NS_ENSURE_SUCCESS(rv, rv);
     }
     // Otherwise, the page was not in moz_places, so now we have to add it.
     else {
       rv = mHistory->InsertPlace(aPlace);
       NS_ENSURE_SUCCESS(rv, rv);
-
-      // We need the place id and guid of the page we just inserted when we
-      // have a callback or when the GUID isn't known.  No point in doing the
-      // disk I/O if we do not need it.
-      if (!!mCallback || aPlace.guid.IsEmpty()) {
-        bool exists;
-        rv = mHistory->FetchPageInfo(aPlace, &exists);
-        NS_ENSURE_SUCCESS(rv, rv);
-
-        if (!exists) {
-          NS_NOTREACHED("should have an entry in moz_places");
-        }
-      }
+      aPlace.placeId = nsNavHistory::sLastInsertedPlaceId;
     }
-
-    rv = AddVisit(aPlace, aReferrer);
+    MOZ_ASSERT(aPlace.placeId > 0);
+
+    rv = AddVisit(aPlace);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // TODO (bug 623969) we shouldn't update this after each visit, but
     // rather only for each unique place to save disk I/O.
 
     // Don't update frecency if the page should not appear in autocomplete.
     if (aPlace.shouldUpdateFrecency) {
       rv = UpdateFrecency(aPlace);
       NS_ENSURE_SUCCESS(rv, rv);
     }
 
     return NS_OK;
   }
 
   /**
-   * Loads visit information about the page into _place.
-   *
-   * @param _place
-   *        The VisitData for the place we need to know visit information about.
-   * @param [optional] aThresholdStart
-   *        The timestamp of a new visit (not represented by _place) used to
-   *        determine if the page was recently visited or not.
-   * @return true if the page was recently (determined with aThresholdStart)
-   *         visited, false otherwise.
-   */
-  bool FetchVisitInfo(VisitData& _place,
-                      PRTime aThresholdStart = 0)
-  {
-    NS_PRECONDITION(!_place.spec.IsEmpty(), "must have a non-empty spec!");
-
-    nsCOMPtr<mozIStorageStatement> stmt;
-    // If we have a visitTime, we want information on that specific visit.
-    if (_place.visitTime) {
-      stmt = mHistory->GetStatement(
-        "SELECT id, visit_date "
-        "FROM moz_historyvisits "
-        "WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) "
-        "AND visit_date = :visit_date "
-      );
-      NS_ENSURE_TRUE(stmt, false);
-
-      mozStorageStatementScoper scoper(stmt);
-      nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("visit_date"),
-                                          _place.visitTime);
-      NS_ENSURE_SUCCESS(rv, false);
-
-      scoper.Abandon();
-    }
-    // Otherwise, we want information about the most recent visit.
-    else {
-      stmt = mHistory->GetStatement(
-        "SELECT id, visit_date "
-        "FROM moz_historyvisits "
-        "WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) "
-        "ORDER BY visit_date DESC "
-      );
-      NS_ENSURE_TRUE(stmt, false);
-    }
-    mozStorageStatementScoper scoper(stmt);
-
-    nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"),
-                                  _place.spec);
-    NS_ENSURE_SUCCESS(rv, false);
-
-    bool hasResult;
-    rv = stmt->ExecuteStep(&hasResult);
-    NS_ENSURE_SUCCESS(rv, false);
-    if (!hasResult) {
-      return false;
-    }
-
-    rv = stmt->GetInt64(0, &_place.visitId);
-    NS_ENSURE_SUCCESS(rv, false);
-    rv = stmt->GetInt64(1, reinterpret_cast<int64_t*>(&_place.visitTime));
-    NS_ENSURE_SUCCESS(rv, false);
-
-    // If we have been given a visit threshold start time, go ahead and
-    // calculate if we have been recently visited.
-    if (aThresholdStart &&
-        aThresholdStart - _place.visitTime <= RECENT_EVENT_THRESHOLD) {
-      return true;
-    }
-
-    return false;
-  }
-
-  /**
    * Fetches information about a referrer for aPlace if it was a recent
    * visit or not.
    *
-   * @param aReferrer
-   *        The VisitData for the referrer.  This will be populated with
-   *        FetchVisitInfo.
    * @param aPlace
    *        The VisitData for the visit we will eventually add.
    *
    */
-  void FetchReferrerInfo(VisitData& aReferrer,
-                         VisitData& aPlace)
+  void FetchReferrerInfo(VisitData& aPlace)
   {
-    if (aReferrer.spec.IsEmpty()) {
+    if (aPlace.referrerSpec.IsEmpty()) {
       return;
     }
 
-    if (!FetchVisitInfo(aReferrer, aPlace.visitTime)) {
-      // We must change both the place and referrer to indicate that we will
-      // not be using the referrer's data. This behavior has test coverage, so
-      // if this invariant changes, we'll know.
+    VisitData referrer;
+    referrer.spec = aPlace.referrerSpec;
+    // If the referrer is the same as the page, we don't need to fetch it.
+    if (aPlace.referrerSpec.Equals(aPlace.spec)) {
+      referrer = aPlace;
+      // The page last visit id is also the referrer visit id.
+      aPlace.referrerVisitId = aPlace.lastVisitId;
+    } else {
+      bool exists = false;
+      if (NS_SUCCEEDED(mHistory->FetchPageInfo(referrer, &exists)) && exists) {
+        // Copy the referrer last visit id.
+        aPlace.referrerVisitId = referrer.lastVisitId;
+      }
+    }
+
+    // Check if the page has effectively been visited recently, otherwise
+    // discard the referrer info.
+    if (!aPlace.referrerVisitId || !referrer.lastVisitTime ||
+        aPlace.visitTime - referrer.lastVisitTime > RECENT_EVENT_THRESHOLD) {
+      // We will not be using the referrer data.
       aPlace.referrerSpec.Truncate();
-      aReferrer.visitId = 0;
+      aPlace.referrerVisitId = 0;
     }
   }
 
   /**
    * Adds a visit for _place and updates it with the right visit id.
    *
    * @param _place
    *        The VisitData for the place we need to know visit information about.
-   * @param aReferrer
-   *        A reference to the referrer's visit data.
    */
-  nsresult AddVisit(VisitData& _place,
-                    const VisitData& aReferrer)
+  nsresult AddVisit(VisitData& _place)
   {
+    MOZ_ASSERT(_place.placeId > 0);
+
     nsresult rv;
     nsCOMPtr<mozIStorageStatement> stmt;
-    if (_place.placeId) {
-      stmt = mHistory->GetStatement(
-        "INSERT INTO moz_historyvisits "
-          "(from_visit, place_id, visit_date, visit_type, session) "
-        "VALUES (:from_visit, :page_id, :visit_date, :visit_type, 0) "
-      );
-      NS_ENSURE_STATE(stmt);
-      rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), _place.placeId);
-      NS_ENSURE_SUCCESS(rv, rv);
-    }
-    else {
-      stmt = mHistory->GetStatement(
-        "INSERT INTO moz_historyvisits "
-          "(from_visit, place_id, visit_date, visit_type, session) "
-        "VALUES (:from_visit, (SELECT id FROM moz_places WHERE url = :page_url), :visit_date, :visit_type, 0) "
-      );
-      NS_ENSURE_STATE(stmt);
-      rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), _place.spec);
-      NS_ENSURE_SUCCESS(rv, rv);
-    }
+    stmt = mHistory->GetStatement(
+      "INSERT INTO moz_historyvisits "
+        "(from_visit, place_id, visit_date, visit_type, session) "
+      "VALUES (:from_visit, :page_id, :visit_date, :visit_type, 0) "
+    );
+    NS_ENSURE_STATE(stmt);
+    mozStorageStatementScoper scoper(stmt);
+
+    rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), _place.placeId);
+    NS_ENSURE_SUCCESS(rv, rv);
     rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("from_visit"),
-                               aReferrer.visitId);
+                               _place.referrerVisitId);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("visit_date"),
                                _place.visitTime);
     NS_ENSURE_SUCCESS(rv, rv);
     uint32_t transitionType = _place.transitionType;
     MOZ_ASSERT(transitionType >= nsINavHistoryService::TRANSITION_LINK &&
                transitionType <= nsINavHistoryService::TRANSITION_RELOAD,
                  "Invalid transition type!");
     rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("visit_type"),
                                transitionType);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    mozStorageStatementScoper scoper(stmt);
     rv = stmt->Execute();
     NS_ENSURE_SUCCESS(rv, rv);
 
-    // Now that it should be in the database, we need to obtain the id of the
-    // visit we just added.
-    (void)FetchVisitInfo(_place);
+    _place.visitId = nsNavHistory::sLastInsertedVisitId;
+    MOZ_ASSERT(_place.visitId > 0);
 
     return NS_OK;
   }
 
   /**
    * Updates the frecency, and possibly the hidden-ness of aPlace.
    *
    * @param aPlace
    *        The VisitData for the place we want to update.
    */
   nsresult UpdateFrecency(const VisitData& aPlace)
   {
     MOZ_ASSERT(aPlace.shouldUpdateFrecency);
+    MOZ_ASSERT(aPlace.placeId > 0);
 
     nsresult rv;
     { // First, set our frecency to the proper value.
       nsCOMPtr<mozIStorageStatement> stmt;
-      if (aPlace.placeId) {
-        stmt = mHistory->GetStatement(
-          "UPDATE moz_places "
-          "SET frecency = NOTIFY_FRECENCY("
-            "CALCULATE_FRECENCY(:page_id), "
-            "url, guid, hidden, last_visit_date"
-          ") "
-          "WHERE id = :page_id"
-        );
-        NS_ENSURE_STATE(stmt);
-        rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId);
-        NS_ENSURE_SUCCESS(rv, rv);
-      }
-      else {
-        stmt = mHistory->GetStatement(
-          "UPDATE moz_places "
-          "SET frecency = NOTIFY_FRECENCY("
-            "CALCULATE_FRECENCY(id), url, guid, hidden, last_visit_date"
-          ") "
-          "WHERE url = :page_url"
-        );
-        NS_ENSURE_STATE(stmt);
-        rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aPlace.spec);
-        NS_ENSURE_SUCCESS(rv, rv);
-      }
+      stmt = mHistory->GetStatement(
+        "UPDATE moz_places "
+        "SET frecency = NOTIFY_FRECENCY("
+          "CALCULATE_FRECENCY(:page_id), "
+          "url, guid, hidden, last_visit_date"
+        ") "
+        "WHERE id = :page_id"
+      );
+      NS_ENSURE_STATE(stmt);
       mozStorageStatementScoper scoper(stmt);
 
+      rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId);
+      NS_ENSURE_SUCCESS(rv, rv);
+
       rv = stmt->Execute();
       NS_ENSURE_SUCCESS(rv, rv);
     }
 
-    if (!aPlace.hidden) {
+    if (!aPlace.hidden && aPlace.shouldUpdateHidden) {
       // Mark the page as not hidden if the frecency is now nonzero.
       nsCOMPtr<mozIStorageStatement> stmt;
-      if (aPlace.placeId) {
-        stmt = mHistory->GetStatement(
-          "UPDATE moz_places "
-          "SET hidden = 0 "
-          "WHERE id = :page_id AND frecency <> 0"
-        );
-        NS_ENSURE_STATE(stmt);
-        rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId);
-        NS_ENSURE_SUCCESS(rv, rv);
-      }
-      else {
-        stmt = mHistory->GetStatement(
-          "UPDATE moz_places "
-          "SET hidden = 0 "
-          "WHERE url = :page_url AND frecency <> 0"
-        );
-        NS_ENSURE_STATE(stmt);
-        rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aPlace.spec);
-        NS_ENSURE_SUCCESS(rv, rv);
-      }
-
+      stmt = mHistory->GetStatement(
+        "UPDATE moz_places "
+        "SET hidden = 0 "
+        "WHERE id = :page_id AND frecency <> 0"
+      );
+      NS_ENSURE_STATE(stmt);
       mozStorageStatementScoper scoper(stmt);
+
+      rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId);
+      NS_ENSURE_SUCCESS(rv, rv);
+
       rv = stmt->Execute();
       NS_ENSURE_SUCCESS(rv, rv);
     }
 
     return NS_OK;
   }
 
   mozIStorageConnection* mDBConn;
 
   nsTArray<VisitData> mPlaces;
-  nsTArray<VisitData> mReferrers;
 
   nsMainThreadPtrHandle<mozIVisitInfoCallback> mCallback;
 
   /**
    * Strong reference to the History object because we do not want it to
    * disappear out from under us.
    */
   RefPtr<History> mHistory;
@@ -1423,18 +1318,18 @@ public:
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (!exists || !mPlace.titleChanged) {
       // We have no record of this page, or we have no title change, so there
       // is no need to do any further work.
       return NS_OK;
     }
 
-    NS_ASSERTION(mPlace.placeId > 0,
-                 "We somehow have an invalid place id here!");
+    MOZ_ASSERT(mPlace.placeId > 0,
+               "We somehow have an invalid place id here!");
 
     // Now we can update our database record.
     nsCOMPtr<mozIStorageStatement> stmt =
       mHistory->GetStatement(
         "UPDATE moz_places "
         "SET title = :page_title "
         "WHERE id = :page_id "
       );
@@ -1949,18 +1844,17 @@ StoreAndNotifyEmbedVisit(VisitData& aPla
   if (!!aCallback) {
     nsMainThreadPtrHandle<mozIVisitInfoCallback>
       callback(new nsMainThreadPtrHolder<mozIVisitInfoCallback>(aCallback));
     nsCOMPtr<nsIRunnable> event =
       new NotifyPlaceInfoCallback(callback, aPlace, true, NS_OK);
     (void)NS_DispatchToMainThread(event);
   }
 
-  VisitData noReferrer;
-  nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(aPlace, noReferrer);
+  nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(aPlace);
   (void)NS_DispatchToMainThread(event);
 }
 
 } // namespace
 
 ////////////////////////////////////////////////////////////////////////////////
 //// History
 
@@ -2130,20 +2024,21 @@ History::GetIsVisitedStatement(mozIStora
     NS_ENSURE_STATE(dbConn);
     mConcurrentStatementsHolder = new ConcurrentStatementsHolder(dbConn);
   }
   mConcurrentStatementsHolder->GetIsVisitedStatement(aCallback);
   return NS_OK;
 }
 
 nsresult
-History::InsertPlace(const VisitData& aPlace)
+History::InsertPlace(VisitData& aPlace)
 {
-  NS_PRECONDITION(aPlace.placeId == 0, "should not have a valid place id!");
-  NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!");
+  MOZ_ASSERT(aPlace.placeId == 0, "should not have a valid place id!");
+  MOZ_ASSERT(!aPlace.shouldUpdateHidden, "We should not need to update hidden");
+  MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!");
 
   nsCOMPtr<mozIStorageStatement> stmt = GetStatement(
       "INSERT INTO moz_places "
         "(url, title, rev_host, hidden, typed, frecency, guid) "
       "VALUES (:url, :title, :rev_host, :hidden, :typed, :frecency, :guid) "
     );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
@@ -2167,42 +2062,42 @@ History::InsertPlace(const VisitData& aP
   NS_ENSURE_SUCCESS(rv, rv);
   // When inserting a page for a first visit that should not appear in
   // autocomplete, for example an error page, use a zero frecency.
   int32_t frecency = aPlace.shouldUpdateFrecency ? aPlace.frecency : 0;
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("frecency"), frecency);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), aPlace.hidden);
   NS_ENSURE_SUCCESS(rv, rv);
-  nsAutoCString guid(aPlace.guid);
   if (aPlace.guid.IsVoid()) {
-    rv = GenerateGUID(guid);
+    rv = GenerateGUID(aPlace.guid);
     NS_ENSURE_SUCCESS(rv, rv);
   }
-  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), guid);
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aPlace.guid);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = stmt->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Post an onFrecencyChanged observer notification.
   const nsNavHistory* navHistory = nsNavHistory::GetConstHistoryService();
   NS_ENSURE_STATE(navHistory);
-  navHistory->DispatchFrecencyChangedNotification(aPlace.spec, frecency, guid,
+  navHistory->DispatchFrecencyChangedNotification(aPlace.spec, frecency,
+                                                  aPlace.guid,
                                                   aPlace.hidden,
                                                   aPlace.visitTime);
 
   return NS_OK;
 }
 
 nsresult
 History::UpdatePlace(const VisitData& aPlace)
 {
-  NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!");
-  NS_PRECONDITION(aPlace.placeId > 0, "must have a valid place id!");
-  NS_PRECONDITION(!aPlace.guid.IsVoid(), "must have a guid!");
+  MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!");
+  MOZ_ASSERT(aPlace.placeId > 0, "must have a valid place id!");
+  MOZ_ASSERT(!aPlace.guid.IsVoid(), "must have a guid!");
 
   nsCOMPtr<mozIStorageStatement> stmt = GetStatement(
       "UPDATE moz_places "
       "SET title = :title, "
           "hidden = :hidden, "
           "typed = :typed, "
           "guid = :guid "
       "WHERE id = :page_id "
@@ -2233,39 +2128,43 @@ History::UpdatePlace(const VisitData& aP
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
 nsresult
 History::FetchPageInfo(VisitData& _place, bool* _exists)
 {
-  NS_PRECONDITION(!_place.spec.IsEmpty() || !_place.guid.IsEmpty(), "must have either a non-empty spec or guid!");
-  NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!");
+  MOZ_ASSERT(!_place.spec.IsEmpty() || !_place.guid.IsEmpty(), "must have either a non-empty spec or guid!");
+  MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!");
 
   nsresult rv;
 
   // URI takes precedence.
   nsCOMPtr<mozIStorageStatement> stmt;
   bool selectByURI = !_place.spec.IsEmpty();
   if (selectByURI) {
     stmt = GetStatement(
-      "SELECT guid, id, title, hidden, typed, frecency "
-      "FROM moz_places "
+      "SELECT guid, id, title, hidden, typed, frecency, visit_count, last_visit_date, "
+      "(SELECT id FROM moz_historyvisits "
+       "WHERE place_id = h.id AND visit_date = h.last_visit_date) AS last_visit_id "
+      "FROM moz_places h "
       "WHERE url = :page_url "
     );
     NS_ENSURE_STATE(stmt);
 
     rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), _place.spec);
     NS_ENSURE_SUCCESS(rv, rv);
   }
   else {
     stmt = GetStatement(
-      "SELECT url, id, title, hidden, typed, frecency "
-      "FROM moz_places "
+      "SELECT url, id, title, hidden, typed, frecency, visit_count, last_visit_date, "
+      "(SELECT id FROM moz_historyvisits "
+       "WHERE place_id = h.id AND visit_date = h.last_visit_date) AS last_visit_id "
+      "FROM moz_places h "
       "WHERE guid = :guid "
     );
     NS_ENSURE_STATE(stmt);
 
     rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), _place.guid);
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
@@ -2318,16 +2217,25 @@ History::FetchPageInfo(VisitData& _place
 
   int32_t typed;
   rv = stmt->GetInt32(4, &typed);
   NS_ENSURE_SUCCESS(rv, rv);
   _place.typed = !!typed;
 
   rv = stmt->GetInt32(5, &_place.frecency);
   NS_ENSURE_SUCCESS(rv, rv);
+  int32_t visitCount;
+  rv = stmt->GetInt32(6, &visitCount);
+  NS_ENSURE_SUCCESS(rv, rv);
+  _place.visitCount = visitCount;
+  rv = stmt->GetInt64(7, &_place.lastVisitTime);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = stmt->GetInt64(8, &_place.lastVisitId);
+  NS_ENSURE_SUCCESS(rv, rv);
+
   return NS_OK;
 }
 
 MOZ_DEFINE_MALLOC_SIZE_OF(HistoryMallocSizeOf)
 
 NS_IMETHODIMP
 History::CollectReports(nsIHandleReportCallback* aHandleReport,
                         nsISupports* aData, bool aAnonymize)
--- a/toolkit/components/places/History.h
+++ b/toolkit/components/places/History.h
@@ -64,17 +64,17 @@ public:
   nsresult GetIsVisitedStatement(mozIStorageCompletionCallback* aCallback);
 
   /**
    * Adds an entry in moz_places with the data in aVisitData.
    *
    * @param aVisitData
    *        The visit data to use to populate a new row in moz_places.
    */
-  nsresult InsertPlace(const VisitData& aVisitData);
+  nsresult InsertPlace(VisitData& aVisitData);
 
   /**
    * Updates an entry in moz_places with the data in aVisitData.
    *
    * @param aVisitData
    *        The visit data to use to update the existing row in moz_places.
    */
   nsresult UpdatePlace(const VisitData& aVisitData);
--- a/toolkit/components/places/SQLFunctions.cpp
+++ b/toolkit/components/places/SQLFunctions.cpp
@@ -839,10 +839,60 @@ namespace places {
 
     RefPtr<nsVariant> result = new nsVariant();
     rv = result->SetAsInt32(newFrecency);
     NS_ENSURE_SUCCESS(rv, rv);
     result.forget(_result);
     return NS_OK;
   }
 
+////////////////////////////////////////////////////////////////////////////////
+//// Store Last Inserted Id Function
+
+  StoreLastInsertedIdFunction::~StoreLastInsertedIdFunction()
+  {
+  }
+
+  /* static */
+  nsresult
+  StoreLastInsertedIdFunction::create(mozIStorageConnection *aDBConn)
+  {
+    RefPtr<StoreLastInsertedIdFunction> function =
+      new StoreLastInsertedIdFunction();
+    nsresult rv = aDBConn->CreateFunction(
+      NS_LITERAL_CSTRING("store_last_inserted_id"), 2, function
+    );
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+  }
+
+  NS_IMPL_ISUPPORTS(
+    StoreLastInsertedIdFunction,
+    mozIStorageFunction
+  )
+
+  NS_IMETHODIMP
+  StoreLastInsertedIdFunction::OnFunctionCall(mozIStorageValueArray *aArgs,
+                                              nsIVariant **_result)
+  {
+    uint32_t numArgs;
+    nsresult rv = aArgs->GetNumEntries(&numArgs);
+    NS_ENSURE_SUCCESS(rv, rv);
+    MOZ_ASSERT(numArgs == 2);
+
+    nsAutoCString table;
+    rv = aArgs->GetUTF8String(0, table);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    int64_t lastInsertedId = aArgs->AsInt64(1);
+
+    nsNavHistory::StoreLastInsertedId(table, lastInsertedId);
+
+    RefPtr<nsVariant> result = new nsVariant();
+    rv = result->SetAsInt64(lastInsertedId);
+    NS_ENSURE_SUCCESS(rv, rv);
+    result.forget(_result);
+    return NS_OK;
+  }
+
 } // namespace places
 } // namespace mozilla
--- a/toolkit/components/places/SQLFunctions.h
+++ b/toolkit/components/places/SQLFunctions.h
@@ -319,12 +319,40 @@ public:
    *
    * @param aDBConn
    *        The database connection to register with.
    */
   static nsresult create(mozIStorageConnection *aDBConn);
 };
 
 
+////////////////////////////////////////////////////////////////////////////////
+//// Store Last Inserted Id Function
+
+/**
+ * Store the last inserted id for reference purpose.
+ *
+ * @param tableName
+ *        The table name.
+ * @param id
+ *        The last inserted id.
+ * @return null
+ */
+class StoreLastInsertedIdFunction final : public mozIStorageFunction
+{
+  ~StoreLastInsertedIdFunction();
+public:
+  NS_DECL_THREADSAFE_ISUPPORTS
+  NS_DECL_MOZISTORAGEFUNCTION
+
+  /**
+   * Registers the function with the specified database connection.
+   *
+   * @param aDBConn
+   *        The database connection to register with.
+   */
+  static nsresult create(mozIStorageConnection *aDBConn);
+};
+
 } // namespace places
 } // namespace mozilla
 
 #endif // mozilla_places_SQLFunctions_h_
--- a/toolkit/components/places/nsINavHistoryService.idl
+++ b/toolkit/components/places/nsINavHistoryService.idl
@@ -639,40 +639,54 @@ interface nsINavHistoryObserver : nsISup
 
   /**
    * Notifies you that we are done doing a bunch of things and you should go
    * ahead and update UI, etc.
    */
   void onEndUpdateBatch();
 
   /**
-   * Called when a resource is visited. This is called the first time a
-   * resource (page, image, etc.) is seen as well as every subsequent time.
+   * Called everytime a URI is visited.
    *
-   * Normally, transition types of TRANSITION_EMBED (corresponding to images in
-   * a page, for example) are not displayed in history results (unless
-   * includeHidden is set). Many observers can ignore _EMBED notifications
-   * (which will comprise the majority of visit notifications) to save work.
+   * @note TRANSITION_EMBED visits (corresponding to images in a page, for
+   *       example) are not displayed in history results. Most observers can
+   *       ignore TRANSITION_EMBED visit notifications (which will comprise the
+   *       majority of visit notifications) to save work.
    *
-   * @param aVisitID        ID of the visit that was just created.
-   * @param aTime           Time of the visit
-   * @param aSessionID      No longer supported (always set to 0).
-   * @param aReferringID    The ID of the visit the user came from. 0 if empty.
-   * @param aTransitionType One of nsINavHistory.TRANSITION_*
-   * @param aGUID           The unique ID associated with the page.
-   * @param aHidden         Whether the visited page is marked as hidden.
+   * @param aVisitId
+   *        Id of the visit that was just created.
+   * @param aTime
+   *        Time of the visit.
+   * @param aSessionId
+   *        No longer supported and always set to 0.
+   * @param aReferrerVisitId
+   *        The id of the visit the user came from, defaults to 0 for no referrer.
+   * @param aTransitionType
+   *        One of nsINavHistory.TRANSITION_*
+   * @param aGuid
+   *        The unique id associated with the page.
+   * @param aHidden
+   *        Whether the visited page is marked as hidden.
+   * @param aVisitCount
+   *        Number of visits (included this one) for this URI.
+   * @param aTyped
+   *        Whether the URI has been typed or not.
+   *        TODO (Bug 1271801): This will become a count, rather than a boolean.
+   *        For future compatibility, always compare it with "> 0".
    */
   void onVisit(in nsIURI aURI,
-               in long long aVisitID,
+               in long long aVisitId,
                in PRTime aTime,
-               in long long aSessionID,
-               in long long aReferringID,
+               in long long aSessionId,
+               in long long aReferrerVisitId,
                in unsigned long aTransitionType,
-               in ACString aGUID,
-               in boolean aHidden);
+               in ACString aGuid,
+               in boolean aHidden,
+               in unsigned long aVisitCount,
+               in unsigned long aTyped);
 
   /**
    * Called whenever either the "real" title or the custom title of the page
    * changed. BOTH TITLES ARE ALWAYS INCLUDED in this notification, even though
    * only one will change at a time. Often, consumers will want to display the
    * user title if it is available, and fall back to the page title (the one
    * specified in the <title> tag of the page).
    *
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -2668,17 +2668,17 @@ nsNavBookmarks::OnEndUpdateBatch()
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
                         int64_t aSessionID, int64_t aReferringID,
                         uint32_t aTransitionType, const nsACString& aGUID,
-                        bool aHidden)
+                        bool aHidden, uint32_t aVisitCount, uint32_t aTyped)
 {
   NS_ENSURE_ARG(aURI);
 
   // If the page is bookmarked, notify observers for each associated bookmark.
   ItemVisitData visitData;
   nsresult rv = aURI->GetSpec(visitData.bookmark.url);
   NS_ENSURE_SUCCESS(rv, rv);
   visitData.visitId = aVisitId;
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -400,18 +400,18 @@ nsNavHistory::GetOrCreateIdForPage(nsIUR
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (*_pageId != 0) {
     return NS_OK;
   }
 
   // Create a new hidden, untyped and unvisited entry.
   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
-    "INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid) "
-    "VALUES (:page_url, :rev_host, :hidden, :frecency, GENERATE_GUID()) "
+    "INSERT INTO moz_places (url, rev_host, hidden, frecency, guid) "
+    "VALUES (:page_url, :rev_host, :hidden, :frecency, :guid) "
   );
   NS_ENSURE_STATE(stmt);
   mozStorageStatementScoper scoper(stmt);
 
   rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aURI);
   NS_ENSURE_SUCCESS(rv, rv);
   // host (reversed with trailing period)
   nsAutoString revHost;
@@ -426,38 +426,26 @@ nsNavHistory::GetOrCreateIdForPage(nsIUR
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), 1);
   NS_ENSURE_SUCCESS(rv, rv);
   nsAutoCString spec;
   rv = aURI->GetSpec(spec);
   NS_ENSURE_SUCCESS(rv, rv);
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("frecency"),
                              IsQueryURI(spec) ? 0 : -1);
   NS_ENSURE_SUCCESS(rv, rv);
+  nsAutoCString guid;
+  rv = GenerateGUID(_GUID);
+  NS_ENSURE_SUCCESS(rv, rv);
+  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), _GUID);
+  NS_ENSURE_SUCCESS(rv, rv);
 
   rv = stmt->Execute();
   NS_ENSURE_SUCCESS(rv, rv);
 
-  {
-    nsCOMPtr<mozIStorageStatement> getIdStmt = mDB->GetStatement(
-      "SELECT id, guid FROM moz_places WHERE url = :page_url "
-    );
-    NS_ENSURE_STATE(getIdStmt);
-    mozStorageStatementScoper getIdScoper(getIdStmt);
-
-    rv = URIBinder::Bind(getIdStmt, NS_LITERAL_CSTRING("page_url"), aURI);
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    bool hasResult = false;
-    rv = getIdStmt->ExecuteStep(&hasResult);
-    NS_ENSURE_SUCCESS(rv, rv);
-    NS_ASSERTION(hasResult, "hasResult is false but the call succeeded?");
-    *_pageId = getIdStmt->AsInt64(0);
-    rv = getIdStmt->GetUTF8String(1, _GUID);
-    NS_ENSURE_SUCCESS(rv, rv);
-  }
+  *_pageId = sLastInsertedPlaceId;
 
   return NS_OK;
 }
 
 
 void
 nsNavHistory::LoadPrefs()
 {
@@ -492,38 +480,40 @@ nsNavHistory::LoadPrefs()
   FRECENCY_PREF(mDefaultWeight,            PREF_FREC_DEFAULT_BUCKET_WEIGHT);
 
 #undef FRECENCY_PREF
 }
 
 
 void
 nsNavHistory::NotifyOnVisit(nsIURI* aURI,
-                          int64_t aVisitID,
+                          int64_t aVisitId,
                           PRTime aTime,
-                          int64_t referringVisitID,
+                          int64_t aReferrerVisitId,
                           int32_t aTransitionType,
-                          const nsACString& aGUID,
-                          bool aHidden)
+                          const nsACString& aGuid,
+                          bool aHidden,
+                          uint32_t aVisitCount,
+                          uint32_t aTyped)
 {
-  MOZ_ASSERT(!aGUID.IsEmpty());
+  MOZ_ASSERT(!aGuid.IsEmpty());
   // If there's no history, this visit will surely add a day.  If the visit is
   // added before or after the last cached day, the day count may have changed.
   // Otherwise adding multiple visits in the same day should not invalidate
   // the cache.
   if (mDaysOfHistory == 0) {
     mDaysOfHistory = 1;
   } else if (aTime > mLastCachedEndOfDay || aTime < mLastCachedStartOfDay) {
     mDaysOfHistory = -1;
   }
 
   NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                    nsINavHistoryObserver,
-                   OnVisit(aURI, aVisitID, aTime, 0,
-                           referringVisitID, aTransitionType, aGUID, aHidden));
+                   OnVisit(aURI, aVisitId, aTime, 0, aReferrerVisitId,
+                           aTransitionType, aGuid, aHidden, aVisitCount, aTyped));
 }
 
 void
 nsNavHistory::NotifyTitleChange(nsIURI* aURI,
                                 const nsString& aTitle,
                                 const nsACString& aGUID)
 {
   MOZ_ASSERT(!aGUID.IsEmpty());
@@ -608,16 +598,31 @@ nsNavHistory::DispatchFrecencyChangedNot
                                                   PRTime aLastVisitDate) const
 {
   nsCOMPtr<nsIRunnable> notif = new FrecencyNotification(aSpec, aNewFrecency,
                                                          aGUID, aHidden,
                                                          aLastVisitDate);
   (void)NS_DispatchToMainThread(notif);
 }
 
+Atomic<int64_t> nsNavHistory::sLastInsertedPlaceId(0);
+Atomic<int64_t> nsNavHistory::sLastInsertedVisitId(0);
+
+void // static
+nsNavHistory::StoreLastInsertedId(const nsACString& aTable,
+                                  const int64_t aLastInsertedId) {
+  if (aTable.Equals(NS_LITERAL_CSTRING("moz_places"))) {
+    nsNavHistory::sLastInsertedPlaceId = aLastInsertedId;
+  } else if (aTable.Equals(NS_LITERAL_CSTRING("moz_historyvisits"))) {
+    nsNavHistory::sLastInsertedVisitId = aLastInsertedId;
+  } else {
+    MOZ_ASSERT(false, "Trying to store the insert id for an unknown table?");
+  }
+}
+
 int32_t
 nsNavHistory::GetDaysOfHistory() {
   MOZ_ASSERT(NS_IsMainThread(), "This can only be called on the main thread");
 
   if (mDaysOfHistory != -1)
     return mDaysOfHistory;
 
   // SQLite doesn't have a CEIL() function, so we must do that later.
--- a/toolkit/components/places/nsNavHistory.h
+++ b/toolkit/components/places/nsNavHistory.h
@@ -22,16 +22,17 @@
 #include "nsToolkitCompsCID.h"
 #include "nsURIHashKey.h"
 #include "nsTHashtable.h"
 
 #include "nsNavHistoryResult.h"
 #include "nsNavHistoryQuery.h"
 #include "Database.h"
 #include "mozilla/Attributes.h"
+#include "mozilla/Atomics.h"
 
 #define QUERYUPDATE_TIME 0
 #define QUERYUPDATE_SIMPLE 1
 #define QUERYUPDATE_COMPLEX 2
 #define QUERYUPDATE_COMPLEX_WITH_BOOKMARKS 3
 #define QUERYUPDATE_HOST 4
 
 // Clamp title and URL to generously large, but not too large, length.
@@ -425,22 +426,24 @@ public:
   {
     return mNumVisitsForFrecency;
   }
 
   /**
    * Fires onVisit event to nsINavHistoryService observers
    */
   void NotifyOnVisit(nsIURI* aURI,
-                     int64_t aVisitID,
+                     int64_t aVisitId,
                      PRTime aTime,
-                     int64_t referringVisitID,
+                     int64_t aReferrerVisitId,
                      int32_t aTransitionType,
-                     const nsACString& aGUID,
-                     bool aHidden);
+                     const nsACString& aGuid,
+                     bool aHidden,
+                     uint32_t aVisitCount,
+                     uint32_t aTyped);
 
   /**
    * Fires onTitleChanged event to nsINavHistoryService observers
    */
   void NotifyTitleChange(nsIURI* aURI,
                          const nsString& title,
                          const nsACString& aGUID);
 
@@ -462,16 +465,25 @@ public:
    * Posts a runnable to the main thread that calls NotifyFrecencyChanged.
    */
   void DispatchFrecencyChangedNotification(const nsACString& aSpec,
                                            int32_t aNewFrecency,
                                            const nsACString& aGUID,
                                            bool aHidden,
                                            PRTime aLastVisitDate) const;
 
+  /**
+   * Store last insterted id for a table.
+   */
+  static mozilla::Atomic<int64_t> sLastInsertedPlaceId;
+  static mozilla::Atomic<int64_t> sLastInsertedVisitId;
+
+  static void StoreLastInsertedId(const nsACString& aTable,
+                                  const int64_t aLastInsertedId);
+
   bool isBatching() {
     return mBatchLevel > 0;
   }
 
 private:
   ~nsNavHistory();
 
   // used by GetHistoryService
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -4592,20 +4592,25 @@ nsNavHistoryResult::OnItemMoved(int64_t 
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavHistoryResult::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
                             int64_t aSessionId, int64_t aReferringId,
                             uint32_t aTransitionType, const nsACString& aGUID,
-                            bool aHidden)
+                            bool aHidden, uint32_t aVisitCount, uint32_t aTyped)
 {
   NS_ENSURE_ARG(aURI);
 
+  // Embed visits are never shown in our views.
+  if (aTransitionType == nsINavHistoryService::TRANSITION_EMBED) {
+    return NS_OK;
+  }
+
   uint32_t added = 0;
 
   ENUMERATE_HISTORY_OBSERVERS(OnVisit(aURI, aVisitId, aTime, aSessionId,
                                       aReferringId, aTransitionType, aGUID,
                                       aHidden, &added));
 
   if (!mRootNode->mExpanded)
     return NS_OK;
--- a/toolkit/components/places/nsNavHistoryResult.h
+++ b/toolkit/components/places/nsNavHistoryResult.h
@@ -89,17 +89,17 @@ private:
                      bool aHidden, uint32_t* aAdded);
 
 // The external version is used by results.
 #define NS_DECL_BOOKMARK_HISTORY_OBSERVER_EXTERNAL(...)                 \
   NS_DECL_BOOKMARK_HISTORY_OBSERVER_BASE(__VA_ARGS__)                   \
   NS_IMETHOD OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,      \
                      int64_t aSessionId, int64_t aReferringId,          \
                      uint32_t aTransitionType, const nsACString& aGUID, \
-                     bool aHidden) __VA_ARGS__;
+                     bool aHidden, uint32_t aVisitCount, uint32_t aTyped) __VA_ARGS__;
 
 // nsNavHistoryResult
 //
 //    nsNavHistory creates this object and fills in mChildren (by getting
 //    it through GetTopLevel()). Then FilledAllResults() is called to finish
 //    object initialization.
 
 #define NS_NAVHISTORYRESULT_IID \
--- a/toolkit/components/places/nsPlacesTriggers.h
+++ b/toolkit/components/places/nsPlacesTriggers.h
@@ -22,16 +22,17 @@
 /**
  * This triggers update visit_count and last_visit_date based on historyvisits
  * table changes.
  */
 #define CREATE_HISTORYVISITS_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_historyvisits_afterinsert_v2_trigger " \
   "AFTER INSERT ON moz_historyvisits FOR EACH ROW " \
   "BEGIN " \
+    "SELECT store_last_inserted_id('moz_historyvisits', NEW.id); " \
     "UPDATE moz_places SET " \
       "visit_count = visit_count + (SELECT NEW.visit_type NOT IN (" EXCLUDED_VISIT_TYPES ")), "\
       "last_visit_date = MAX(IFNULL(last_visit_date, 0), NEW.visit_date) " \
     "WHERE id = NEW.place_id;" \
   "END" \
 )
 
 #define CREATE_HISTORYVISITS_AFTERDELETE_TRIGGER NS_LITERAL_CSTRING( \
@@ -89,30 +90,30 @@
   "END "
 
 /**
  * These triggers update the hostnames table whenever moz_places changes.
  */
 #define CREATE_PLACES_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
   "CREATE TEMP TRIGGER moz_places_afterinsert_trigger " \
   "AFTER INSERT ON moz_places FOR EACH ROW " \
-  "WHEN LENGTH(NEW.rev_host) > 1 " \
   "BEGIN " \
+    "SELECT store_last_inserted_id('moz_places', NEW.id); " \
     "INSERT OR REPLACE INTO moz_hosts (id, host, frecency, typed, prefix) " \
-    "VALUES (" \
-      "(SELECT id FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), " \
-      "fixup_url(get_unreversed_host(NEW.rev_host)), " \
-      "MAX(IFNULL((SELECT frecency FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), -1), NEW.frecency), " \
-      "MAX(IFNULL((SELECT typed FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), 0), NEW.typed), " \
-      "(" HOSTS_PREFIX_PRIORITY_FRAGMENT \
-       "FROM ( " \
-          "SELECT fixup_url(get_unreversed_host(NEW.rev_host)) AS host " \
-        ") AS match " \
-      ") " \
-    "); " \
+    "SELECT " \
+        "(SELECT id FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), " \
+        "fixup_url(get_unreversed_host(NEW.rev_host)), " \
+        "MAX(IFNULL((SELECT frecency FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), -1), NEW.frecency), " \
+        "MAX(IFNULL((SELECT typed FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), 0), NEW.typed), " \
+        "(" HOSTS_PREFIX_PRIORITY_FRAGMENT \
+         "FROM ( " \
+            "SELECT fixup_url(get_unreversed_host(NEW.rev_host)) AS host " \
+          ") AS match " \
+        ") " \
+    " WHERE LENGTH(NEW.rev_host) > 1; " \
   "END" \
 )
 
 // This is a hack to workaround the lack of FOR EACH STATEMENT in Sqlite, until
 // bug 871908 can be fixed properly.
 // We store the modified hosts in a temp table, and after every DELETE FROM
 // moz_places, we issue a DELETE FROM moz_updatehosts_temp.  The AFTER DELETE
 // trigger will then take care of updating the moz_hosts table.
--- a/toolkit/components/places/tests/PlacesTestUtils.jsm
+++ b/toolkit/components/places/tests/PlacesTestUtils.jsm
@@ -52,21 +52,26 @@ this.PlacesTestUtils = Object.freeze({
     }
 
     // Create mozIVisitInfo for each entry.
     let now = Date.now();
     for (let place of places) {
       if (typeof place.uri == "string") {
         place.uri = NetUtil.newURI(place.uri);
       } else if (place.uri instanceof URL) {
-        place.uri = NetUtil.newURI(place.href);
+        place.uri = NetUtil.newURI(place.uri.href);
       }
       if (typeof place.title != "string") {
         place.title = "test visit for " + place.uri.spec;
       }
+      if (typeof place.referrer == "string") {
+        place.referrer = NetUtil.newURI(place.referrer);
+      } else if (place.referrer instanceof URL) {
+        place.referrer = NetUtil.newURI(place.referrer.href);
+      }
       place.visits = [{
         transitionType: place.transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
                                                        : place.transition,
         visitDate: place.visitDate || (now++) * 1000,
         referrerURI: place.referrer
       }];
     }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/places/tests/history/test_updatePlaces_sameUri_titleChanged.js
@@ -0,0 +1,52 @@
+// Test that repeated additions of the same URI through updatePlaces, properly
+// update from_visit and notify titleChanged.
+
+add_task(function* test() {
+  let uri = "http://test.com/";
+
+  let promiseTitleChangedNotifications = new Promise(resolve => {
+    let historyObserver = {
+      _count: 0,
+      __proto__: NavHistoryObserver.prototype,
+      onTitleChanged(aURI, aTitle, aGUID) {
+        Assert.equal(aURI.spec, uri, "Should notify the proper url");
+        if (++this._count == 2) {
+          PlacesUtils.history.removeObserver(historyObserver);
+          resolve();
+        }
+      }
+    };
+    PlacesUtils.history.addObserver(historyObserver, false);
+  });
+
+  // This repeats the url on purpose, don't merge it into a single place entry.
+  yield PlacesTestUtils.addVisits([
+    { uri, title: "test" },
+    { uri, referrer: uri, title: "test2" },
+  ]);
+
+  let options = PlacesUtils.history.getNewQueryOptions();
+  let query = PlacesUtils.history.getNewQuery();
+  query.uri = NetUtil.newURI(uri);
+  options.resultType = options.RESULTS_AS_VISIT;
+  let root = PlacesUtils.history.executeQuery(query, options).root;
+  root.containerOpen = true;
+
+  Assert.equal(root.childCount, 2);
+
+  let child = root.getChild(0);
+  Assert.equal(child.visitType, TRANSITION_LINK, "Visit type should be TRANSITION_LINK");
+  Assert.equal(child.visitId, 1, "Visit ID should be 1");
+  Assert.equal(child.fromVisitId, -1, "Should have no referrer visit ID");
+  Assert.equal(child.title, "test2", "Should have the correct title");
+
+  child = root.getChild(1);
+  Assert.equal(child.visitType, TRANSITION_LINK, "Visit type should be TRANSITION_LINK");
+  Assert.equal(child.visitId, 2, "Visit ID should be 2");
+  Assert.equal(child.fromVisitId, 1, "First visit should be the referring visit");
+  Assert.equal(child.title, "test2", "Should have the correct title");
+
+  root.containerOpen = false;
+
+  yield promiseTitleChangedNotifications;
+});
--- a/toolkit/components/places/tests/history/xpcshell.ini
+++ b/toolkit/components/places/tests/history/xpcshell.ini
@@ -1,8 +1,9 @@
 [DEFAULT]
 head = head_history.js
 tail =
 
 [test_insert.js]
 [test_remove.js]
 [test_removeVisits.js]
 [test_removeVisitsByFilter.js]
+[test_updatePlaces_sameUri_titleChanged.js]
--- a/toolkit/components/places/tests/unit/test_history_observer.js
+++ b/toolkit/components/places/tests/unit/test_history_observer.js
@@ -20,25 +20,25 @@ NavHistoryObserver.prototype = {
 };
 
 /**
  * Registers a one-time history observer for and calls the callback
  * when the specified nsINavHistoryObserver method is called.
  * Returns a promise that is resolved when the callback returns.
  */
 function onNotify(callback) {
-  let deferred = Promise.defer();
-  let obs = new NavHistoryObserver();
-  obs[callback.name] = function () {
-    PlacesUtils.history.removeObserver(this);
-    callback.apply(this, arguments);
-    deferred.resolve();
-  };
-  PlacesUtils.history.addObserver(obs, false);
-  return deferred.promise;
+  return new Promise(resolve => {
+    let obs = new NavHistoryObserver();
+    obs[callback.name] = function () {
+      PlacesUtils.history.removeObserver(this);
+      callback.apply(this, arguments);
+      resolve();
+    };
+    PlacesUtils.history.addObserver(obs, false);
+  });
 }
 
 /**
  * Asynchronous task that adds a visit to the history database.
  */
 function* task_add_visit(uri, timestamp, transition) {
   uri = uri || NetUtil.newURI("http://firefox.com/");
   timestamp = timestamp || Date.now() * 1000;
@@ -49,108 +49,160 @@ function* task_add_visit(uri, timestamp,
   });
   return [uri, timestamp];
 }
 
 add_task(function* test_onVisit() {
   let promiseNotify = onNotify(function onVisit(aURI, aVisitID, aTime,
                                                 aSessionID, aReferringID,
                                                 aTransitionType, aGUID,
-                                                aHidden) {
-    do_check_true(aURI.equals(testuri));
-    do_check_true(aVisitID > 0);
-    do_check_eq(aTime, testtime);
-    do_check_eq(aSessionID, 0);
-    do_check_eq(aReferringID, 0);
-    do_check_eq(aTransitionType, TRANSITION_TYPED);
+                                                aHidden, aVisitCount, aTyped) {
+    Assert.ok(aURI.equals(testuri));
+    Assert.ok(aVisitID > 0);
+    Assert.equal(aTime, testtime);
+    Assert.equal(aSessionID, 0);
+    Assert.equal(aReferringID, 0);
+    Assert.equal(aTransitionType, TRANSITION_TYPED);
     do_check_guid_for_uri(aURI, aGUID);
-    do_check_false(aHidden);
+    Assert.ok(!aHidden);
+    Assert.equal(aVisitCount, 1);
+    Assert.equal(aTyped, 1);
   });
   let testuri = NetUtil.newURI("http://firefox.com/");
   let testtime = Date.now() * 1000;
   yield task_add_visit(testuri, testtime);
   yield promiseNotify;
 });
 
 add_task(function* test_onVisit() {
   let promiseNotify = onNotify(function onVisit(aURI, aVisitID, aTime,
                                                 aSessionID, aReferringID,
                                                 aTransitionType, aGUID,
-                                                aHidden) {
-    do_check_true(aURI.equals(testuri));
-    do_check_true(aVisitID > 0);
-    do_check_eq(aTime, testtime);
-    do_check_eq(aSessionID, 0);
-    do_check_eq(aReferringID, 0);
-    do_check_eq(aTransitionType, TRANSITION_FRAMED_LINK);
+                                                aHidden, aVisitCount, aTyped) {
+    Assert.ok(aURI.equals(testuri));
+    Assert.ok(aVisitID > 0);
+    Assert.equal(aTime, testtime);
+    Assert.equal(aSessionID, 0);
+    Assert.equal(aReferringID, 0);
+    Assert.equal(aTransitionType, TRANSITION_FRAMED_LINK);
     do_check_guid_for_uri(aURI, aGUID);
-    do_check_true(aHidden);
+    Assert.ok(aHidden);
+    Assert.equal(aVisitCount, 1);
+    Assert.equal(aTyped, 0);
   });
   let testuri = NetUtil.newURI("http://hidden.firefox.com/");
   let testtime = Date.now() * 1000;
   yield task_add_visit(testuri, testtime, TRANSITION_FRAMED_LINK);
   yield promiseNotify;
 });
 
+add_task(function* test_multiple_onVisit() {
+  let testuri = NetUtil.newURI("http://self.firefox.com/");
+  let promiseNotifications = new Promise(resolve => {
+    let observer = {
+      _c: 0,
+      __proto__: NavHistoryObserver.prototype,
+      onVisit(uri, id, time, undefined, referrerId, transition, guid,
+              hidden, visitCount, typed) {
+        Assert.ok(testuri.equals(uri));
+        Assert.ok(id > 0);
+        Assert.ok(time > 0);
+        Assert.ok(!hidden);
+        do_check_guid_for_uri(uri, guid);
+        switch (++this._c) {
+          case 1:
+            Assert.equal(referrerId, 0);
+            Assert.equal(transition, TRANSITION_LINK);
+            Assert.equal(visitCount, 1);
+            Assert.equal(typed, 0);
+            break;
+          case 2:
+            Assert.ok(referrerId > 0);
+            Assert.equal(transition, TRANSITION_LINK);
+            Assert.equal(visitCount, 2);
+            Assert.equal(typed, 0);
+            break;
+          case 3:
+            Assert.equal(referrerId, 0);
+            Assert.equal(transition, TRANSITION_TYPED);
+            Assert.equal(visitCount, 3);
+            Assert.equal(typed, 1);
+
+            PlacesUtils.history.removeObserver(observer, false);
+            resolve();
+            break;
+        }
+      }
+    };
+    PlacesUtils.history.addObserver(observer, false);
+  });
+  yield PlacesTestUtils.addVisits([
+    { uri: testuri, transition: TRANSITION_LINK },
+    { uri: testuri, referrer: testuri, transition: TRANSITION_LINK },
+    { uri: testuri, transition: TRANSITION_TYPED },
+  ]);
+  yield promiseNotifications;
+});
+
 add_task(function* test_onDeleteURI() {
   let promiseNotify = onNotify(function onDeleteURI(aURI, aGUID, aReason) {
-    do_check_true(aURI.equals(testuri));
+    Assert.ok(aURI.equals(testuri));
     // Can't use do_check_guid_for_uri() here because the visit is already gone.
-    do_check_eq(aGUID, testguid);
-    do_check_eq(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
+    Assert.equal(aGUID, testguid);
+    Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
   });
   let [testuri] = yield task_add_visit();
   let testguid = do_get_guid_for_uri(testuri);
   PlacesUtils.bhistory.removePage(testuri);
   yield promiseNotify;
 });
 
 add_task(function* test_onDeleteVisits() {
   let promiseNotify = onNotify(function onDeleteVisits(aURI, aVisitTime, aGUID,
                                                        aReason) {
-    do_check_true(aURI.equals(testuri));
+    Assert.ok(aURI.equals(testuri));
     // Can't use do_check_guid_for_uri() here because the visit is already gone.
-    do_check_eq(aGUID, testguid);
-    do_check_eq(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
-    do_check_eq(aVisitTime, 0); // All visits have been removed.
+    Assert.equal(aGUID, testguid);
+    Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
+    Assert.equal(aVisitTime, 0); // All visits have been removed.
   });
   let msecs24hrsAgo = Date.now() - (86400 * 1000);
   let [testuri] = yield task_add_visit(undefined, msecs24hrsAgo * 1000);
   // Add a bookmark so the page is not removed.
   PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
                                        testuri,
                                        PlacesUtils.bookmarks.DEFAULT_INDEX,
                                        "test");
   let testguid = do_get_guid_for_uri(testuri);
   PlacesUtils.bhistory.removePage(testuri);
   yield promiseNotify;
 });
 
 add_task(function* test_onTitleChanged() {
   let promiseNotify = onNotify(function onTitleChanged(aURI, aTitle, aGUID) {
-    do_check_true(aURI.equals(testuri));
-    do_check_eq(aTitle, title);
+    Assert.ok(aURI.equals(testuri));
+    Assert.equal(aTitle, title);
     do_check_guid_for_uri(aURI, aGUID);
   });
 
   let [testuri] = yield task_add_visit();
   let title = "test-title";
   yield PlacesTestUtils.addVisits({
     uri: testuri,
     title: title
   });
   yield promiseNotify;
 });
 
 add_task(function* test_onPageChanged() {
   let promiseNotify = onNotify(function onPageChanged(aURI, aChangedAttribute,
                                                       aNewValue, aGUID) {
-    do_check_eq(aChangedAttribute, Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON);
-    do_check_true(aURI.equals(testuri));
-    do_check_eq(aNewValue, SMALLPNG_DATA_URI.spec);
+    Assert.equal(aChangedAttribute, Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON);
+    Assert.ok(aURI.equals(testuri));
+    Assert.equal(aNewValue, SMALLPNG_DATA_URI.spec);
     do_check_guid_for_uri(aURI, aGUID);
   });
 
   let [testuri] = yield task_add_visit();
 
   // The new favicon for the page must have data associated with it in order to
   // receive the onPageChanged notification.  To keep this test self-contained,
   // we use an URI representing the smallest possible PNG file.
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/chromium/README.txt
@@ -0,0 +1,23 @@
+# Overview
+
+'safebrowsing.proto' is modified from [1] with the following line added:
+
+"package mozilla.safebrowsing;"
+
+to avoid naming pollution. We use this source file along with protobuf compiler (protoc) to generate safebrowsing.pb.h/cc for safebrowsing v4 update and hash completion. The current generated files are compiled by protoc 2.6.1 since the protobuf library in gecko is not upgraded to 3.0 yet.
+
+# Update
+
+If you want to update to the latest upstream version,
+
+1. Checkout the latest one in [2]
+2. Use protoc to generate safebrowsing.pb.h and safebrowsing.pb.cc. For example,
+
+$ protoc -I=. --cpp_out="../protobuf/" safebrowsing.proto
+
+(Note that we should use protoc v2.6.1 [3] to compile. You can find the compiled protoc in [4] if you don't have one.)
+
+[1] https://chromium.googlesource.com/chromium/src.git/+/9c4485f1ce7cac7ae82f7a4ae36ccc663afe806c/components/safe_browsing_db/safebrowsing.proto
+[2] https://chromium.googlesource.com/chromium/src.git/+/master/components/safe_browsing_db/safebrowsing.proto
+[3] https://github.com/google/protobuf/releases/tag/v2.6.1
+[4] https://repo1.maven.org/maven2/com/google/protobuf/protoc
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/chromium/safebrowsing.proto
@@ -0,0 +1,473 @@
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// This file includes Safe Browsing V4 API blacklist request and response
+// protocol buffers. They should be kept in sync with the server implementation.
+
+syntax = "proto2";
+
+option optimize_for = LITE_RUNTIME;
+
+package mozilla.safebrowsing;
+
+message ThreatInfo {
+  // The threat types to be checked.
+  repeated ThreatType threat_types = 1;
+
+  // The platform types to be checked.
+  repeated PlatformType platform_types = 2;
+
+  // The entry types to be checked.
+  repeated ThreatEntryType threat_entry_types = 4;
+
+  // The threat entries to be checked.
+  repeated ThreatEntry threat_entries = 3;
+}
+
+// A match when checking a threat entry in the Safe Browsing threat lists.
+message ThreatMatch {
+  // The threat type matching this threat.
+  optional ThreatType threat_type = 1;
+
+  // The platform type matching this threat.
+  optional PlatformType platform_type = 2;
+
+  // The threat entry type matching this threat.
+  optional ThreatEntryType threat_entry_type = 6;
+
+  // The threat matching this threat.
+  optional ThreatEntry threat = 3;
+
+  // Optional metadata associated with this threat.
+  optional ThreatEntryMetadata threat_entry_metadata = 4;
+
+  // The cache lifetime for the returned match. Clients must not cache this
+  // response for more than this duration to avoid false positives.
+  optional Duration cache_duration = 5;
+}
+
+// Request to check entries against lists.
+message FindThreatMatchesRequest {
+  // The client metadata.
+  optional ClientInfo client = 1;
+
+  // The lists and entries to be checked for matches.
+  optional ThreatInfo threat_info = 2;
+}
+
+// Response type for requests to find threat matches.
+message FindThreatMatchesResponse {
+  // The threat list matches.
+  repeated ThreatMatch matches = 1;
+}
+
+// Describes a Safe Browsing API update request. Clients can request updates for
+// multiple lists in a single request.
+message FetchThreatListUpdatesRequest {
+  // The client metadata.
+  optional ClientInfo client = 1;
+
+  // A single list update request.
+  message ListUpdateRequest {
+    // The type of threat posed by entries present in the list.
+    optional ThreatType threat_type = 1;
+
+    // The type of platform at risk by entries present in the list.
+    optional PlatformType platform_type = 2;
+
+    // The types of entries present in the list.
+    optional ThreatEntryType threat_entry_type = 5;
+
+    // The current state of the client for the requested list (the encrypted
+    // ClientState that was sent to the client from the previous update
+    // request).
+    optional bytes state = 3;
+
+    // The constraints for this update.
+    message Constraints {
+      // The maximum size in number of entries. The update will not contain more
+      // entries than this value.  This should be a power of 2 between 2**10 and
+      // 2**20.  If zero, no update size limit is set.
+      optional int32 max_update_entries = 1;
+
+      // Sets the maxmimum number of entries that the client is willing to have
+      // in the local database. This should be a power of 2 between 2**10 and
+      // 2**20. If zero, no database size limit is set.
+      optional int32 max_database_entries = 2;
+
+      // Requests the list for a specific geographic location. If not set the
+      // server may pick that value based on the user's IP address. Expects ISO
+      // 3166-1 alpha-2 format.
+      optional string region = 3;
+
+      // The compression types supported by the client.
+      repeated CompressionType supported_compressions = 4;
+    }
+
+    // The constraints associated with this request.
+    optional Constraints constraints = 4;
+  }
+
+  // The requested threat list updates.
+  repeated ListUpdateRequest list_update_requests = 3;
+}
+
+// Response type for threat list update requests.
+message FetchThreatListUpdatesResponse {
+  // An update to an individual list.
+  message ListUpdateResponse {
+    // The threat type for which data is returned.
+    optional ThreatType threat_type = 1;
+
+    // The format of the threats.
+    optional ThreatEntryType threat_entry_type = 2;
+
+    // The platform type for which data is returned.
+    optional PlatformType platform_type = 3;
+
+    // The type of response sent to the client.
+    enum ResponseType {
+      // Unknown.
+      RESPONSE_TYPE_UNSPECIFIED = 0;
+
+      // Partial updates are applied to the client's existing local database.
+      PARTIAL_UPDATE = 1;
+
+      // Full updates replace the client's entire local database. This means
+      // that either the client was seriously out-of-date or the client is
+      // believed to be corrupt.
+      FULL_UPDATE = 2;
+    }
+
+    // The type of response. This may indicate that an action is required by the
+    // client when the response is received.
+    optional ResponseType response_type = 4;
+
+    // A set of entries to add to a local threat type's list. Repeated to allow
+    // for a combination of compressed and raw data to be sent in a single
+    // response.
+    repeated ThreatEntrySet additions = 5;
+
+    // A set of entries to remove from a local threat type's list. Repeated for
+    // the same reason as above.
+    repeated ThreatEntrySet removals = 6;
+
+    // The new client state, in encrypted format. Opaque to clients.
+    optional bytes new_client_state = 7;
+
+    // The expected SHA256 hash of the client state; that is, of the sorted list
+    // of all hashes present in the database after applying the provided update.
+    // If the client state doesn't match the expected state, the client must
+    // disregard this update and retry later.
+    optional Checksum checksum = 8;
+  }
+
+  // The list updates requested by the clients.
+  repeated ListUpdateResponse list_update_responses = 1;
+
+  // The minimum duration the client must wait before issuing any update
+  // request. If this field is not set clients may update as soon as they want.
+  optional Duration minimum_wait_duration = 2;
+}
+
+// Request to return full hashes matched by the provided hash prefixes.
+message FindFullHashesRequest {
+  // The client metadata.
+  optional ClientInfo client = 1;
+
+  // The current client states for each of the client's local threat lists.
+  repeated bytes client_states = 2;
+
+  // The lists and hashes to be checked.
+  optional ThreatInfo threat_info = 3;
+}
+
+// Response type for requests to find full hashes.
+message FindFullHashesResponse {
+  // The full hashes that matched the requested prefixes.
+  repeated ThreatMatch matches = 1;
+
+  // The minimum duration the client must wait before issuing any find hashes
+  // request. If this field is not set, clients can issue a request as soon as
+  // they want.
+  optional Duration minimum_wait_duration = 2;
+
+  // For requested entities that did not match the threat list, how long to
+  // cache the response.
+  optional Duration negative_cache_duration = 3;
+}
+
+// A hit comprised of multiple resources; one is the threat list entry that was
+// encountered by the client, while others give context as to how the client
+// arrived at the unsafe entry.
+message ThreatHit {
+  // The threat type reported.
+  optional ThreatType threat_type = 1;
+
+  // The platform type reported.
+  optional PlatformType platform_type = 2;
+
+  // The threat entry responsible for the hit. Full hash should be reported for
+  // hash-based hits.
+  optional ThreatEntry entry = 3;
+
+  // Types of resources reported by the client as part of a single hit.
+  enum ThreatSourceType {
+    // Unknown.
+    THREAT_SOURCE_TYPE_UNSPECIFIED = 0;
+    // The URL that matched the threat list (for which GetFullHash returned a
+    // valid hash).
+    MATCHING_URL = 1;
+    // The final top-level URL of the tab that the client was browsing when the
+    // match occurred.
+    TAB_URL = 2;
+    // A redirect URL that was fetched before hitting the final TAB_URL.
+    TAB_REDIRECT = 3;
+  }
+
+  // A single resource related to a threat hit.
+  message ThreatSource {
+    // The URL of the resource.
+    optional string url = 1;
+
+    // The type of source reported.
+    optional ThreatSourceType type = 2;
+
+    // The remote IP of the resource in ASCII format. Either IPv4 or IPv6.
+    optional string remote_ip = 3;
+
+    // Referrer of the resource. Only set if the referrer is available.
+    optional string referrer = 4;
+  }
+
+  // The resources related to the threat hit.
+  repeated ThreatSource resources = 4;
+}
+
+// Types of threats.
+enum ThreatType {
+  // Unknown.
+  THREAT_TYPE_UNSPECIFIED = 0;
+
+  // Malware threat type.
+  MALWARE_THREAT = 1;
+
+  // Social engineering threat type.
+  SOCIAL_ENGINEERING_PUBLIC = 2;
+
+  // Unwanted software threat type.
+  UNWANTED_SOFTWARE = 3;
+
+  // Potentially harmful application threat type.
+  POTENTIALLY_HARMFUL_APPLICATION = 4;
+
+  // Social engineering threat type for internal use.
+  SOCIAL_ENGINEERING = 5;
+
+  // API abuse threat type.
+  API_ABUSE = 6;
+}
+
+// Types of platforms.
+enum PlatformType {
+  // Unknown platform.
+  PLATFORM_TYPE_UNSPECIFIED = 0;
+
+  // Threat posed to Windows.
+  WINDOWS_PLATFORM = 1;
+
+  // Threat posed to Linux.
+  LINUX_PLATFORM = 2;
+
+  // Threat posed to Android.
+  // This cannot be ANDROID because that symbol is defined for android builds
+  // here: build/config/android/BUILD.gn line21.
+  ANDROID_PLATFORM = 3;
+
+  // Threat posed to OSX.
+  OSX_PLATFORM = 4;
+
+  // Threat posed to iOS.
+  IOS_PLATFORM = 5;
+
+  // Threat posed to at least one of the defined platforms.
+  ANY_PLATFORM = 6;
+
+  // Threat posed to all defined platforms.
+  ALL_PLATFORMS = 7;
+
+  // Threat posed to Chrome.
+  CHROME_PLATFORM = 8;
+}
+
+// The client metadata associated with Safe Browsing API requests.
+message ClientInfo {
+  // A client ID that (hopefully) uniquely identifies the client implementation
+  // of the Safe Browsing API.
+  optional string client_id = 1;
+
+  // The version of the client implementation.
+  optional string client_version = 2;
+}
+
+// The expected state of a client's local database.
+message Checksum {
+  // The SHA256 hash of the client state; that is, of the sorted list of all
+  // hashes present in the database.
+  optional bytes sha256 = 1;
+}
+
+// The ways in which threat entry sets can be compressed.
+enum CompressionType {
+  // Unknown.
+  COMPRESSION_TYPE_UNSPECIFIED = 0;
+
+  // Raw, uncompressed data.
+  RAW = 1;
+
+  // Rice-Golomb encoded data.
+  RICE = 2;
+}
+
+// An individual threat; for example, a malicious URL or its hash
+// representation. Only one of these fields should be set.
+message ThreatEntry {
+  // A variable-length SHA256 hash with size between 4 and 32 bytes inclusive.
+  optional bytes hash = 1;
+
+  // A URL.
+  optional string url = 2;
+}
+
+// Types of entries that pose threats. Threat lists are collections of entries
+// of a single type.
+enum ThreatEntryType {
+  // Unspecified.
+  THREAT_ENTRY_TYPE_UNSPECIFIED = 0;
+
+  // A host-suffix/path-prefix URL expression; for example, "foo.bar.com/baz/".
+  URL = 1;
+
+  // An executable program.
+  EXECUTABLE = 2;
+
+  // An IP range.
+  IP_RANGE = 3;
+}
+
+// A set of threats that should be added or removed from a client's local
+// database.
+message ThreatEntrySet {
+  // The compression type for the entries in this set.
+  optional CompressionType compression_type = 1;
+
+  // At most one of the following fields should be set.
+
+  // The raw SHA256-formatted entries.
+  optional RawHashes raw_hashes = 2;
+
+  // The raw removal indices for a local list.
+  optional RawIndices raw_indices = 3;
+
+  // The encoded 4-byte prefixes of SHA256-formatted entries, using a
+  // Golomb-Rice encoding.
+  optional RiceDeltaEncoding rice_hashes = 4;
+
+  // The encoded local, lexicographically-sorted list indices, using a
+  // Golomb-Rice encoding. Used for sending compressed removal indicies.
+  optional RiceDeltaEncoding rice_indices = 5;
+}
+
+// A set of raw indicies to remove from a local list.
+message RawIndices {
+  // The indicies to remove from a lexicographically-sorted local list.
+  repeated int32 indices = 1;
+}
+
+// The uncompressed threat entries in hash format of a particular prefix length.
+// Hashes can be anywhere from 4 to 32 bytes in size. A large majority are 4
+// bytes, but some hashes are lengthened if they collide with the hash of a
+// popular URL.
+//
+// Used for sending ThreatEntrySet to clients that do not support compression,
+// or when sending non-4-byte hashes to clients that do support compression.
+message RawHashes {
+  // The number of bytes for each prefix encoded below.  This field can be
+  // anywhere from 4 (shortest prefix) to 32 (full SHA256 hash).
+  optional int32 prefix_size = 1;
+
+  // The hashes, all concatenated into one long string.  Each hash has a prefix
+  // size of |prefix_size| above. Hashes are sorted in lexicographic order.
+  optional bytes raw_hashes = 2;
+}
+
+// The Rice-Golomb encoded data. Used for sending compressed 4-byte hashes or
+// compressed removal indices.
+message RiceDeltaEncoding {
+  // The offset of the first entry in the encoded data, or, if only a single
+  // integer was encoded, that single integer's value.
+  optional int64 first_value = 1;
+
+  // The Golomb-Rice parameter which is a number between 2 and 28. This field
+  // is missing (that is, zero) if num_entries is zero.
+  optional int32 rice_parameter = 2;
+
+  // The number of entries that are delta encoded in the encoded data. If only a
+  // single integer was encoded, this will be zero and the single value will be
+  // stored in first_value.
+  optional int32 num_entries = 3;
+
+  // The encoded deltas that are encoded using the Golomb-Rice coder.
+  optional bytes encoded_data = 4;
+}
+
+// The metadata associated with a specific threat entry. The client is expected
+// to know the metadata key/value pairs associated with each threat type.
+message ThreatEntryMetadata {
+  // A single metadata entry.
+  message MetadataEntry {
+    // The metadata entry key.
+    optional bytes key = 1;
+
+    // The metadata entry value.
+    optional bytes value = 2;
+  }
+
+  // The metadata entries.
+  repeated MetadataEntry entries = 1;
+}
+
+// Describes an individual threat list. A list is defined by three parameters:
+// the type of threat posed, the type of platform targeted by the threat, and
+// the type of entries in the list.
+message ThreatListDescriptor {
+  // The threat type posed by the list's entries.
+  optional ThreatType threat_type = 1;
+
+  // The platform type targeted by the list's entries.
+  optional PlatformType platform_type = 2;
+
+  // The entry types contained in the list.
+  optional ThreatEntryType threat_entry_type = 3;
+}
+
+// A collection of lists available for download.
+message ListThreatListsResponse {
+  // The lists available for download.
+  repeated ThreatListDescriptor threat_lists = 1;
+}
+
+message Duration {
+  // Signed seconds of the span of time. Must be from -315,576,000,000
+  // to +315,576,000,000 inclusive.
+  optional int64 seconds = 1;
+
+  // Signed fractions of a second at nanosecond resolution of the span
+  // of time. Durations less than one second are represented with a 0
+  // `seconds` field and a positive or negative `nanos` field. For durations
+  // of one second or more, a non-zero value for the `nanos` field must be
+  // of the same sign as the `seconds` field. Must be from -999,999,999
+  // to +999,999,999 inclusive.
+  optional int32 nanos = 2;
+}
--- a/toolkit/components/url-classifier/moz.build
+++ b/toolkit/components/url-classifier/moz.build
@@ -12,24 +12,28 @@ XPIDL_SOURCES += [
     'nsIUrlClassifierPrefixSet.idl',
     'nsIUrlClassifierStreamUpdater.idl',
     'nsIUrlClassifierUtils.idl',
     'nsIUrlListManager.idl',
 ]
 
 XPIDL_MODULE = 'url-classifier'
 
+# Disable RTTI in google protocol buffer
+DEFINES['GOOGLE_PROTOBUF_NO_RTTI'] = True
+
 UNIFIED_SOURCES += [
     'ChunkSet.cpp',
     'Classifier.cpp',
     'LookupCache.cpp',
     'nsCheckSummedOutputStream.cpp',
     'nsUrlClassifierDBService.cpp',
     'nsUrlClassifierProxies.cpp',
     'nsUrlClassifierUtils.cpp',
+    'protobuf/safebrowsing.pb.cc',
     'ProtocolParser.cpp',
 ]
 
 # define conflicting LOG() macros
 SOURCES += [
     'nsUrlClassifierPrefixSet.cpp',
     'nsUrlClassifierStreamUpdater.cpp',
 ]
@@ -53,16 +57,17 @@ EXTRA_PP_COMPONENTS += [
 EXTRA_JS_MODULES += [
     'SafeBrowsing.jsm',
 ]
 
 EXPORTS += [
     'Entries.h',
     'LookupCache.h',
     'nsUrlClassifierPrefixSet.h',
+    'protobuf/safebrowsing.pb.h',
 ]
 
 FINAL_LIBRARY = 'xul'
 
 LOCAL_INCLUDES += [
     '../build',
     '/ipc/chromium/src',
 ]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/url-classifier/protobuf/safebrowsing.pb.cc
@@ -0,0 +1,7166 @@
+// Generated by the protocol buffer compiler.  DO NOT EDIT!
+// source: safebrowsing.proto
+
+#define INTERNAL_SUPPRESS_PROTOBUF_FIELD_DEPRECATION
+#include "safebrowsing.pb.h"
+
+#include <algorithm>
+
+#include <google/protobuf/stubs/common.h>
+#include <google/protobuf/stubs/once.h>
+#include <google/protobuf/io/coded_stream.h>
+#include <google/protobuf/wire_format_lite_inl.h>
+#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
+// @@protoc_insertion_point(includes)
+
+namespace mozilla {
+namespace safebrowsing {
+
+void protobuf_ShutdownFile_safebrowsing_2eproto() {
+  delete ThreatInfo::default_instance_;
+  delete ThreatMatch::default_instance_;
+  delete FindThreatMatchesRequest::default_instance_;
+  delete FindThreatMatchesResponse::default_instance_;
+  delete FetchThreatListUpdatesRequest::default_instance_;
+  delete FetchThreatListUpdatesRequest_ListUpdateRequest::default_instance_;
+  delete FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance_;
+  delete FetchThreatListUpdatesResponse::default_instance_;
+  delete FetchThreatListUpdatesResponse_ListUpdateResponse::default_instance_;
+  delete FindFullHashesRequest::default_instance_;
+  delete FindFullHashesResponse::default_instance_;
+  delete ThreatHit::default_instance_;
+  delete ThreatHit_ThreatSource::default_instance_;
+  delete ClientInfo::default_instance_;
+  delete Checksum::default_instance_;
+  delete ThreatEntry::default_instance_;
+  delete ThreatEntrySet::default_instance_;
+  delete RawIndices::default_instance_;
+  delete RawHashes::default_instance_;
+  delete RiceDeltaEncoding::default_instance_;
+  delete ThreatEntryMetadata::default_instance_;
+  delete ThreatEntryMetadata_MetadataEntry::default_instance_;
+  delete ThreatListDescriptor::default_instance_;
+  delete ListThreatListsResponse::default_instance_;
+  delete Duration::default_instance_;
+}
+
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+void protobuf_AddDesc_safebrowsing_2eproto_impl() {
+  GOOGLE_PROTOBUF_VERIFY_VERSION;
+
+#else
+void protobuf_AddDesc_safebrowsing_2eproto() {
+  static bool already_here = false;
+  if (already_here) return;
+  already_here = true;
+  GOOGLE_PROTOBUF_VERIFY_VERSION;
+
+#endif
+  ThreatInfo::default_instance_ = new ThreatInfo();
+  ThreatMatch::default_instance_ = new ThreatMatch();
+  FindThreatMatchesRequest::default_instance_ = new FindThreatMatchesRequest();
+  FindThreatMatchesResponse::default_instance_ = new FindThreatMatchesResponse();
+  FetchThreatListUpdatesRequest::default_instance_ = new FetchThreatListUpdatesRequest();
+  FetchThreatListUpdatesRequest_ListUpdateRequest::default_instance_ = new FetchThreatListUpdatesRequest_ListUpdateRequest();
+  FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance_ = new FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints();
+  FetchThreatListUpdatesResponse::default_instance_ = new FetchThreatListUpdatesResponse();
+  FetchThreatListUpdatesResponse_ListUpdateResponse::default_instance_ = new FetchThreatListUpdatesResponse_ListUpdateResponse();
+  FindFullHashesRequest::default_instance_ = new FindFullHashesRequest();
+  FindFullHashesResponse::default_instance_ = new FindFullHashesResponse();
+  ThreatHit::default_instance_ = new ThreatHit();
+  ThreatHit_ThreatSource::default_instance_ = new ThreatHit_ThreatSource();
+  ClientInfo::default_instance_ = new ClientInfo();
+  Checksum::default_instance_ = new Checksum();
+  ThreatEntry::default_instance_ = new ThreatEntry();
+  ThreatEntrySet::default_instance_ = new ThreatEntrySet();
+  RawIndices::default_instance_ = new RawIndices();
+  RawHashes::default_instance_ = new RawHashes();
+  RiceDeltaEncoding::default_instance_ = new RiceDeltaEncoding();
+  ThreatEntryMetadata::default_instance_ = new ThreatEntryMetadata();
+  ThreatEntryMetadata_MetadataEntry::default_instance_ = new ThreatEntryMetadata_MetadataEntry();
+  ThreatListDescriptor::default_instance_ = new ThreatListDescriptor();
+  ListThreatListsResponse::default_instance_ = new ListThreatListsResponse();
+  Duration::default_instance_ = new Duration();
+  ThreatInfo::default_instance_->InitAsDefaultInstance();
+  ThreatMatch::default_instance_->InitAsDefaultInstance();
+  FindThreatMatchesRequest::default_instance_->InitAsDefaultInstance();
+  FindThreatMatchesResponse::default_instance_->InitAsDefaultInstance();
+  FetchThreatListUpdatesRequest::default_instance_->InitAsDefaultInstance();
+  FetchThreatListUpdatesRequest_ListUpdateRequest::default_instance_->InitAsDefaultInstance();
+  FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance_->InitAsDefaultInstance();
+  FetchThreatListUpdatesResponse::default_instance_->InitAsDefaultInstance();
+  FetchThreatListUpdatesResponse_ListUpdateResponse::default_instance_->InitAsDefaultInstance();
+  FindFullHashesRequest::default_instance_->InitAsDefaultInstance();
+  FindFullHashesResponse::default_instance_->InitAsDefaultInstance();
+  ThreatHit::default_instance_->InitAsDefaultInstance();
+  ThreatHit_ThreatSource::default_instance_->InitAsDefaultInstance();
+  ClientInfo::default_instance_->InitAsDefaultInstance();
+  Checksum::default_instance_->InitAsDefaultInstance();
+  ThreatEntry::default_instance_->InitAsDefaultInstance();
+  ThreatEntrySet::default_instance_->InitAsDefaultInstance();
+  RawIndices::default_instance_->InitAsDefaultInstance();
+  RawHashes::default_instance_->InitAsDefaultInstance();
+  RiceDeltaEncoding::default_instance_->InitAsDefaultInstance();
+  ThreatEntryMetadata::default_instance_->InitAsDefaultInstance();
+  ThreatEntryMetadata_MetadataEntry::default_instance_->InitAsDefaultInstance();
+  ThreatListDescriptor::default_instance_->InitAsDefaultInstance();
+  ListThreatListsResponse::default_instance_->InitAsDefaultInstance();
+  Duration::default_instance_->InitAsDefaultInstance();
+  ::google::protobuf::internal::OnShutdown(&protobuf_ShutdownFile_safebrowsing_2eproto);
+}
+
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+GOOGLE_PROTOBUF_DECLARE_ONCE(protobuf_AddDesc_safebrowsing_2eproto_once_);
+void protobuf_AddDesc_safebrowsing_2eproto() {
+  ::google::protobuf::GoogleOnceInit(&protobuf_AddDesc_safebrowsing_2eproto_once_,
+                 &protobuf_AddDesc_safebrowsing_2eproto_impl);
+}
+#else
+// Force AddDescriptors() to be called at static initialization time.
+struct StaticDescriptorInitializer_safebrowsing_2eproto {
+  StaticDescriptorInitializer_safebrowsing_2eproto() {
+    protobuf_AddDesc_safebrowsing_2eproto();
+  }
+} static_descriptor_initializer_safebrowsing_2eproto_;
+#endif
+bool ThreatType_IsValid(int value) {
+  switch(value) {
+    case 0:
+    case 1:
+    case 2:
+    case 3:
+    case 4:
+    case 5:
+    case 6:
+      return true;
+    default:
+      return false;
+  }
+}
+
+bool PlatformType_IsValid(int value) {
+  switch(value) {
+    case 0:
+    case 1:
+    case 2:
+    case 3:
+    case 4:
+    case 5:
+    case 6:
+    case 7:
+    case 8:
+      return true;
+    default:
+      return false;
+  }
+}
+
+bool CompressionType_IsValid(int value) {
+  switch(value) {
+    case 0:
+    case 1:
+    case 2:
+      return true;
+    default:
+      return false;
+  }
+}
+
+bool ThreatEntryType_IsValid(int value) {
+  switch(value) {
+    case 0:
+    case 1:
+    case 2:
+    case 3:
+      return true;
+    default:
+      return false;
+  }
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ThreatInfo::kThreatTypesFieldNumber;
+const int ThreatInfo::kPlatformTypesFieldNumber;
+const int ThreatInfo::kThreatEntryTypesFieldNumber;
+const int ThreatInfo::kThreatEntriesFieldNumber;
+#endif  // !_MSC_VER
+
+ThreatInfo::ThreatInfo()
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatInfo)
+}
+
+void ThreatInfo::InitAsDefaultInstance() {
+}
+
+ThreatInfo::ThreatInfo(const ThreatInfo& from)
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  MergeFrom(from);
+  // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatInfo)
+}
+
+void ThreatInfo::SharedCtor() {
+  _cached_size_ = 0;
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatInfo::~ThreatInfo() {
+  // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatInfo)
+  SharedDtor();
+}
+
+void ThreatInfo::SharedDtor() {
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  if (this != &default_instance()) {
+  #else
+  if (this != default_instance_) {
+  #endif
+  }
+}
+
+void ThreatInfo::SetCachedSize(int size) const {
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatInfo& ThreatInfo::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  protobuf_AddDesc_safebrowsing_2eproto();
+#else
+  if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+  return *default_instance_;
+}
+
+ThreatInfo* ThreatInfo::default_instance_ = NULL;
+
+ThreatInfo* ThreatInfo::New() const {
+  return new ThreatInfo;
+}
+
+void ThreatInfo::Clear() {
+  threat_types_.Clear();
+  platform_types_.Clear();
+  threat_entry_types_.Clear();
+  threat_entries_.Clear();
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+  mutable_unknown_fields()->clear();
+}
+
+bool ThreatInfo::MergePartialFromCodedStream(
+    ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+  ::google::protobuf::uint32 tag;
+  ::google::protobuf::io::StringOutputStream unknown_fields_string(
+      mutable_unknown_fields());
+  ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+      &unknown_fields_string);
+  // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatInfo)
+  for (;;) {
+    ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+    tag = p.first;
+    if (!p.second) goto handle_unusual;
+    switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+      // repeated .mozilla.safebrowsing.ThreatType threat_types = 1;
+      case 1: {
+        if (tag == 8) {
+         parse_threat_types:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::ThreatType_IsValid(value)) {
+            add_threat_types(static_cast< ::mozilla::safebrowsing::ThreatType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else if (tag == 10) {
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPackedEnumNoInline(
+                 input,
+                 &::mozilla::safebrowsing::ThreatType_IsValid,
+                 this->mutable_threat_types())));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(8)) goto parse_threat_types;
+        if (input->ExpectTag(16)) goto parse_platform_types;
+        break;
+      }
+
+      // repeated .mozilla.safebrowsing.PlatformType platform_types = 2;
+      case 2: {
+        if (tag == 16) {
+         parse_platform_types:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::PlatformType_IsValid(value)) {
+            add_platform_types(static_cast< ::mozilla::safebrowsing::PlatformType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else if (tag == 18) {
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPackedEnumNoInline(
+                 input,
+                 &::mozilla::safebrowsing::PlatformType_IsValid,
+                 this->mutable_platform_types())));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(16)) goto parse_platform_types;
+        if (input->ExpectTag(26)) goto parse_threat_entries;
+        break;
+      }
+
+      // repeated .mozilla.safebrowsing.ThreatEntry threat_entries = 3;
+      case 3: {
+        if (tag == 26) {
+         parse_threat_entries:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+                input, add_threat_entries()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(26)) goto parse_threat_entries;
+        if (input->ExpectTag(32)) goto parse_threat_entry_types;
+        break;
+      }
+
+      // repeated .mozilla.safebrowsing.ThreatEntryType threat_entry_types = 4;
+      case 4: {
+        if (tag == 32) {
+         parse_threat_entry_types:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::ThreatEntryType_IsValid(value)) {
+            add_threat_entry_types(static_cast< ::mozilla::safebrowsing::ThreatEntryType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else if (tag == 34) {
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPackedEnumNoInline(
+                 input,
+                 &::mozilla::safebrowsing::ThreatEntryType_IsValid,
+                 this->mutable_threat_entry_types())));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(32)) goto parse_threat_entry_types;
+        if (input->ExpectAtEnd()) goto success;
+        break;
+      }
+
+      default: {
+      handle_unusual:
+        if (tag == 0 ||
+            ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+            ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+          goto success;
+        }
+        DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+            input, tag, &unknown_fields_stream));
+        break;
+      }
+    }
+  }
+success:
+  // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatInfo)
+  return true;
+failure:
+  // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatInfo)
+  return false;
+#undef DO_
+}
+
+void ThreatInfo::SerializeWithCachedSizes(
+    ::google::protobuf::io::CodedOutputStream* output) const {
+  // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatInfo)
+  // repeated .mozilla.safebrowsing.ThreatType threat_types = 1;
+  for (int i = 0; i < this->threat_types_size(); i++) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      1, this->threat_types(i), output);
+  }
+
+  // repeated .mozilla.safebrowsing.PlatformType platform_types = 2;
+  for (int i = 0; i < this->platform_types_size(); i++) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      2, this->platform_types(i), output);
+  }
+
+  // repeated .mozilla.safebrowsing.ThreatEntry threat_entries = 3;
+  for (int i = 0; i < this->threat_entries_size(); i++) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      3, this->threat_entries(i), output);
+  }
+
+  // repeated .mozilla.safebrowsing.ThreatEntryType threat_entry_types = 4;
+  for (int i = 0; i < this->threat_entry_types_size(); i++) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      4, this->threat_entry_types(i), output);
+  }
+
+  output->WriteRaw(unknown_fields().data(),
+                   unknown_fields().size());
+  // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatInfo)
+}
+
+int ThreatInfo::ByteSize() const {
+  int total_size = 0;
+
+  // repeated .mozilla.safebrowsing.ThreatType threat_types = 1;
+  {
+    int data_size = 0;
+    for (int i = 0; i < this->threat_types_size(); i++) {
+      data_size += ::google::protobuf::internal::WireFormatLite::EnumSize(
+        this->threat_types(i));
+    }
+    total_size += 1 * this->threat_types_size() + data_size;
+  }
+
+  // repeated .mozilla.safebrowsing.PlatformType platform_types = 2;
+  {
+    int data_size = 0;
+    for (int i = 0; i < this->platform_types_size(); i++) {
+      data_size += ::google::protobuf::internal::WireFormatLite::EnumSize(
+        this->platform_types(i));
+    }
+    total_size += 1 * this->platform_types_size() + data_size;
+  }
+
+  // repeated .mozilla.safebrowsing.ThreatEntryType threat_entry_types = 4;
+  {
+    int data_size = 0;
+    for (int i = 0; i < this->threat_entry_types_size(); i++) {
+      data_size += ::google::protobuf::internal::WireFormatLite::EnumSize(
+        this->threat_entry_types(i));
+    }
+    total_size += 1 * this->threat_entry_types_size() + data_size;
+  }
+
+  // repeated .mozilla.safebrowsing.ThreatEntry threat_entries = 3;
+  total_size += 1 * this->threat_entries_size();
+  for (int i = 0; i < this->threat_entries_size(); i++) {
+    total_size +=
+      ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+        this->threat_entries(i));
+  }
+
+  total_size += unknown_fields().size();
+
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = total_size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+  return total_size;
+}
+
+void ThreatInfo::CheckTypeAndMergeFrom(
+    const ::google::protobuf::MessageLite& from) {
+  MergeFrom(*::google::protobuf::down_cast<const ThreatInfo*>(&from));
+}
+
+void ThreatInfo::MergeFrom(const ThreatInfo& from) {
+  GOOGLE_CHECK_NE(&from, this);
+  threat_types_.MergeFrom(from.threat_types_);
+  platform_types_.MergeFrom(from.platform_types_);
+  threat_entry_types_.MergeFrom(from.threat_entry_types_);
+  threat_entries_.MergeFrom(from.threat_entries_);
+  mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatInfo::CopyFrom(const ThreatInfo& from) {
+  if (&from == this) return;
+  Clear();
+  MergeFrom(from);
+}
+
+bool ThreatInfo::IsInitialized() const {
+
+  return true;
+}
+
+void ThreatInfo::Swap(ThreatInfo* other) {
+  if (other != this) {
+    threat_types_.Swap(&other->threat_types_);
+    platform_types_.Swap(&other->platform_types_);
+    threat_entry_types_.Swap(&other->threat_entry_types_);
+    threat_entries_.Swap(&other->threat_entries_);
+    std::swap(_has_bits_[0], other->_has_bits_[0]);
+    _unknown_fields_.swap(other->_unknown_fields_);
+    std::swap(_cached_size_, other->_cached_size_);
+  }
+}
+
+::std::string ThreatInfo::GetTypeName() const {
+  return "mozilla.safebrowsing.ThreatInfo";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int ThreatMatch::kThreatTypeFieldNumber;
+const int ThreatMatch::kPlatformTypeFieldNumber;
+const int ThreatMatch::kThreatEntryTypeFieldNumber;
+const int ThreatMatch::kThreatFieldNumber;
+const int ThreatMatch::kThreatEntryMetadataFieldNumber;
+const int ThreatMatch::kCacheDurationFieldNumber;
+#endif  // !_MSC_VER
+
+ThreatMatch::ThreatMatch()
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  // @@protoc_insertion_point(constructor:mozilla.safebrowsing.ThreatMatch)
+}
+
+void ThreatMatch::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  threat_ = const_cast< ::mozilla::safebrowsing::ThreatEntry*>(
+      ::mozilla::safebrowsing::ThreatEntry::internal_default_instance());
+#else
+  threat_ = const_cast< ::mozilla::safebrowsing::ThreatEntry*>(&::mozilla::safebrowsing::ThreatEntry::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  threat_entry_metadata_ = const_cast< ::mozilla::safebrowsing::ThreatEntryMetadata*>(
+      ::mozilla::safebrowsing::ThreatEntryMetadata::internal_default_instance());
+#else
+  threat_entry_metadata_ = const_cast< ::mozilla::safebrowsing::ThreatEntryMetadata*>(&::mozilla::safebrowsing::ThreatEntryMetadata::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  cache_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(
+      ::mozilla::safebrowsing::Duration::internal_default_instance());
+#else
+  cache_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(&::mozilla::safebrowsing::Duration::default_instance());
+#endif
+}
+
+ThreatMatch::ThreatMatch(const ThreatMatch& from)
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  MergeFrom(from);
+  // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.ThreatMatch)
+}
+
+void ThreatMatch::SharedCtor() {
+  _cached_size_ = 0;
+  threat_type_ = 0;
+  platform_type_ = 0;
+  threat_entry_type_ = 0;
+  threat_ = NULL;
+  threat_entry_metadata_ = NULL;
+  cache_duration_ = NULL;
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+ThreatMatch::~ThreatMatch() {
+  // @@protoc_insertion_point(destructor:mozilla.safebrowsing.ThreatMatch)
+  SharedDtor();
+}
+
+void ThreatMatch::SharedDtor() {
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  if (this != &default_instance()) {
+  #else
+  if (this != default_instance_) {
+  #endif
+    delete threat_;
+    delete threat_entry_metadata_;
+    delete cache_duration_;
+  }
+}
+
+void ThreatMatch::SetCachedSize(int size) const {
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const ThreatMatch& ThreatMatch::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  protobuf_AddDesc_safebrowsing_2eproto();
+#else
+  if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+  return *default_instance_;
+}
+
+ThreatMatch* ThreatMatch::default_instance_ = NULL;
+
+ThreatMatch* ThreatMatch::New() const {
+  return new ThreatMatch;
+}
+
+void ThreatMatch::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>(      \
+  &reinterpret_cast<ThreatMatch*>(16)->f) - \
+   reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do {                              \
+    size_t f = OFFSET_OF_FIELD_(first);                    \
+    size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last);  \
+    ::memset(&first, 0, n);                                \
+  } while (0)
+
+  if (_has_bits_[0 / 32] & 63) {
+    ZR_(threat_type_, platform_type_);
+    threat_entry_type_ = 0;
+    if (has_threat()) {
+      if (threat_ != NULL) threat_->::mozilla::safebrowsing::ThreatEntry::Clear();
+    }
+    if (has_threat_entry_metadata()) {
+      if (threat_entry_metadata_ != NULL) threat_entry_metadata_->::mozilla::safebrowsing::ThreatEntryMetadata::Clear();
+    }
+    if (has_cache_duration()) {
+      if (cache_duration_ != NULL) cache_duration_->::mozilla::safebrowsing::Duration::Clear();
+    }
+  }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+  mutable_unknown_fields()->clear();
+}
+
+bool ThreatMatch::MergePartialFromCodedStream(
+    ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+  ::google::protobuf::uint32 tag;
+  ::google::protobuf::io::StringOutputStream unknown_fields_string(
+      mutable_unknown_fields());
+  ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+      &unknown_fields_string);
+  // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.ThreatMatch)
+  for (;;) {
+    ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+    tag = p.first;
+    if (!p.second) goto handle_unusual;
+    switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+      // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+      case 1: {
+        if (tag == 8) {
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::ThreatType_IsValid(value)) {
+            set_threat_type(static_cast< ::mozilla::safebrowsing::ThreatType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(16)) goto parse_platform_type;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+      case 2: {
+        if (tag == 16) {
+         parse_platform_type:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::PlatformType_IsValid(value)) {
+            set_platform_type(static_cast< ::mozilla::safebrowsing::PlatformType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(26)) goto parse_threat;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.ThreatEntry threat = 3;
+      case 3: {
+        if (tag == 26) {
+         parse_threat:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+               input, mutable_threat()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(34)) goto parse_threat_entry_metadata;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.ThreatEntryMetadata threat_entry_metadata = 4;
+      case 4: {
+        if (tag == 34) {
+         parse_threat_entry_metadata:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+               input, mutable_threat_entry_metadata()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(42)) goto parse_cache_duration;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.Duration cache_duration = 5;
+      case 5: {
+        if (tag == 42) {
+         parse_cache_duration:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+               input, mutable_cache_duration()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(48)) goto parse_threat_entry_type;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 6;
+      case 6: {
+        if (tag == 48) {
+         parse_threat_entry_type:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::ThreatEntryType_IsValid(value)) {
+            set_threat_entry_type(static_cast< ::mozilla::safebrowsing::ThreatEntryType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectAtEnd()) goto success;
+        break;
+      }
+
+      default: {
+      handle_unusual:
+        if (tag == 0 ||
+            ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+            ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+          goto success;
+        }
+        DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+            input, tag, &unknown_fields_stream));
+        break;
+      }
+    }
+  }
+success:
+  // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.ThreatMatch)
+  return true;
+failure:
+  // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.ThreatMatch)
+  return false;
+#undef DO_
+}
+
+void ThreatMatch::SerializeWithCachedSizes(
+    ::google::protobuf::io::CodedOutputStream* output) const {
+  // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.ThreatMatch)
+  // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+  if (has_threat_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      1, this->threat_type(), output);
+  }
+
+  // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+  if (has_platform_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      2, this->platform_type(), output);
+  }
+
+  // optional .mozilla.safebrowsing.ThreatEntry threat = 3;
+  if (has_threat()) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      3, this->threat(), output);
+  }
+
+  // optional .mozilla.safebrowsing.ThreatEntryMetadata threat_entry_metadata = 4;
+  if (has_threat_entry_metadata()) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      4, this->threat_entry_metadata(), output);
+  }
+
+  // optional .mozilla.safebrowsing.Duration cache_duration = 5;
+  if (has_cache_duration()) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      5, this->cache_duration(), output);
+  }
+
+  // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 6;
+  if (has_threat_entry_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      6, this->threat_entry_type(), output);
+  }
+
+  output->WriteRaw(unknown_fields().data(),
+                   unknown_fields().size());
+  // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.ThreatMatch)
+}
+
+int ThreatMatch::ByteSize() const {
+  int total_size = 0;
+
+  if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+    if (has_threat_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_type());
+    }
+
+    // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+    if (has_platform_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->platform_type());
+    }
+
+    // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 6;
+    if (has_threat_entry_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_entry_type());
+    }
+
+    // optional .mozilla.safebrowsing.ThreatEntry threat = 3;
+    if (has_threat()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+          this->threat());
+    }
+
+    // optional .mozilla.safebrowsing.ThreatEntryMetadata threat_entry_metadata = 4;
+    if (has_threat_entry_metadata()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+          this->threat_entry_metadata());
+    }
+
+    // optional .mozilla.safebrowsing.Duration cache_duration = 5;
+    if (has_cache_duration()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+          this->cache_duration());
+    }
+
+  }
+  total_size += unknown_fields().size();
+
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = total_size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+  return total_size;
+}
+
+void ThreatMatch::CheckTypeAndMergeFrom(
+    const ::google::protobuf::MessageLite& from) {
+  MergeFrom(*::google::protobuf::down_cast<const ThreatMatch*>(&from));
+}
+
+void ThreatMatch::MergeFrom(const ThreatMatch& from) {
+  GOOGLE_CHECK_NE(&from, this);
+  if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    if (from.has_threat_type()) {
+      set_threat_type(from.threat_type());
+    }
+    if (from.has_platform_type()) {
+      set_platform_type(from.platform_type());
+    }
+    if (from.has_threat_entry_type()) {
+      set_threat_entry_type(from.threat_entry_type());
+    }
+    if (from.has_threat()) {
+      mutable_threat()->::mozilla::safebrowsing::ThreatEntry::MergeFrom(from.threat());
+    }
+    if (from.has_threat_entry_metadata()) {
+      mutable_threat_entry_metadata()->::mozilla::safebrowsing::ThreatEntryMetadata::MergeFrom(from.threat_entry_metadata());
+    }
+    if (from.has_cache_duration()) {
+      mutable_cache_duration()->::mozilla::safebrowsing::Duration::MergeFrom(from.cache_duration());
+    }
+  }
+  mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void ThreatMatch::CopyFrom(const ThreatMatch& from) {
+  if (&from == this) return;
+  Clear();
+  MergeFrom(from);
+}
+
+bool ThreatMatch::IsInitialized() const {
+
+  return true;
+}
+
+void ThreatMatch::Swap(ThreatMatch* other) {
+  if (other != this) {
+    std::swap(threat_type_, other->threat_type_);
+    std::swap(platform_type_, other->platform_type_);
+    std::swap(threat_entry_type_, other->threat_entry_type_);
+    std::swap(threat_, other->threat_);
+    std::swap(threat_entry_metadata_, other->threat_entry_metadata_);
+    std::swap(cache_duration_, other->cache_duration_);
+    std::swap(_has_bits_[0], other->_has_bits_[0]);
+    _unknown_fields_.swap(other->_unknown_fields_);
+    std::swap(_cached_size_, other->_cached_size_);
+  }
+}
+
+::std::string ThreatMatch::GetTypeName() const {
+  return "mozilla.safebrowsing.ThreatMatch";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int FindThreatMatchesRequest::kClientFieldNumber;
+const int FindThreatMatchesRequest::kThreatInfoFieldNumber;
+#endif  // !_MSC_VER
+
+FindThreatMatchesRequest::FindThreatMatchesRequest()
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FindThreatMatchesRequest)
+}
+
+void FindThreatMatchesRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  client_ = const_cast< ::mozilla::safebrowsing::ClientInfo*>(
+      ::mozilla::safebrowsing::ClientInfo::internal_default_instance());
+#else
+  client_ = const_cast< ::mozilla::safebrowsing::ClientInfo*>(&::mozilla::safebrowsing::ClientInfo::default_instance());
+#endif
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  threat_info_ = const_cast< ::mozilla::safebrowsing::ThreatInfo*>(
+      ::mozilla::safebrowsing::ThreatInfo::internal_default_instance());
+#else
+  threat_info_ = const_cast< ::mozilla::safebrowsing::ThreatInfo*>(&::mozilla::safebrowsing::ThreatInfo::default_instance());
+#endif
+}
+
+FindThreatMatchesRequest::FindThreatMatchesRequest(const FindThreatMatchesRequest& from)
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  MergeFrom(from);
+  // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FindThreatMatchesRequest)
+}
+
+void FindThreatMatchesRequest::SharedCtor() {
+  _cached_size_ = 0;
+  client_ = NULL;
+  threat_info_ = NULL;
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FindThreatMatchesRequest::~FindThreatMatchesRequest() {
+  // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FindThreatMatchesRequest)
+  SharedDtor();
+}
+
+void FindThreatMatchesRequest::SharedDtor() {
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  if (this != &default_instance()) {
+  #else
+  if (this != default_instance_) {
+  #endif
+    delete client_;
+    delete threat_info_;
+  }
+}
+
+void FindThreatMatchesRequest::SetCachedSize(int size) const {
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FindThreatMatchesRequest& FindThreatMatchesRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  protobuf_AddDesc_safebrowsing_2eproto();
+#else
+  if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+  return *default_instance_;
+}
+
+FindThreatMatchesRequest* FindThreatMatchesRequest::default_instance_ = NULL;
+
+FindThreatMatchesRequest* FindThreatMatchesRequest::New() const {
+  return new FindThreatMatchesRequest;
+}
+
+void FindThreatMatchesRequest::Clear() {
+  if (_has_bits_[0 / 32] & 3) {
+    if (has_client()) {
+      if (client_ != NULL) client_->::mozilla::safebrowsing::ClientInfo::Clear();
+    }
+    if (has_threat_info()) {
+      if (threat_info_ != NULL) threat_info_->::mozilla::safebrowsing::ThreatInfo::Clear();
+    }
+  }
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+  mutable_unknown_fields()->clear();
+}
+
+bool FindThreatMatchesRequest::MergePartialFromCodedStream(
+    ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+  ::google::protobuf::uint32 tag;
+  ::google::protobuf::io::StringOutputStream unknown_fields_string(
+      mutable_unknown_fields());
+  ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+      &unknown_fields_string);
+  // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FindThreatMatchesRequest)
+  for (;;) {
+    ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+    tag = p.first;
+    if (!p.second) goto handle_unusual;
+    switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+      // optional .mozilla.safebrowsing.ClientInfo client = 1;
+      case 1: {
+        if (tag == 10) {
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+               input, mutable_client()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(18)) goto parse_threat_info;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.ThreatInfo threat_info = 2;
+      case 2: {
+        if (tag == 18) {
+         parse_threat_info:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+               input, mutable_threat_info()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectAtEnd()) goto success;
+        break;
+      }
+
+      default: {
+      handle_unusual:
+        if (tag == 0 ||
+            ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+            ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+          goto success;
+        }
+        DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+            input, tag, &unknown_fields_stream));
+        break;
+      }
+    }
+  }
+success:
+  // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FindThreatMatchesRequest)
+  return true;
+failure:
+  // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FindThreatMatchesRequest)
+  return false;
+#undef DO_
+}
+
+void FindThreatMatchesRequest::SerializeWithCachedSizes(
+    ::google::protobuf::io::CodedOutputStream* output) const {
+  // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FindThreatMatchesRequest)
+  // optional .mozilla.safebrowsing.ClientInfo client = 1;
+  if (has_client()) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      1, this->client(), output);
+  }
+
+  // optional .mozilla.safebrowsing.ThreatInfo threat_info = 2;
+  if (has_threat_info()) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      2, this->threat_info(), output);
+  }
+
+  output->WriteRaw(unknown_fields().data(),
+                   unknown_fields().size());
+  // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FindThreatMatchesRequest)
+}
+
+int FindThreatMatchesRequest::ByteSize() const {
+  int total_size = 0;
+
+  if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    // optional .mozilla.safebrowsing.ClientInfo client = 1;
+    if (has_client()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+          this->client());
+    }
+
+    // optional .mozilla.safebrowsing.ThreatInfo threat_info = 2;
+    if (has_threat_info()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+          this->threat_info());
+    }
+
+  }
+  total_size += unknown_fields().size();
+
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = total_size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+  return total_size;
+}
+
+void FindThreatMatchesRequest::CheckTypeAndMergeFrom(
+    const ::google::protobuf::MessageLite& from) {
+  MergeFrom(*::google::protobuf::down_cast<const FindThreatMatchesRequest*>(&from));
+}
+
+void FindThreatMatchesRequest::MergeFrom(const FindThreatMatchesRequest& from) {
+  GOOGLE_CHECK_NE(&from, this);
+  if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    if (from.has_client()) {
+      mutable_client()->::mozilla::safebrowsing::ClientInfo::MergeFrom(from.client());
+    }
+    if (from.has_threat_info()) {
+      mutable_threat_info()->::mozilla::safebrowsing::ThreatInfo::MergeFrom(from.threat_info());
+    }
+  }
+  mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FindThreatMatchesRequest::CopyFrom(const FindThreatMatchesRequest& from) {
+  if (&from == this) return;
+  Clear();
+  MergeFrom(from);
+}
+
+bool FindThreatMatchesRequest::IsInitialized() const {
+
+  return true;
+}
+
+void FindThreatMatchesRequest::Swap(FindThreatMatchesRequest* other) {
+  if (other != this) {
+    std::swap(client_, other->client_);
+    std::swap(threat_info_, other->threat_info_);
+    std::swap(_has_bits_[0], other->_has_bits_[0]);
+    _unknown_fields_.swap(other->_unknown_fields_);
+    std::swap(_cached_size_, other->_cached_size_);
+  }
+}
+
+::std::string FindThreatMatchesRequest::GetTypeName() const {
+  return "mozilla.safebrowsing.FindThreatMatchesRequest";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int FindThreatMatchesResponse::kMatchesFieldNumber;
+#endif  // !_MSC_VER
+
+FindThreatMatchesResponse::FindThreatMatchesResponse()
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FindThreatMatchesResponse)
+}
+
+void FindThreatMatchesResponse::InitAsDefaultInstance() {
+}
+
+FindThreatMatchesResponse::FindThreatMatchesResponse(const FindThreatMatchesResponse& from)
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  MergeFrom(from);
+  // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FindThreatMatchesResponse)
+}
+
+void FindThreatMatchesResponse::SharedCtor() {
+  _cached_size_ = 0;
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FindThreatMatchesResponse::~FindThreatMatchesResponse() {
+  // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FindThreatMatchesResponse)
+  SharedDtor();
+}
+
+void FindThreatMatchesResponse::SharedDtor() {
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  if (this != &default_instance()) {
+  #else
+  if (this != default_instance_) {
+  #endif
+  }
+}
+
+void FindThreatMatchesResponse::SetCachedSize(int size) const {
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FindThreatMatchesResponse& FindThreatMatchesResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  protobuf_AddDesc_safebrowsing_2eproto();
+#else
+  if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+  return *default_instance_;
+}
+
+FindThreatMatchesResponse* FindThreatMatchesResponse::default_instance_ = NULL;
+
+FindThreatMatchesResponse* FindThreatMatchesResponse::New() const {
+  return new FindThreatMatchesResponse;
+}
+
+void FindThreatMatchesResponse::Clear() {
+  matches_.Clear();
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+  mutable_unknown_fields()->clear();
+}
+
+bool FindThreatMatchesResponse::MergePartialFromCodedStream(
+    ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+  ::google::protobuf::uint32 tag;
+  ::google::protobuf::io::StringOutputStream unknown_fields_string(
+      mutable_unknown_fields());
+  ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+      &unknown_fields_string);
+  // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FindThreatMatchesResponse)
+  for (;;) {
+    ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+    tag = p.first;
+    if (!p.second) goto handle_unusual;
+    switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+      // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+      case 1: {
+        if (tag == 10) {
+         parse_matches:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+                input, add_matches()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(10)) goto parse_matches;
+        if (input->ExpectAtEnd()) goto success;
+        break;
+      }
+
+      default: {
+      handle_unusual:
+        if (tag == 0 ||
+            ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+            ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+          goto success;
+        }
+        DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+            input, tag, &unknown_fields_stream));
+        break;
+      }
+    }
+  }
+success:
+  // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FindThreatMatchesResponse)
+  return true;
+failure:
+  // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FindThreatMatchesResponse)
+  return false;
+#undef DO_
+}
+
+void FindThreatMatchesResponse::SerializeWithCachedSizes(
+    ::google::protobuf::io::CodedOutputStream* output) const {
+  // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FindThreatMatchesResponse)
+  // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+  for (int i = 0; i < this->matches_size(); i++) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      1, this->matches(i), output);
+  }
+
+  output->WriteRaw(unknown_fields().data(),
+                   unknown_fields().size());
+  // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FindThreatMatchesResponse)
+}
+
+int FindThreatMatchesResponse::ByteSize() const {
+  int total_size = 0;
+
+  // repeated .mozilla.safebrowsing.ThreatMatch matches = 1;
+  total_size += 1 * this->matches_size();
+  for (int i = 0; i < this->matches_size(); i++) {
+    total_size +=
+      ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+        this->matches(i));
+  }
+
+  total_size += unknown_fields().size();
+
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = total_size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+  return total_size;
+}
+
+void FindThreatMatchesResponse::CheckTypeAndMergeFrom(
+    const ::google::protobuf::MessageLite& from) {
+  MergeFrom(*::google::protobuf::down_cast<const FindThreatMatchesResponse*>(&from));
+}
+
+void FindThreatMatchesResponse::MergeFrom(const FindThreatMatchesResponse& from) {
+  GOOGLE_CHECK_NE(&from, this);
+  matches_.MergeFrom(from.matches_);
+  mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FindThreatMatchesResponse::CopyFrom(const FindThreatMatchesResponse& from) {
+  if (&from == this) return;
+  Clear();
+  MergeFrom(from);
+}
+
+bool FindThreatMatchesResponse::IsInitialized() const {
+
+  return true;
+}
+
+void FindThreatMatchesResponse::Swap(FindThreatMatchesResponse* other) {
+  if (other != this) {
+    matches_.Swap(&other->matches_);
+    std::swap(_has_bits_[0], other->_has_bits_[0]);
+    _unknown_fields_.swap(other->_unknown_fields_);
+    std::swap(_cached_size_, other->_cached_size_);
+  }
+}
+
+::std::string FindThreatMatchesResponse::GetTypeName() const {
+  return "mozilla.safebrowsing.FindThreatMatchesResponse";
+}
+
+
+// ===================================================================
+
+#ifndef _MSC_VER
+const int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::kMaxUpdateEntriesFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::kMaxDatabaseEntriesFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::kRegionFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::kSupportedCompressionsFieldNumber;
+#endif  // !_MSC_VER
+
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints()
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::InitAsDefaultInstance() {
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints(const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& from)
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  MergeFrom(from);
+  // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::SharedCtor() {
+  ::google::protobuf::internal::GetEmptyString();
+  _cached_size_ = 0;
+  max_update_entries_ = 0;
+  max_database_entries_ = 0;
+  region_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::~FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints() {
+  // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+  SharedDtor();
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::SharedDtor() {
+  if (region_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+    delete region_;
+  }
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  if (this != &default_instance()) {
+  #else
+  if (this != default_instance_) {
+  #endif
+  }
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::SetCachedSize(int size) const {
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  protobuf_AddDesc_safebrowsing_2eproto();
+#else
+  if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+  return *default_instance_;
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance_ = NULL;
+
+FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::New() const {
+  return new FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>(      \
+  &reinterpret_cast<FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints*>(16)->f) - \
+   reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do {                              \
+    size_t f = OFFSET_OF_FIELD_(first);                    \
+    size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last);  \
+    ::memset(&first, 0, n);                                \
+  } while (0)
+
+  if (_has_bits_[0 / 32] & 7) {
+    ZR_(max_update_entries_, max_database_entries_);
+    if (has_region()) {
+      if (region_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+        region_->clear();
+      }
+    }
+  }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+  supported_compressions_.Clear();
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+  mutable_unknown_fields()->clear();
+}
+
+bool FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::MergePartialFromCodedStream(
+    ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+  ::google::protobuf::uint32 tag;
+  ::google::protobuf::io::StringOutputStream unknown_fields_string(
+      mutable_unknown_fields());
+  ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+      &unknown_fields_string);
+  // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+  for (;;) {
+    ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+    tag = p.first;
+    if (!p.second) goto handle_unusual;
+    switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+      // optional int32 max_update_entries = 1;
+      case 1: {
+        if (tag == 8) {
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+                 input, &max_update_entries_)));
+          set_has_max_update_entries();
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(16)) goto parse_max_database_entries;
+        break;
+      }
+
+      // optional int32 max_database_entries = 2;
+      case 2: {
+        if (tag == 16) {
+         parse_max_database_entries:
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   ::google::protobuf::int32, ::google::protobuf::internal::WireFormatLite::TYPE_INT32>(
+                 input, &max_database_entries_)));
+          set_has_max_database_entries();
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(26)) goto parse_region;
+        break;
+      }
+
+      // optional string region = 3;
+      case 3: {
+        if (tag == 26) {
+         parse_region:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadString(
+                input, this->mutable_region()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(32)) goto parse_supported_compressions;
+        break;
+      }
+
+      // repeated .mozilla.safebrowsing.CompressionType supported_compressions = 4;
+      case 4: {
+        if (tag == 32) {
+         parse_supported_compressions:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::CompressionType_IsValid(value)) {
+            add_supported_compressions(static_cast< ::mozilla::safebrowsing::CompressionType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else if (tag == 34) {
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPackedEnumNoInline(
+                 input,
+                 &::mozilla::safebrowsing::CompressionType_IsValid,
+                 this->mutable_supported_compressions())));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(32)) goto parse_supported_compressions;
+        if (input->ExpectAtEnd()) goto success;
+        break;
+      }
+
+      default: {
+      handle_unusual:
+        if (tag == 0 ||
+            ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+            ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+          goto success;
+        }
+        DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+            input, tag, &unknown_fields_stream));
+        break;
+      }
+    }
+  }
+success:
+  // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+  return true;
+failure:
+  // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+  return false;
+#undef DO_
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::SerializeWithCachedSizes(
+    ::google::protobuf::io::CodedOutputStream* output) const {
+  // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+  // optional int32 max_update_entries = 1;
+  if (has_max_update_entries()) {
+    ::google::protobuf::internal::WireFormatLite::WriteInt32(1, this->max_update_entries(), output);
+  }
+
+  // optional int32 max_database_entries = 2;
+  if (has_max_database_entries()) {
+    ::google::protobuf::internal::WireFormatLite::WriteInt32(2, this->max_database_entries(), output);
+  }
+
+  // optional string region = 3;
+  if (has_region()) {
+    ::google::protobuf::internal::WireFormatLite::WriteStringMaybeAliased(
+      3, this->region(), output);
+  }
+
+  // repeated .mozilla.safebrowsing.CompressionType supported_compressions = 4;
+  for (int i = 0; i < this->supported_compressions_size(); i++) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      4, this->supported_compressions(i), output);
+  }
+
+  output->WriteRaw(unknown_fields().data(),
+                   unknown_fields().size());
+  // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints)
+}
+
+int FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::ByteSize() const {
+  int total_size = 0;
+
+  if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    // optional int32 max_update_entries = 1;
+    if (has_max_update_entries()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::Int32Size(
+          this->max_update_entries());
+    }
+
+    // optional int32 max_database_entries = 2;
+    if (has_max_database_entries()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::Int32Size(
+          this->max_database_entries());
+    }
+
+    // optional string region = 3;
+    if (has_region()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::StringSize(
+          this->region());
+    }
+
+  }
+  // repeated .mozilla.safebrowsing.CompressionType supported_compressions = 4;
+  {
+    int data_size = 0;
+    for (int i = 0; i < this->supported_compressions_size(); i++) {
+      data_size += ::google::protobuf::internal::WireFormatLite::EnumSize(
+        this->supported_compressions(i));
+    }
+    total_size += 1 * this->supported_compressions_size() + data_size;
+  }
+
+  total_size += unknown_fields().size();
+
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = total_size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+  return total_size;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::CheckTypeAndMergeFrom(
+    const ::google::protobuf::MessageLite& from) {
+  MergeFrom(*::google::protobuf::down_cast<const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints*>(&from));
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::MergeFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& from) {
+  GOOGLE_CHECK_NE(&from, this);
+  supported_compressions_.MergeFrom(from.supported_compressions_);
+  if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    if (from.has_max_update_entries()) {
+      set_max_update_entries(from.max_update_entries());
+    }
+    if (from.has_max_database_entries()) {
+      set_max_database_entries(from.max_database_entries());
+    }
+    if (from.has_region()) {
+      set_region(from.region());
+    }
+  }
+  mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::CopyFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints& from) {
+  if (&from == this) return;
+  Clear();
+  MergeFrom(from);
+}
+
+bool FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::IsInitialized() const {
+
+  return true;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::Swap(FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints* other) {
+  if (other != this) {
+    std::swap(max_update_entries_, other->max_update_entries_);
+    std::swap(max_database_entries_, other->max_database_entries_);
+    std::swap(region_, other->region_);
+    supported_compressions_.Swap(&other->supported_compressions_);
+    std::swap(_has_bits_[0], other->_has_bits_[0]);
+    _unknown_fields_.swap(other->_unknown_fields_);
+    std::swap(_cached_size_, other->_cached_size_);
+  }
+}
+
+::std::string FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::GetTypeName() const {
+  return "mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int FetchThreatListUpdatesRequest_ListUpdateRequest::kThreatTypeFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest::kPlatformTypeFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest::kThreatEntryTypeFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest::kStateFieldNumber;
+const int FetchThreatListUpdatesRequest_ListUpdateRequest::kConstraintsFieldNumber;
+#endif  // !_MSC_VER
+
+FetchThreatListUpdatesRequest_ListUpdateRequest::FetchThreatListUpdatesRequest_ListUpdateRequest()
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  constraints_ = const_cast< ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints*>(
+      ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::internal_default_instance());
+#else
+  constraints_ = const_cast< ::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints*>(&::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::default_instance());
+#endif
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest::FetchThreatListUpdatesRequest_ListUpdateRequest(const FetchThreatListUpdatesRequest_ListUpdateRequest& from)
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  MergeFrom(from);
+  // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::SharedCtor() {
+  ::google::protobuf::internal::GetEmptyString();
+  _cached_size_ = 0;
+  threat_type_ = 0;
+  platform_type_ = 0;
+  threat_entry_type_ = 0;
+  state_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+  constraints_ = NULL;
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest::~FetchThreatListUpdatesRequest_ListUpdateRequest() {
+  // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+  SharedDtor();
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::SharedDtor() {
+  if (state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+    delete state_;
+  }
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  if (this != &default_instance()) {
+  #else
+  if (this != default_instance_) {
+  #endif
+    delete constraints_;
+  }
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::SetCachedSize(int size) const {
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FetchThreatListUpdatesRequest_ListUpdateRequest& FetchThreatListUpdatesRequest_ListUpdateRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  protobuf_AddDesc_safebrowsing_2eproto();
+#else
+  if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+  return *default_instance_;
+}
+
+FetchThreatListUpdatesRequest_ListUpdateRequest* FetchThreatListUpdatesRequest_ListUpdateRequest::default_instance_ = NULL;
+
+FetchThreatListUpdatesRequest_ListUpdateRequest* FetchThreatListUpdatesRequest_ListUpdateRequest::New() const {
+  return new FetchThreatListUpdatesRequest_ListUpdateRequest;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>(      \
+  &reinterpret_cast<FetchThreatListUpdatesRequest_ListUpdateRequest*>(16)->f) - \
+   reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do {                              \
+    size_t f = OFFSET_OF_FIELD_(first);                    \
+    size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last);  \
+    ::memset(&first, 0, n);                                \
+  } while (0)
+
+  if (_has_bits_[0 / 32] & 31) {
+    ZR_(threat_type_, platform_type_);
+    threat_entry_type_ = 0;
+    if (has_state()) {
+      if (state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+        state_->clear();
+      }
+    }
+    if (has_constraints()) {
+      if (constraints_ != NULL) constraints_->::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::Clear();
+    }
+  }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+  mutable_unknown_fields()->clear();
+}
+
+bool FetchThreatListUpdatesRequest_ListUpdateRequest::MergePartialFromCodedStream(
+    ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+  ::google::protobuf::uint32 tag;
+  ::google::protobuf::io::StringOutputStream unknown_fields_string(
+      mutable_unknown_fields());
+  ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+      &unknown_fields_string);
+  // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+  for (;;) {
+    ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+    tag = p.first;
+    if (!p.second) goto handle_unusual;
+    switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+      // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+      case 1: {
+        if (tag == 8) {
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::ThreatType_IsValid(value)) {
+            set_threat_type(static_cast< ::mozilla::safebrowsing::ThreatType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(16)) goto parse_platform_type;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+      case 2: {
+        if (tag == 16) {
+         parse_platform_type:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::PlatformType_IsValid(value)) {
+            set_platform_type(static_cast< ::mozilla::safebrowsing::PlatformType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(26)) goto parse_state;
+        break;
+      }
+
+      // optional bytes state = 3;
+      case 3: {
+        if (tag == 26) {
+         parse_state:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+                input, this->mutable_state()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(34)) goto parse_constraints;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints constraints = 4;
+      case 4: {
+        if (tag == 34) {
+         parse_constraints:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+               input, mutable_constraints()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(40)) goto parse_threat_entry_type;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 5;
+      case 5: {
+        if (tag == 40) {
+         parse_threat_entry_type:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::ThreatEntryType_IsValid(value)) {
+            set_threat_entry_type(static_cast< ::mozilla::safebrowsing::ThreatEntryType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectAtEnd()) goto success;
+        break;
+      }
+
+      default: {
+      handle_unusual:
+        if (tag == 0 ||
+            ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+            ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+          goto success;
+        }
+        DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+            input, tag, &unknown_fields_stream));
+        break;
+      }
+    }
+  }
+success:
+  // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+  return true;
+failure:
+  // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+  return false;
+#undef DO_
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::SerializeWithCachedSizes(
+    ::google::protobuf::io::CodedOutputStream* output) const {
+  // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+  // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+  if (has_threat_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      1, this->threat_type(), output);
+  }
+
+  // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+  if (has_platform_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      2, this->platform_type(), output);
+  }
+
+  // optional bytes state = 3;
+  if (has_state()) {
+    ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+      3, this->state(), output);
+  }
+
+  // optional .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints constraints = 4;
+  if (has_constraints()) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      4, this->constraints(), output);
+  }
+
+  // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 5;
+  if (has_threat_entry_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      5, this->threat_entry_type(), output);
+  }
+
+  output->WriteRaw(unknown_fields().data(),
+                   unknown_fields().size());
+  // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest)
+}
+
+int FetchThreatListUpdatesRequest_ListUpdateRequest::ByteSize() const {
+  int total_size = 0;
+
+  if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+    if (has_threat_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_type());
+    }
+
+    // optional .mozilla.safebrowsing.PlatformType platform_type = 2;
+    if (has_platform_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->platform_type());
+    }
+
+    // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 5;
+    if (has_threat_entry_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_entry_type());
+    }
+
+    // optional bytes state = 3;
+    if (has_state()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::BytesSize(
+          this->state());
+    }
+
+    // optional .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest.Constraints constraints = 4;
+    if (has_constraints()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+          this->constraints());
+    }
+
+  }
+  total_size += unknown_fields().size();
+
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = total_size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+  return total_size;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::CheckTypeAndMergeFrom(
+    const ::google::protobuf::MessageLite& from) {
+  MergeFrom(*::google::protobuf::down_cast<const FetchThreatListUpdatesRequest_ListUpdateRequest*>(&from));
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::MergeFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest& from) {
+  GOOGLE_CHECK_NE(&from, this);
+  if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    if (from.has_threat_type()) {
+      set_threat_type(from.threat_type());
+    }
+    if (from.has_platform_type()) {
+      set_platform_type(from.platform_type());
+    }
+    if (from.has_threat_entry_type()) {
+      set_threat_entry_type(from.threat_entry_type());
+    }
+    if (from.has_state()) {
+      set_state(from.state());
+    }
+    if (from.has_constraints()) {
+      mutable_constraints()->::mozilla::safebrowsing::FetchThreatListUpdatesRequest_ListUpdateRequest_Constraints::MergeFrom(from.constraints());
+    }
+  }
+  mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::CopyFrom(const FetchThreatListUpdatesRequest_ListUpdateRequest& from) {
+  if (&from == this) return;
+  Clear();
+  MergeFrom(from);
+}
+
+bool FetchThreatListUpdatesRequest_ListUpdateRequest::IsInitialized() const {
+
+  return true;
+}
+
+void FetchThreatListUpdatesRequest_ListUpdateRequest::Swap(FetchThreatListUpdatesRequest_ListUpdateRequest* other) {
+  if (other != this) {
+    std::swap(threat_type_, other->threat_type_);
+    std::swap(platform_type_, other->platform_type_);
+    std::swap(threat_entry_type_, other->threat_entry_type_);
+    std::swap(state_, other->state_);
+    std::swap(constraints_, other->constraints_);
+    std::swap(_has_bits_[0], other->_has_bits_[0]);
+    _unknown_fields_.swap(other->_unknown_fields_);
+    std::swap(_cached_size_, other->_cached_size_);
+  }
+}
+
+::std::string FetchThreatListUpdatesRequest_ListUpdateRequest::GetTypeName() const {
+  return "mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int FetchThreatListUpdatesRequest::kClientFieldNumber;
+const int FetchThreatListUpdatesRequest::kListUpdateRequestsFieldNumber;
+#endif  // !_MSC_VER
+
+FetchThreatListUpdatesRequest::FetchThreatListUpdatesRequest()
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+}
+
+void FetchThreatListUpdatesRequest::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  client_ = const_cast< ::mozilla::safebrowsing::ClientInfo*>(
+      ::mozilla::safebrowsing::ClientInfo::internal_default_instance());
+#else
+  client_ = const_cast< ::mozilla::safebrowsing::ClientInfo*>(&::mozilla::safebrowsing::ClientInfo::default_instance());
+#endif
+}
+
+FetchThreatListUpdatesRequest::FetchThreatListUpdatesRequest(const FetchThreatListUpdatesRequest& from)
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  MergeFrom(from);
+  // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+}
+
+void FetchThreatListUpdatesRequest::SharedCtor() {
+  _cached_size_ = 0;
+  client_ = NULL;
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FetchThreatListUpdatesRequest::~FetchThreatListUpdatesRequest() {
+  // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+  SharedDtor();
+}
+
+void FetchThreatListUpdatesRequest::SharedDtor() {
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  if (this != &default_instance()) {
+  #else
+  if (this != default_instance_) {
+  #endif
+    delete client_;
+  }
+}
+
+void FetchThreatListUpdatesRequest::SetCachedSize(int size) const {
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FetchThreatListUpdatesRequest& FetchThreatListUpdatesRequest::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  protobuf_AddDesc_safebrowsing_2eproto();
+#else
+  if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+  return *default_instance_;
+}
+
+FetchThreatListUpdatesRequest* FetchThreatListUpdatesRequest::default_instance_ = NULL;
+
+FetchThreatListUpdatesRequest* FetchThreatListUpdatesRequest::New() const {
+  return new FetchThreatListUpdatesRequest;
+}
+
+void FetchThreatListUpdatesRequest::Clear() {
+  if (has_client()) {
+    if (client_ != NULL) client_->::mozilla::safebrowsing::ClientInfo::Clear();
+  }
+  list_update_requests_.Clear();
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+  mutable_unknown_fields()->clear();
+}
+
+bool FetchThreatListUpdatesRequest::MergePartialFromCodedStream(
+    ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+  ::google::protobuf::uint32 tag;
+  ::google::protobuf::io::StringOutputStream unknown_fields_string(
+      mutable_unknown_fields());
+  ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+      &unknown_fields_string);
+  // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+  for (;;) {
+    ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+    tag = p.first;
+    if (!p.second) goto handle_unusual;
+    switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+      // optional .mozilla.safebrowsing.ClientInfo client = 1;
+      case 1: {
+        if (tag == 10) {
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+               input, mutable_client()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(26)) goto parse_list_update_requests;
+        break;
+      }
+
+      // repeated .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest list_update_requests = 3;
+      case 3: {
+        if (tag == 26) {
+         parse_list_update_requests:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+                input, add_list_update_requests()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(26)) goto parse_list_update_requests;
+        if (input->ExpectAtEnd()) goto success;
+        break;
+      }
+
+      default: {
+      handle_unusual:
+        if (tag == 0 ||
+            ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+            ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+          goto success;
+        }
+        DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+            input, tag, &unknown_fields_stream));
+        break;
+      }
+    }
+  }
+success:
+  // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+  return true;
+failure:
+  // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+  return false;
+#undef DO_
+}
+
+void FetchThreatListUpdatesRequest::SerializeWithCachedSizes(
+    ::google::protobuf::io::CodedOutputStream* output) const {
+  // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+  // optional .mozilla.safebrowsing.ClientInfo client = 1;
+  if (has_client()) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      1, this->client(), output);
+  }
+
+  // repeated .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest list_update_requests = 3;
+  for (int i = 0; i < this->list_update_requests_size(); i++) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      3, this->list_update_requests(i), output);
+  }
+
+  output->WriteRaw(unknown_fields().data(),
+                   unknown_fields().size());
+  // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FetchThreatListUpdatesRequest)
+}
+
+int FetchThreatListUpdatesRequest::ByteSize() const {
+  int total_size = 0;
+
+  if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    // optional .mozilla.safebrowsing.ClientInfo client = 1;
+    if (has_client()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+          this->client());
+    }
+
+  }
+  // repeated .mozilla.safebrowsing.FetchThreatListUpdatesRequest.ListUpdateRequest list_update_requests = 3;
+  total_size += 1 * this->list_update_requests_size();
+  for (int i = 0; i < this->list_update_requests_size(); i++) {
+    total_size +=
+      ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+        this->list_update_requests(i));
+  }
+
+  total_size += unknown_fields().size();
+
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = total_size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+  return total_size;
+}
+
+void FetchThreatListUpdatesRequest::CheckTypeAndMergeFrom(
+    const ::google::protobuf::MessageLite& from) {
+  MergeFrom(*::google::protobuf::down_cast<const FetchThreatListUpdatesRequest*>(&from));
+}
+
+void FetchThreatListUpdatesRequest::MergeFrom(const FetchThreatListUpdatesRequest& from) {
+  GOOGLE_CHECK_NE(&from, this);
+  list_update_requests_.MergeFrom(from.list_update_requests_);
+  if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    if (from.has_client()) {
+      mutable_client()->::mozilla::safebrowsing::ClientInfo::MergeFrom(from.client());
+    }
+  }
+  mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FetchThreatListUpdatesRequest::CopyFrom(const FetchThreatListUpdatesRequest& from) {
+  if (&from == this) return;
+  Clear();
+  MergeFrom(from);
+}
+
+bool FetchThreatListUpdatesRequest::IsInitialized() const {
+
+  return true;
+}
+
+void FetchThreatListUpdatesRequest::Swap(FetchThreatListUpdatesRequest* other) {
+  if (other != this) {
+    std::swap(client_, other->client_);
+    list_update_requests_.Swap(&other->list_update_requests_);
+    std::swap(_has_bits_[0], other->_has_bits_[0]);
+    _unknown_fields_.swap(other->_unknown_fields_);
+    std::swap(_cached_size_, other->_cached_size_);
+  }
+}
+
+::std::string FetchThreatListUpdatesRequest::GetTypeName() const {
+  return "mozilla.safebrowsing.FetchThreatListUpdatesRequest";
+}
+
+
+// ===================================================================
+
+bool FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_IsValid(int value) {
+  switch(value) {
+    case 0:
+    case 1:
+    case 2:
+      return true;
+    default:
+      return false;
+  }
+}
+
+#ifndef _MSC_VER
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::RESPONSE_TYPE_UNSPECIFIED;
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::PARTIAL_UPDATE;
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::FULL_UPDATE;
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::ResponseType_MIN;
+const FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType FetchThreatListUpdatesResponse_ListUpdateResponse::ResponseType_MAX;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::ResponseType_ARRAYSIZE;
+#endif  // _MSC_VER
+#ifndef _MSC_VER
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kThreatTypeFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kThreatEntryTypeFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kPlatformTypeFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kResponseTypeFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kAdditionsFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kRemovalsFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kNewClientStateFieldNumber;
+const int FetchThreatListUpdatesResponse_ListUpdateResponse::kChecksumFieldNumber;
+#endif  // !_MSC_VER
+
+FetchThreatListUpdatesResponse_ListUpdateResponse::FetchThreatListUpdatesResponse_ListUpdateResponse()
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  checksum_ = const_cast< ::mozilla::safebrowsing::Checksum*>(
+      ::mozilla::safebrowsing::Checksum::internal_default_instance());
+#else
+  checksum_ = const_cast< ::mozilla::safebrowsing::Checksum*>(&::mozilla::safebrowsing::Checksum::default_instance());
+#endif
+}
+
+FetchThreatListUpdatesResponse_ListUpdateResponse::FetchThreatListUpdatesResponse_ListUpdateResponse(const FetchThreatListUpdatesResponse_ListUpdateResponse& from)
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  MergeFrom(from);
+  // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::SharedCtor() {
+  ::google::protobuf::internal::GetEmptyString();
+  _cached_size_ = 0;
+  threat_type_ = 0;
+  threat_entry_type_ = 0;
+  platform_type_ = 0;
+  response_type_ = 0;
+  new_client_state_ = const_cast< ::std::string*>(&::google::protobuf::internal::GetEmptyStringAlreadyInited());
+  checksum_ = NULL;
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FetchThreatListUpdatesResponse_ListUpdateResponse::~FetchThreatListUpdatesResponse_ListUpdateResponse() {
+  // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+  SharedDtor();
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::SharedDtor() {
+  if (new_client_state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+    delete new_client_state_;
+  }
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  if (this != &default_instance()) {
+  #else
+  if (this != default_instance_) {
+  #endif
+    delete checksum_;
+  }
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::SetCachedSize(int size) const {
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FetchThreatListUpdatesResponse_ListUpdateResponse& FetchThreatListUpdatesResponse_ListUpdateResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  protobuf_AddDesc_safebrowsing_2eproto();
+#else
+  if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+  return *default_instance_;
+}
+
+FetchThreatListUpdatesResponse_ListUpdateResponse* FetchThreatListUpdatesResponse_ListUpdateResponse::default_instance_ = NULL;
+
+FetchThreatListUpdatesResponse_ListUpdateResponse* FetchThreatListUpdatesResponse_ListUpdateResponse::New() const {
+  return new FetchThreatListUpdatesResponse_ListUpdateResponse;
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::Clear() {
+#define OFFSET_OF_FIELD_(f) (reinterpret_cast<char*>(      \
+  &reinterpret_cast<FetchThreatListUpdatesResponse_ListUpdateResponse*>(16)->f) - \
+   reinterpret_cast<char*>(16))
+
+#define ZR_(first, last) do {                              \
+    size_t f = OFFSET_OF_FIELD_(first);                    \
+    size_t n = OFFSET_OF_FIELD_(last) - f + sizeof(last);  \
+    ::memset(&first, 0, n);                                \
+  } while (0)
+
+  if (_has_bits_[0 / 32] & 207) {
+    ZR_(threat_type_, response_type_);
+    if (has_new_client_state()) {
+      if (new_client_state_ != &::google::protobuf::internal::GetEmptyStringAlreadyInited()) {
+        new_client_state_->clear();
+      }
+    }
+    if (has_checksum()) {
+      if (checksum_ != NULL) checksum_->::mozilla::safebrowsing::Checksum::Clear();
+    }
+  }
+
+#undef OFFSET_OF_FIELD_
+#undef ZR_
+
+  additions_.Clear();
+  removals_.Clear();
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+  mutable_unknown_fields()->clear();
+}
+
+bool FetchThreatListUpdatesResponse_ListUpdateResponse::MergePartialFromCodedStream(
+    ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+  ::google::protobuf::uint32 tag;
+  ::google::protobuf::io::StringOutputStream unknown_fields_string(
+      mutable_unknown_fields());
+  ::google::protobuf::io::CodedOutputStream unknown_fields_stream(
+      &unknown_fields_string);
+  // @@protoc_insertion_point(parse_start:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+  for (;;) {
+    ::std::pair< ::google::protobuf::uint32, bool> p = input->ReadTagWithCutoff(127);
+    tag = p.first;
+    if (!p.second) goto handle_unusual;
+    switch (::google::protobuf::internal::WireFormatLite::GetTagFieldNumber(tag)) {
+      // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+      case 1: {
+        if (tag == 8) {
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::ThreatType_IsValid(value)) {
+            set_threat_type(static_cast< ::mozilla::safebrowsing::ThreatType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(16)) goto parse_threat_entry_type;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 2;
+      case 2: {
+        if (tag == 16) {
+         parse_threat_entry_type:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::ThreatEntryType_IsValid(value)) {
+            set_threat_entry_type(static_cast< ::mozilla::safebrowsing::ThreatEntryType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(24)) goto parse_platform_type;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.PlatformType platform_type = 3;
+      case 3: {
+        if (tag == 24) {
+         parse_platform_type:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::PlatformType_IsValid(value)) {
+            set_platform_type(static_cast< ::mozilla::safebrowsing::PlatformType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(32)) goto parse_response_type;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.ResponseType response_type = 4;
+      case 4: {
+        if (tag == 32) {
+         parse_response_type:
+          int value;
+          DO_((::google::protobuf::internal::WireFormatLite::ReadPrimitive<
+                   int, ::google::protobuf::internal::WireFormatLite::TYPE_ENUM>(
+                 input, &value)));
+          if (::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType_IsValid(value)) {
+            set_response_type(static_cast< ::mozilla::safebrowsing::FetchThreatListUpdatesResponse_ListUpdateResponse_ResponseType >(value));
+          } else {
+            unknown_fields_stream.WriteVarint32(tag);
+            unknown_fields_stream.WriteVarint32(value);
+          }
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(42)) goto parse_additions;
+        break;
+      }
+
+      // repeated .mozilla.safebrowsing.ThreatEntrySet additions = 5;
+      case 5: {
+        if (tag == 42) {
+         parse_additions:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+                input, add_additions()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(42)) goto parse_additions;
+        if (input->ExpectTag(50)) goto parse_removals;
+        break;
+      }
+
+      // repeated .mozilla.safebrowsing.ThreatEntrySet removals = 6;
+      case 6: {
+        if (tag == 50) {
+         parse_removals:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+                input, add_removals()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(50)) goto parse_removals;
+        if (input->ExpectTag(58)) goto parse_new_client_state;
+        break;
+      }
+
+      // optional bytes new_client_state = 7;
+      case 7: {
+        if (tag == 58) {
+         parse_new_client_state:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadBytes(
+                input, this->mutable_new_client_state()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectTag(66)) goto parse_checksum;
+        break;
+      }
+
+      // optional .mozilla.safebrowsing.Checksum checksum = 8;
+      case 8: {
+        if (tag == 66) {
+         parse_checksum:
+          DO_(::google::protobuf::internal::WireFormatLite::ReadMessageNoVirtual(
+               input, mutable_checksum()));
+        } else {
+          goto handle_unusual;
+        }
+        if (input->ExpectAtEnd()) goto success;
+        break;
+      }
+
+      default: {
+      handle_unusual:
+        if (tag == 0 ||
+            ::google::protobuf::internal::WireFormatLite::GetTagWireType(tag) ==
+            ::google::protobuf::internal::WireFormatLite::WIRETYPE_END_GROUP) {
+          goto success;
+        }
+        DO_(::google::protobuf::internal::WireFormatLite::SkipField(
+            input, tag, &unknown_fields_stream));
+        break;
+      }
+    }
+  }
+success:
+  // @@protoc_insertion_point(parse_success:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+  return true;
+failure:
+  // @@protoc_insertion_point(parse_failure:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+  return false;
+#undef DO_
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::SerializeWithCachedSizes(
+    ::google::protobuf::io::CodedOutputStream* output) const {
+  // @@protoc_insertion_point(serialize_start:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+  // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+  if (has_threat_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      1, this->threat_type(), output);
+  }
+
+  // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 2;
+  if (has_threat_entry_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      2, this->threat_entry_type(), output);
+  }
+
+  // optional .mozilla.safebrowsing.PlatformType platform_type = 3;
+  if (has_platform_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      3, this->platform_type(), output);
+  }
+
+  // optional .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.ResponseType response_type = 4;
+  if (has_response_type()) {
+    ::google::protobuf::internal::WireFormatLite::WriteEnum(
+      4, this->response_type(), output);
+  }
+
+  // repeated .mozilla.safebrowsing.ThreatEntrySet additions = 5;
+  for (int i = 0; i < this->additions_size(); i++) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      5, this->additions(i), output);
+  }
+
+  // repeated .mozilla.safebrowsing.ThreatEntrySet removals = 6;
+  for (int i = 0; i < this->removals_size(); i++) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      6, this->removals(i), output);
+  }
+
+  // optional bytes new_client_state = 7;
+  if (has_new_client_state()) {
+    ::google::protobuf::internal::WireFormatLite::WriteBytesMaybeAliased(
+      7, this->new_client_state(), output);
+  }
+
+  // optional .mozilla.safebrowsing.Checksum checksum = 8;
+  if (has_checksum()) {
+    ::google::protobuf::internal::WireFormatLite::WriteMessage(
+      8, this->checksum(), output);
+  }
+
+  output->WriteRaw(unknown_fields().data(),
+                   unknown_fields().size());
+  // @@protoc_insertion_point(serialize_end:mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse)
+}
+
+int FetchThreatListUpdatesResponse_ListUpdateResponse::ByteSize() const {
+  int total_size = 0;
+
+  if (_has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    // optional .mozilla.safebrowsing.ThreatType threat_type = 1;
+    if (has_threat_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_type());
+    }
+
+    // optional .mozilla.safebrowsing.ThreatEntryType threat_entry_type = 2;
+    if (has_threat_entry_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->threat_entry_type());
+    }
+
+    // optional .mozilla.safebrowsing.PlatformType platform_type = 3;
+    if (has_platform_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->platform_type());
+    }
+
+    // optional .mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse.ResponseType response_type = 4;
+    if (has_response_type()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::EnumSize(this->response_type());
+    }
+
+    // optional bytes new_client_state = 7;
+    if (has_new_client_state()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::BytesSize(
+          this->new_client_state());
+    }
+
+    // optional .mozilla.safebrowsing.Checksum checksum = 8;
+    if (has_checksum()) {
+      total_size += 1 +
+        ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+          this->checksum());
+    }
+
+  }
+  // repeated .mozilla.safebrowsing.ThreatEntrySet additions = 5;
+  total_size += 1 * this->additions_size();
+  for (int i = 0; i < this->additions_size(); i++) {
+    total_size +=
+      ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+        this->additions(i));
+  }
+
+  // repeated .mozilla.safebrowsing.ThreatEntrySet removals = 6;
+  total_size += 1 * this->removals_size();
+  for (int i = 0; i < this->removals_size(); i++) {
+    total_size +=
+      ::google::protobuf::internal::WireFormatLite::MessageSizeNoVirtual(
+        this->removals(i));
+  }
+
+  total_size += unknown_fields().size();
+
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = total_size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+  return total_size;
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::CheckTypeAndMergeFrom(
+    const ::google::protobuf::MessageLite& from) {
+  MergeFrom(*::google::protobuf::down_cast<const FetchThreatListUpdatesResponse_ListUpdateResponse*>(&from));
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::MergeFrom(const FetchThreatListUpdatesResponse_ListUpdateResponse& from) {
+  GOOGLE_CHECK_NE(&from, this);
+  additions_.MergeFrom(from.additions_);
+  removals_.MergeFrom(from.removals_);
+  if (from._has_bits_[0 / 32] & (0xffu << (0 % 32))) {
+    if (from.has_threat_type()) {
+      set_threat_type(from.threat_type());
+    }
+    if (from.has_threat_entry_type()) {
+      set_threat_entry_type(from.threat_entry_type());
+    }
+    if (from.has_platform_type()) {
+      set_platform_type(from.platform_type());
+    }
+    if (from.has_response_type()) {
+      set_response_type(from.response_type());
+    }
+    if (from.has_new_client_state()) {
+      set_new_client_state(from.new_client_state());
+    }
+    if (from.has_checksum()) {
+      mutable_checksum()->::mozilla::safebrowsing::Checksum::MergeFrom(from.checksum());
+    }
+  }
+  mutable_unknown_fields()->append(from.unknown_fields());
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::CopyFrom(const FetchThreatListUpdatesResponse_ListUpdateResponse& from) {
+  if (&from == this) return;
+  Clear();
+  MergeFrom(from);
+}
+
+bool FetchThreatListUpdatesResponse_ListUpdateResponse::IsInitialized() const {
+
+  return true;
+}
+
+void FetchThreatListUpdatesResponse_ListUpdateResponse::Swap(FetchThreatListUpdatesResponse_ListUpdateResponse* other) {
+  if (other != this) {
+    std::swap(threat_type_, other->threat_type_);
+    std::swap(threat_entry_type_, other->threat_entry_type_);
+    std::swap(platform_type_, other->platform_type_);
+    std::swap(response_type_, other->response_type_);
+    additions_.Swap(&other->additions_);
+    removals_.Swap(&other->removals_);
+    std::swap(new_client_state_, other->new_client_state_);
+    std::swap(checksum_, other->checksum_);
+    std::swap(_has_bits_[0], other->_has_bits_[0]);
+    _unknown_fields_.swap(other->_unknown_fields_);
+    std::swap(_cached_size_, other->_cached_size_);
+  }
+}
+
+::std::string FetchThreatListUpdatesResponse_ListUpdateResponse::GetTypeName() const {
+  return "mozilla.safebrowsing.FetchThreatListUpdatesResponse.ListUpdateResponse";
+}
+
+
+// -------------------------------------------------------------------
+
+#ifndef _MSC_VER
+const int FetchThreatListUpdatesResponse::kListUpdateResponsesFieldNumber;
+const int FetchThreatListUpdatesResponse::kMinimumWaitDurationFieldNumber;
+#endif  // !_MSC_VER
+
+FetchThreatListUpdatesResponse::FetchThreatListUpdatesResponse()
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  // @@protoc_insertion_point(constructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+}
+
+void FetchThreatListUpdatesResponse::InitAsDefaultInstance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  minimum_wait_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(
+      ::mozilla::safebrowsing::Duration::internal_default_instance());
+#else
+  minimum_wait_duration_ = const_cast< ::mozilla::safebrowsing::Duration*>(&::mozilla::safebrowsing::Duration::default_instance());
+#endif
+}
+
+FetchThreatListUpdatesResponse::FetchThreatListUpdatesResponse(const FetchThreatListUpdatesResponse& from)
+  : ::google::protobuf::MessageLite() {
+  SharedCtor();
+  MergeFrom(from);
+  // @@protoc_insertion_point(copy_constructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+}
+
+void FetchThreatListUpdatesResponse::SharedCtor() {
+  _cached_size_ = 0;
+  minimum_wait_duration_ = NULL;
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+}
+
+FetchThreatListUpdatesResponse::~FetchThreatListUpdatesResponse() {
+  // @@protoc_insertion_point(destructor:mozilla.safebrowsing.FetchThreatListUpdatesResponse)
+  SharedDtor();
+}
+
+void FetchThreatListUpdatesResponse::SharedDtor() {
+  #ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  if (this != &default_instance()) {
+  #else
+  if (this != default_instance_) {
+  #endif
+    delete minimum_wait_duration_;
+  }
+}
+
+void FetchThreatListUpdatesResponse::SetCachedSize(int size) const {
+  GOOGLE_SAFE_CONCURRENT_WRITES_BEGIN();
+  _cached_size_ = size;
+  GOOGLE_SAFE_CONCURRENT_WRITES_END();
+}
+const FetchThreatListUpdatesResponse& FetchThreatListUpdatesResponse::default_instance() {
+#ifdef GOOGLE_PROTOBUF_NO_STATIC_INITIALIZER
+  protobuf_AddDesc_safebrowsing_2eproto();
+#else
+  if (default_instance_ == NULL) protobuf_AddDesc_safebrowsing_2eproto();
+#endif
+  return *default_instance_;
+}
+
+FetchThreatListUpdatesResponse* FetchThreatListUpdatesResponse::default_instance_ = NULL;
+
+FetchThreatListUpdatesResponse* FetchThreatListUpdatesResponse::New() const {
+  return new FetchThreatListUpdatesResponse;
+}
+
+void FetchThreatListUpdatesResponse::Clear() {
+  if (has_minimum_wait_duration()) {
+    if (minimum_wait_duration_ != NULL) minimum_wait_duration_->::mozilla::safebrowsing::Duration::Clear();
+  }
+  list_update_responses_.Clear();
+  ::memset(_has_bits_, 0, sizeof(_has_bits_));
+  mutable_unknown_fields()->clear();
+}
+
+bool FetchThreatListUpdatesResponse::MergePartialFromCodedStream(
+    ::google::protobuf::io::CodedInputStream* input) {
+#define DO_(EXPRESSION) if (!(EXPRESSION)) goto failure
+  ::google::protobuf::uint32 tag;