// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* 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";

var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;

Cu.import("resource://gre/modules/AppConstants.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/AddonManager.jsm");
Cu.import("resource://gre/modules/AsyncPrefs.jsm");
Cu.import("resource://gre/modules/DelayedInit.jsm");

if (AppConstants.ACCESSIBILITY) {
  XPCOMUtils.defineLazyModuleGetter(this, "AccessFu",
                                    "resource://gre/modules/accessibility/AccessFu.jsm");
}

XPCOMUtils.defineLazyModuleGetter(this, "SpatialNavigation",
                                  "resource://gre/modules/SpatialNavigation.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "DownloadNotifications",
                                  "resource://gre/modules/DownloadNotifications.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                  "resource://gre/modules/FileUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "JNI",
                                  "resource://gre/modules/JNI.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "UITelemetry",
                                  "resource://gre/modules/UITelemetry.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
                                  "resource://gre/modules/PluralForm.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
                                  "resource://gre/modules/Downloads.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Messaging",
                                  "resource://gre/modules/Messaging.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides",
                                  "resource://gre/modules/UserAgentOverrides.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
                                  "resource://gre/modules/LoginManagerContent.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerParent",
                                  "resource://gre/modules/LoginManagerParent.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");

#ifdef MOZ_SAFE_BROWSING
  XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
                                    "resource://gre/modules/SafeBrowsing.jsm");
#endif

XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                  "resource://gre/modules/BrowserUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
                                  "resource://gre/modules/Sanitizer.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
                                  "resource://gre/modules/Prompt.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
                                  "resource://gre/modules/HelperApps.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
                                  "resource://gre/modules/SSLExceptions.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
                                  "resource://gre/modules/FormHistory.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
                                   "@mozilla.org/uuid-generator;1",
                                   "nsIUUIDGenerator");

XPCOMUtils.defineLazyServiceGetter(this, "Profiler",
                                   "@mozilla.org/tools/profiler;1",
                                   "nsIProfiler");

XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
                                  "resource://gre/modules/SimpleServiceDiscovery.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
                                  "resource://gre/modules/CharsetMenu.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "NetErrorHelper",
                                  "resource://gre/modules/NetErrorHelper.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "PermissionsUtils",
                                  "resource://gre/modules/PermissionsUtils.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                  "resource://gre/modules/Preferences.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "SharedPreferences",
                                  "resource://gre/modules/SharedPreferences.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Notifications",
                                  "resource://gre/modules/Notifications.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "RuntimePermissions", "resource://gre/modules/RuntimePermissions.jsm");

XPCOMUtils.defineLazyModuleGetter(this, "WebsiteMetadata", "resource://gre/modules/WebsiteMetadata.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "FontEnumerator",
  "@mozilla.org/gfx/fontenumerator;1",
  "nsIFontEnumerator");

var lazilyLoadedBrowserScripts = [
  ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
  ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
  ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
  ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
  ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
  ["Linkifier", "chrome://browser/content/Linkify.js"],
  ["CastingApps", "chrome://browser/content/CastingApps.js"],
  ["RemoteDebugger", "chrome://browser/content/RemoteDebugger.js"],
];
if (!AppConstants.RELEASE_OR_BETA) {
  lazilyLoadedBrowserScripts.push(
    ["WebcompatReporter", "chrome://browser/content/WebcompatReporter.js"]);
}

lazilyLoadedBrowserScripts.forEach(function (aScript) {
  let [name, script] = aScript;
  XPCOMUtils.defineLazyGetter(window, name, function() {
    let sandbox = {};
    Services.scriptloader.loadSubScript(script, sandbox);
    return sandbox[name];
  });
});

var lazilyLoadedObserverScripts = [
  ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
  ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
  ["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
  ["PermissionsHelper", ["Permissions:Check", "Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
  ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
  ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
  ["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
  ["Reader", ["Reader:AddToCache", "Reader:RemoveFromCache"], "chrome://browser/content/Reader.js"],
  ["PrintHelper", ["Print:PDF"], "chrome://browser/content/PrintHelper.js"],
];

lazilyLoadedObserverScripts.push(
["ActionBarHandler", ["TextSelection:Get", "TextSelection:Action", "TextSelection:End"],
  "chrome://browser/content/ActionBarHandler.js"]
);

if (AppConstants.MOZ_WEBRTC) {
  lazilyLoadedObserverScripts.push(
    ["WebrtcUI", ["getUserMedia:request",
                  "PeerConnection:request",
                  "recording-device-events",
                  "VideoCapture:Paused",
                  "VideoCapture:Resumed"], "chrome://browser/content/WebrtcUI.js"])
}

lazilyLoadedObserverScripts.forEach(function (aScript) {
  let [name, notifications, script] = aScript;
  XPCOMUtils.defineLazyGetter(window, name, function() {
    let sandbox = {};
    Services.scriptloader.loadSubScript(script, sandbox);
    return sandbox[name];
  });
  let observer = (s, t, d) => {
    Services.obs.removeObserver(observer, t);
    Services.obs.addObserver(window[name], t, false);
    window[name].observe(s, t, d); // Explicitly notify new observer
  };
  notifications.forEach((notification) => {
    Services.obs.addObserver(observer, notification, false);
  });
});

// Lazily-loaded browser scripts that use message listeners.
[
  ["Reader", [
    ["Reader:AddToCache", false],
    ["Reader:RemoveFromCache", false],
    ["Reader:ArticleGet", false],
    ["Reader:DropdownClosed", true], // 'true' allows us to survive mid-air cycle-collection.
    ["Reader:DropdownOpened", false],
    ["Reader:FaviconRequest", false],
    ["Reader:ToolbarHidden", false],
    ["Reader:SystemUIVisibility", false],
    ["Reader:UpdateReaderButton", false],
  ], "chrome://browser/content/Reader.js"],
].forEach(aScript => {
  let [name, messages, script] = aScript;
  XPCOMUtils.defineLazyGetter(window, name, function() {
    let sandbox = {};
    Services.scriptloader.loadSubScript(script, sandbox);
    return sandbox[name];
  });

  let mm = window.getGroupMessageManager("browsers");
  let listener = (message) => {
    mm.removeMessageListener(message.name, listener);
    let listenAfterClose = false;
    for (let [name, laClose] of messages) {
      if (message.name === name) {
        listenAfterClose = laClose;
        break;
      }
    }

    mm.addMessageListener(message.name, window[name], listenAfterClose);
    window[name].receiveMessage(message);
  };

  messages.forEach((message) => {
    let [name, listenAfterClose] = message;
    mm.addMessageListener(name, listener, listenAfterClose);
  });
});

// Lazily-loaded JS modules that use observer notifications
[
  ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView",
            "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
].forEach(module => {
  let [name, notifications, resource] = module;
  XPCOMUtils.defineLazyModuleGetter(this, name, resource);
  let observer = (s, t, d) => {
    Services.obs.removeObserver(observer, t);
    Services.obs.addObserver(this[name], t, false);
    this[name].observe(s, t, d); // Explicitly notify new observer
  };
  notifications.forEach(notification => {
    Services.obs.addObserver(observer, notification, false);
  });
});

XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
  "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");

XPCOMUtils.defineLazyServiceGetter(this, "ParentalControls",
  "@mozilla.org/parental-controls-service;1", "nsIParentalControlsService");

XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
  "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");

XPCOMUtils.defineLazyServiceGetter(window, "URIFixup",
  "@mozilla.org/docshell/urifixup;1", "nsIURIFixup");

if (AppConstants.MOZ_WEBRTC) {
  XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
    "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService");
}

XPCOMUtils.defineLazyModuleGetter(this, "Log",
  "resource://gre/modules/AndroidLog.jsm", "AndroidLog");

// Define the "dump" function as a binding of the Log.d function so it specifies
// the "debug" priority and a log tag.
function dump(msg) {
  Log.d("Browser", msg);
}

const kStateActive = 0x00000001; // :active pseudoclass for elements

const kXLinkNamespace = "http://www.w3.org/1999/xlink";

function fuzzyEquals(a, b) {
  return (Math.abs(a - b) < 1e-6);
}

XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
  let ContentAreaUtils = {};
  Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
  return ContentAreaUtils;
});

XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Point", "resource://gre/modules/Geometry.jsm");

function resolveGeckoURI(aURI) {
  if (!aURI)
    throw "Can't resolve an empty uri";

  if (aURI.startsWith("chrome://")) {
    let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
    return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
  } else if (aURI.startsWith("resource://")) {
    let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
    return handler.resolveURI(Services.io.newURI(aURI, null, null));
  }
  return aURI;
}

/**
 * Cache of commonly used string bundles.
 */
var Strings = {
  init: function () {
    XPCOMUtils.defineLazyGetter(Strings, "brand", () => Services.strings.createBundle("chrome://branding/locale/brand.properties"));
    XPCOMUtils.defineLazyGetter(Strings, "browser", () => Services.strings.createBundle("chrome://browser/locale/browser.properties"));
    XPCOMUtils.defineLazyGetter(Strings, "reader", () => Services.strings.createBundle("chrome://global/locale/aboutReader.properties"));
  },

  flush: function () {
    Services.strings.flushBundles();
    this.init();
  },
};

Strings.init();

const kFormHelperModeDisabled = 0;
const kFormHelperModeEnabled = 1;
const kFormHelperModeDynamic = 2;   // disabled on tablets
const kMaxHistoryListSize = 50;

function InitLater(fn, object, name) {
  return DelayedInit.schedule(fn, object, name, 15000 /* 15s max wait */);
}

var BrowserApp = {
  _tabs: [],
  _selectedTab: null,

  get isTablet() {
    let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
    delete this.isTablet;
    return this.isTablet = sysInfo.get("tablet");
  },

  get isOnLowMemoryPlatform() {
    let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
    delete this.isOnLowMemoryPlatform;
    return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
  },

  deck: null,

  startup: function startup() {
    window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
    dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
    Services.obs.notifyObservers(this.browser, "BrowserChrome:Ready", null);

    this.deck = document.getElementById("browsers");

    BrowserEventHandler.init();

    ViewportHandler.init();

    Services.androidBridge.browserApp = this;

    Services.obs.addObserver(this, "Locale:OS", false);
    Services.obs.addObserver(this, "Locale:Changed", false);
    Services.obs.addObserver(this, "Tab:Load", false);
    Services.obs.addObserver(this, "Tab:Selected", false);
    Services.obs.addObserver(this, "Tab:Closed", false);
    Services.obs.addObserver(this, "Session:Back", false);
    Services.obs.addObserver(this, "Session:Forward", false);
    Services.obs.addObserver(this, "Session:Navigate", false);
    Services.obs.addObserver(this, "Session:Reload", false);
    Services.obs.addObserver(this, "Session:Stop", false);
    Services.obs.addObserver(this, "SaveAs:PDF", false);
    Services.obs.addObserver(this, "Browser:Quit", false);
    Services.obs.addObserver(this, "ScrollTo:FocusedInput", false);
    Services.obs.addObserver(this, "Sanitize:ClearData", false);
    Services.obs.addObserver(this, "FullScreen:Exit", false);
    Services.obs.addObserver(this, "Passwords:Init", false);
    Services.obs.addObserver(this, "FormHistory:Init", false);
    Services.obs.addObserver(this, "android-get-pref", false);
    Services.obs.addObserver(this, "android-set-pref", false);
    Services.obs.addObserver(this, "gather-telemetry", false);
    Services.obs.addObserver(this, "keyword-search", false);
    Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
    Services.obs.addObserver(this, "Fonts:Reload", false);
    Services.obs.addObserver(this, "Vibration:Request", false);

    Messaging.addListener(this.getHistory.bind(this), "Session:GetHistory");

    window.addEventListener("fullscreen", function() {
      Messaging.sendRequest({
        type: window.fullScreen ? "ToggleChrome:Hide" : "ToggleChrome:Show"
      });
    }, false);

    window.addEventListener("fullscreenchange", (e) => {
      // This event gets fired on the document and its entire ancestor chain
      // of documents. When enabling fullscreen, it is fired on the top-level
      // document first and goes down; when disabling the order is reversed
      // (per spec). This means the last event on enabling will be for the innermost
      // document, which will have fullscreenElement set correctly.
      let doc = e.target;
      Messaging.sendRequest({
        type: doc.fullscreenElement ? "DOMFullScreen:Start" : "DOMFullScreen:Stop",
        rootElement: doc.fullscreenElement == doc.documentElement
      });

      if (this.fullscreenTransitionTab) {
        // Tab selection has changed during a fullscreen transition, handle it now.
        let tab = this.fullscreenTransitionTab;
        this.fullscreenTransitionTab = null;
        this._handleTabSelected(tab);
      }
    }, false);

    NativeWindow.init();
    FormAssistant.init();
    IndexedDB.init();
    XPInstallObserver.init();
    CharacterEncoding.init();
    ActivityObserver.init();
    RemoteDebugger.init();
    UserAgentOverrides.init();
    DesktopUserAgent.init();
    Distribution.init();
    Tabs.init();
    SearchEngines.init();
    Experiments.init();

    // XXX maybe we don't do this if the launch was kicked off from external
    Services.io.offline = false;

    // Broadcast a UIReady message so add-ons know we are finished with startup
    let event = document.createEvent("Events");
    event.initEvent("UIReady", true, false);
    window.dispatchEvent(event);

    if (this._startupStatus) {
      this.onAppUpdated();
    }

    if (!ParentalControls.isAllowed(ParentalControls.INSTALL_EXTENSION)) {
      // Disable extension installs
      Services.prefs.setIntPref("extensions.enabledScopes", 1);
      Services.prefs.setIntPref("extensions.autoDisableScopes", 1);
      Services.prefs.setBoolPref("xpinstall.enabled", false);
    } else if (ParentalControls.parentalControlsEnabled) {
      Services.prefs.clearUserPref("extensions.enabledScopes");
      Services.prefs.clearUserPref("extensions.autoDisableScopes");
      Services.prefs.setBoolPref("xpinstall.enabled", true);
    }

    if (ParentalControls.parentalControlsEnabled) {
        let isBlockListEnabled = ParentalControls.isAllowed(ParentalControls.BLOCK_LIST);
        Services.prefs.setBoolPref("browser.safebrowsing.forbiddenURIs.enabled", isBlockListEnabled);
        Services.prefs.setBoolPref("browser.safebrowsing.allowOverride", !isBlockListEnabled);

        let isTelemetryEnabled = ParentalControls.isAllowed(ParentalControls.TELEMETRY);
        Services.prefs.setBoolPref("toolkit.telemetry.enabled", isTelemetryEnabled);

        let isHealthReportEnabled = ParentalControls.isAllowed(ParentalControls.HEALTH_REPORT);
        SharedPreferences.forApp().setBoolPref("android.not_a_preference.healthreport.uploadEnabled", isHealthReportEnabled);
    }

    let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
    if (sysInfo.get("version") < 16) {
      let defaults = Services.prefs.getDefaultBranch(null);
      defaults.setBoolPref("media.autoplay.enabled", false);
    }

    InitLater(() => {
      // The order that context menu items are added is important
      // Make sure the "Open in App" context menu item appears at the bottom of the list
      this.initContextMenu();
      ExternalApps.init();
    }, NativeWindow, "contextmenus");

    if (AppConstants.ACCESSIBILITY) {
      InitLater(() => AccessFu.attach(window), window, "AccessFu");
    }

    // Don't delay loading content.js because when we restore reader mode tabs,
    // we require the reader mode scripts in content.js right away.
    let mm = window.getGroupMessageManager("browsers");
    mm.loadFrameScript("chrome://browser/content/content.js", true);

    // We can't delay registering WebChannel listeners: if the first page is
    // about:accounts, which can happen when starting the Firefox Account flow
    // from the first run experience, or via the Firefox Account Status
    // Activity, we can and do miss messages from the fxa-content-server.
    // However, we never allow suitably restricted profiles from listening to
    // fxa-content-server messages.
    if (ParentalControls.isAllowed(ParentalControls.MODIFY_ACCOUNTS)) {
      console.log("browser.js: loading Firefox Accounts WebChannel");
      Cu.import("resource://gre/modules/FxAccountsWebChannel.jsm");
      EnsureFxAccountsWebChannel();
    } else {
      console.log("browser.js: not loading Firefox Accounts WebChannel; this profile cannot connect to Firefox Accounts.");
    }

    // Notify Java that Gecko has loaded.
    Messaging.sendRequest({ type: "Gecko:Ready" });

    this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
      BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);

      InitLater(() => Cu.import("resource://gre/modules/NotificationDB.jsm"));
      InitLater(() => Cu.import("resource://gre/modules/PresentationDeviceInfoManager.jsm"));

      InitLater(() => Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""));
      InitLater(() => Messaging.sendRequest({ type: "Gecko:DelayedStartup" }));

      if (!AppConstants.RELEASE_OR_BETA) {
        InitLater(() => WebcompatReporter.init());
      }

      // Collect telemetry data.
      // We do this at startup because we want to move away from "gather-telemetry" (bug 1127907)
      InitLater(() => {
        Telemetry.addData("FENNEC_TRACKING_PROTECTION_STATE", parseInt(BrowserApp.getTrackingProtectionState()));
        Telemetry.addData("ZOOMED_VIEW_ENABLED", Services.prefs.getBoolPref("ui.zoomedview.enabled"));
      });

      InitLater(() => LightWeightThemeWebInstaller.init());
      InitLater(() => SpatialNavigation.init(BrowserApp.deck, null), window, "SpatialNavigation");
      InitLater(() => CastingApps.init(), window, "CastingApps");
      InitLater(() => Services.search.init(), Services, "search");
      InitLater(() => DownloadNotifications.init(), window, "DownloadNotifications");

#ifdef MOZ_SAFE_BROWSING
      // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
      InitLater(() => SafeBrowsing.init(), window, "SafeBrowsing");
#endif

      InitLater(() => Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager));
      InitLater(() => LoginManagerParent.init(), window, "LoginManagerParent");

    }, false);

    // Pass caret StateChanged events to ActionBarHandler.
    window.addEventListener("mozcaretstatechanged", e => {
      ActionBarHandler.caretStateChangedHandler(e);
    }, /* useCapture = */ true, /* wantsUntrusted = */ false);
  },

  get _startupStatus() {
    delete this._startupStatus;

    let savedMilestone = null;
    try {
      savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
    } catch (e) {
    }
    let ourMilestone = AppConstants.MOZ_APP_VERSION;
    this._startupStatus = "";
    if (ourMilestone != savedMilestone) {
      Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone);
      this._startupStatus = savedMilestone ? "upgrade" : "new";
    }

    return this._startupStatus;
  },

  /**
   * Pass this a locale string, such as "fr" or "es_ES".
   */
  setLocale: function (locale) {
    console.log("browser.js: requesting locale set: " + locale);
    Messaging.sendRequest({ type: "Locale:Set", locale: locale });
  },

  initContextMenu: function () {
    // We pass a thunk in place of a raw label string. This allows the
    // context menu to automatically accommodate locale changes without
    // having to be rebuilt.
    let stringGetter = name => () => Strings.browser.GetStringFromName(name);

    // TODO: These should eventually move into more appropriate classes
    NativeWindow.contextmenus.add(stringGetter("contextmenu.openInNewTab"),
      NativeWindow.contextmenus.linkOpenableNonPrivateContext,
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
        UITelemetry.addEvent("loadurl.1", "contextmenu", null);

        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
        ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
        let tab = BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });

        let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened");
        let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
        let buttonLabel = Strings.browser.GetStringFromName("newtabpopup.switch");

        Snackbars.show(label, Snackbars.LENGTH_LONG, {
          action: {
            label: buttonLabel,
            callback: () => { BrowserApp.selectTab(tab); },
          }
        });
      });

    let showOpenInPrivateTab = true;
    if ("@mozilla.org/parental-controls-service;1" in Cc) {
      let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService);
      showOpenInPrivateTab = pc.isAllowed(Ci.nsIParentalControlsService.PRIVATE_BROWSING);
    }

    if (showOpenInPrivateTab) {
      NativeWindow.contextmenus.add(stringGetter("contextmenu.openInPrivateTab"),
        NativeWindow.contextmenus.linkOpenableContext,
        function (aTarget) {
          UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
          UITelemetry.addEvent("loadurl.1", "contextmenu", null);

          let url = NativeWindow.contextmenus._getLinkURL(aTarget);
          ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
          let tab = BrowserApp.addTab(url, {selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true});

          let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened");
          let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
          let buttonLabel = Strings.browser.GetStringFromName("newtabpopup.switch");
          Snackbars.show(label, Snackbars.LENGTH_LONG, {
            action: {
              label: buttonLabel,
              callback: () => { BrowserApp.selectTab(tab); },
            }
          });
        });
    }

    NativeWindow.contextmenus.add(stringGetter("contextmenu.copyLink"),
      NativeWindow.contextmenus.linkCopyableContext,
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_link");

        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
        NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.copyEmailAddress"),
      NativeWindow.contextmenus.emailLinkContext,
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_email");

        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
        let emailAddr = NativeWindow.contextmenus._stripScheme(url);
        NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr);
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.copyPhoneNumber"),
      NativeWindow.contextmenus.phoneNumberLinkContext,
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_phone");

        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
        let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
        NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber);
      });

    NativeWindow.contextmenus.add({
      label: stringGetter("contextmenu.shareLink"),
      order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
      selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.linkShareableContext),
      showAsActions: function(aElement) {
        return {
          title: aElement.textContent.trim() || aElement.title.trim(),
          uri: NativeWindow.contextmenus._getLinkURL(aElement),
        };
      },
      icon: "drawable://ic_menu_share",
      callback: function(aTarget) {
        // share.1 telemetry is handled in Java via PromptList
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link");
      }
    });

    NativeWindow.contextmenus.add({
      label: stringGetter("contextmenu.shareEmailAddress"),
      order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
      selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.emailLinkContext),
      showAsActions: function(aElement) {
        let url = NativeWindow.contextmenus._getLinkURL(aElement);
        let emailAddr = NativeWindow.contextmenus._stripScheme(url);
        let title = aElement.textContent || aElement.title;
        return {
          title: title,
          uri: emailAddr,
        };
      },
      icon: "drawable://ic_menu_share",
      callback: function(aTarget) {
        // share.1 telemetry is handled in Java via PromptList
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email");
      }
    });

    NativeWindow.contextmenus.add({
      label: stringGetter("contextmenu.sharePhoneNumber"),
      order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
      selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.phoneNumberLinkContext),
      showAsActions: function(aElement) {
        let url = NativeWindow.contextmenus._getLinkURL(aElement);
        let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
        let title = aElement.textContent || aElement.title;
        return {
          title: title,
          uri: phoneNumber,
        };
      },
      icon: "drawable://ic_menu_share",
      callback: function(aTarget) {
        // share.1 telemetry is handled in Java via PromptList
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone");
      }
    });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.addToContacts"),
      NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.emailLinkContext),
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email");

        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
        Messaging.sendRequest({
          type: "Contact:Add",
          email: url
        });
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.addToContacts"),
      NativeWindow.contextmenus._disableRestricted("ADD_CONTACT", NativeWindow.contextmenus.phoneNumberLinkContext),
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone");

        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
        Messaging.sendRequest({
          type: "Contact:Add",
          phone: url
        });
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.bookmarkLink"),
      NativeWindow.contextmenus._disableRestricted("BOOKMARK", NativeWindow.contextmenus.linkBookmarkableContext),
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark");
        UITelemetry.addEvent("save.1", "contextmenu", null, "bookmark");

        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
        let title = aTarget.textContent || aTarget.title || url;
        Messaging.sendRequest({
          type: "Bookmark:Insert",
          url: url,
          title: title
        });
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.playMedia"),
      NativeWindow.contextmenus.mediaContext("media-paused"),
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_play");
        aTarget.play();
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.pauseMedia"),
      NativeWindow.contextmenus.mediaContext("media-playing"),
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_pause");
        aTarget.pause();
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.showControls2"),
      NativeWindow.contextmenus.mediaContext("media-hidingcontrols"),
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media");
        aTarget.setAttribute("controls", true);
      });

    NativeWindow.contextmenus.add({
      label: stringGetter("contextmenu.shareMedia"),
      order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
      selector: NativeWindow.contextmenus._disableRestricted(
        "SHARE", NativeWindow.contextmenus.videoContext()),
      showAsActions: function(aElement) {
        let url = (aElement.currentSrc || aElement.src);
        let title = aElement.textContent || aElement.title;
        return {
          title: title,
          uri: url,
          type: "video/*",
        };
      },
      icon: "drawable://ic_menu_share",
      callback: function(aTarget) {
        // share.1 telemetry is handled in Java via PromptList
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media");
      }
    });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.fullScreen"),
      NativeWindow.contextmenus.videoContext("not-fullscreen"),
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen");
        aTarget.requestFullscreen();
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.mute"),
      NativeWindow.contextmenus.mediaContext("media-unmuted"),
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute");
        aTarget.muted = true;
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.unmute"),
      NativeWindow.contextmenus.mediaContext("media-muted"),
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute");
        aTarget.muted = false;
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.viewImage"),
      NativeWindow.contextmenus.imageLocationCopyableContext,
      function(aTarget) {
        let url = aTarget.src;
        ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal,
                                          Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);

        UITelemetry.addEvent("action.1", "contextmenu", null, "web_view_image");
        UITelemetry.addEvent("loadurl.1", "contextmenu", null);
        BrowserApp.selectedBrowser.loadURI(url);
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.copyImageLocation"),
      NativeWindow.contextmenus.imageLocationCopyableContext,
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image");

        let url = aTarget.src;
        NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
      });

    NativeWindow.contextmenus.add({
      label: stringGetter("contextmenu.shareImage"),
      selector: NativeWindow.contextmenus._disableRestricted("SHARE", NativeWindow.contextmenus.imageShareableContext),
      order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
      showAsActions: function(aTarget) {
        let doc = aTarget.ownerDocument;
        let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
                                                         .getImgCacheForDocument(doc);
        let props = imageCache.findEntryProperties(aTarget.currentURI, doc);
        let src = aTarget.src;
        return {
          title: src,
          uri: src,
          type: "image/*",
        };
      },
      icon: "drawable://ic_menu_share",
      menu: true,
      callback: function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image");
      }
    });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.saveImage"),
      NativeWindow.contextmenus.imageSaveableContext,
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image");
        UITelemetry.addEvent("save.1", "contextmenu", null, "image");

        RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE).then(function(permissionGranted) {
            if (!permissionGranted) {
                return;
            }

            ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
                                          false, true, aTarget.ownerDocument.documentURIObject,
                                          aTarget.ownerDocument);
        });
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.setImageAs"),
      NativeWindow.contextmenus._disableRestricted("SET_IMAGE", NativeWindow.contextmenus.imageSaveableContext),
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image");

        let src = aTarget.src;
        Messaging.sendRequest({
          type: "Image:SetAs",
          url: src
        });
      });

    NativeWindow.contextmenus.add(
      function(aTarget) {
        if (aTarget instanceof HTMLVideoElement) {
          // If a video element is zero width or height, its essentially
          // an HTMLAudioElement.
          if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 )
            return Strings.browser.GetStringFromName("contextmenu.saveAudio");
          return Strings.browser.GetStringFromName("contextmenu.saveVideo");
        } else if (aTarget instanceof HTMLAudioElement) {
          return Strings.browser.GetStringFromName("contextmenu.saveAudio");
        }
        return Strings.browser.GetStringFromName("contextmenu.saveVideo");
      }, NativeWindow.contextmenus.mediaSaveableContext,
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media");
        UITelemetry.addEvent("save.1", "contextmenu", null, "media");

        let url = aTarget.currentSrc || aTarget.src;
        let filePickerTitleKey = (aTarget instanceof HTMLVideoElement &&
                                  (aTarget.videoWidth != 0 && aTarget.videoHeight != 0))
                                  ? "SaveVideoTitle" : "SaveAudioTitle";
        // Skipped trying to pull MIME type out of cache for now
        ContentAreaUtils.internalSave(url, null, null, null, null, false,
                                      filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
                                      aTarget.ownerDocument, true, null);
      });

    NativeWindow.contextmenus.add(stringGetter("contextmenu.showImage"),
      NativeWindow.contextmenus.imageBlockingPolicyContext,
      function(aTarget) {
        UITelemetry.addEvent("action.1", "contextmenu", null, "web_show_image");
        aTarget.setAttribute("data-ctv-show", "true");
        aTarget.setAttribute("src", aTarget.getAttribute("data-ctv-src"));

        // Shows a snackbar to unblock all images if browser.image_blocking.enabled is enabled.
        let blockedImgs = aTarget.ownerDocument.querySelectorAll("[data-ctv-src]");
        if (blockedImgs.length == 0) {
          return;
        }
        let message = Strings.browser.GetStringFromName("imageblocking.downloadedImage");
        Snackbars.show(message, Snackbars.LENGTH_LONG, {
          action: {
            label: Strings.browser.GetStringFromName("imageblocking.showAllImages"),
            callback: () => {
              UITelemetry.addEvent("action.1", "toast", null, "web_show_all_image");
              for (let i = 0; i < blockedImgs.length; ++i) {
                blockedImgs[i].setAttribute("data-ctv-show", "true");
                blockedImgs[i].setAttribute("src", blockedImgs[i].getAttribute("data-ctv-src"));
              }
            },
          }
        });
    });
  },

  onAppUpdated: function() {
    // initialize the form history and passwords databases on upgrades
    Services.obs.notifyObservers(null, "FormHistory:Init", "");
    Services.obs.notifyObservers(null, "Passwords:Init", "");

    if (this._startupStatus === "upgrade") {
      this._migrateUI();
    }
  },

  _migrateUI: function() {
    const UI_VERSION = 3;
    let currentUIVersion = 0;
    try {
      currentUIVersion = Services.prefs.getIntPref("browser.migration.version");
    } catch(ex) {}
    if (currentUIVersion >= UI_VERSION) {
      return;
    }

    if (currentUIVersion < 1) {
      // Migrate user-set "plugins.click_to_play" pref. See bug 884694.
      // Because the default value is true, a user-set pref means that the pref was set to false.
      if (Services.prefs.prefHasUserValue("plugins.click_to_play")) {
        Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
        Services.prefs.clearUserPref("plugins.click_to_play");
      }

      // Migrate the "privacy.donottrackheader.value" pref. See bug 1042135.
      if (Services.prefs.prefHasUserValue("privacy.donottrackheader.value")) {
        // Make sure the doNotTrack value conforms to the conversion from
        // three-state to two-state. (This reverts a setting of "please track me"
        // to the default "don't say anything").
        if (Services.prefs.getBoolPref("privacy.donottrackheader.enabled") &&
            (Services.prefs.getIntPref("privacy.donottrackheader.value") != 1)) {
          Services.prefs.clearUserPref("privacy.donottrackheader.enabled");
        }

        // This pref has been removed, so always clear it.
        Services.prefs.clearUserPref("privacy.donottrackheader.value");
      }

      // Set the search activity default pref on app upgrade if it has not been set already.
      if (!Services.prefs.prefHasUserValue("searchActivity.default.migrated")) {
        Services.prefs.setBoolPref("searchActivity.default.migrated", true);
        SearchEngines.migrateSearchActivityDefaultPref();
      }

      Reader.migrateCache().catch(e => Cu.reportError("Error migrating Reader cache: " + e));

      // We removed this pref from user visible settings, so we should reset it.
      // Power users can go into about:config to re-enable this if they choose.
      if (Services.prefs.prefHasUserValue("nglayout.debug.paint_flashing")) {
        Services.prefs.clearUserPref("nglayout.debug.paint_flashing");
      }
    }

    if (currentUIVersion < 2) {
      let name;
      if (Services.prefs.prefHasUserValue("browser.search.defaultenginename")) {
        name = Services.prefs.getCharPref("browser.search.defaultenginename");
      }
      if (!name && Services.prefs.prefHasUserValue("browser.search.defaultenginename.US")) {
        name = Services.prefs.getCharPref("browser.search.defaultenginename.US");
      }
      if (name) {
        Services.search.init(() => {
          let engine = Services.search.getEngineByName(name);
          if (engine) {
            Services.search.defaultEngine = engine;
            Services.obs.notifyObservers(null, "default-search-engine-migrated", "");
          }
        });
      }
    }

    if (currentUIVersion < 3) {
      const kOldSafeBrowsingPref = "browser.safebrowsing.enabled";
      // Default value is set to true, a user pref means that the pref was
      // set to false.
      if (Services.prefs.prefHasUserValue(kOldSafeBrowsingPref) &&
          !Services.prefs.getBoolPref(kOldSafeBrowsingPref)) {
        Services.prefs.setBoolPref("browser.safebrowsing.phishing.enabled",
                                   false);
        // Should just remove support for the pref entirely, even if it's
        // only in about:config
        Services.prefs.clearUserPref(kOldSafeBrowsingPref);
      }
    }

    // Update the migration version.
    Services.prefs.setIntPref("browser.migration.version", UI_VERSION);
  },

  // This function returns false during periods where the browser displayed document is
  // different from the browser content document, so user actions and some kinds of viewport
  // updates should be ignored. This period starts when we start loading a new page or
  // switch tabs, and ends when the new browser content document has been drawn and handed
  // off to the compositor.
  isBrowserContentDocumentDisplayed: function() {
    try {
      if (!Services.androidBridge.isContentDocumentDisplayed(window))
        return false;
    } catch (e) {
      return false;
    }

    let tab = this.selectedTab;
    if (!tab)
      return false;
    return tab.contentDocumentIsDisplayed;
  },

  contentDocumentChanged: function() {
    window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true;
    Services.androidBridge.contentDocumentChanged(window);
  },

  get tabs() {
    return this._tabs;
  },

  set selectedTab(aTab) {
    if (this._selectedTab == aTab)
      return;

    if (this._selectedTab) {
      this._selectedTab.setActive(false);
    }

    this._selectedTab = aTab;
    if (!aTab)
      return;

    aTab.setActive(true);
    this.contentDocumentChanged();
    this.deck.selectedPanel = aTab.browser;
    // Focus the browser so that things like selection will be styled correctly.
    aTab.browser.focus();
  },

  get selectedBrowser() {
    if (this._selectedTab)
      return this._selectedTab.browser;
    return null;
  },

  getTabForId: function getTabForId(aId) {
    let tabs = this._tabs;
    for (let i=0; i < tabs.length; i++) {
       if (tabs[i].id == aId)
         return tabs[i];
    }
    return null;
  },

  getTabForBrowser: function getTabForBrowser(aBrowser) {
    let tabs = this._tabs;
    for (let i = 0; i < tabs.length; i++) {
      if (tabs[i].browser == aBrowser)
        return tabs[i];
    }
    return null;
  },

  getTabForWindow: function getTabForWindow(aWindow) {
    let tabs = this._tabs;
    for (let i = 0; i < tabs.length; i++) {
      if (tabs[i].browser.contentWindow == aWindow)
        return tabs[i];
    }
    return null;
  },

  getBrowserForWindow: function getBrowserForWindow(aWindow) {
    let tabs = this._tabs;
    for (let i = 0; i < tabs.length; i++) {
      if (tabs[i].browser.contentWindow == aWindow)
        return tabs[i].browser;
    }
    return null;
  },

  getBrowserForDocument: function getBrowserForDocument(aDocument) {
    let tabs = this._tabs;
    for (let i = 0; i < tabs.length; i++) {
      if (tabs[i].browser.contentDocument == aDocument)
        return tabs[i].browser;
    }
    return null;
  },

  loadURI: function loadURI(aURI, aBrowser, aParams) {
    aBrowser = aBrowser || this.selectedBrowser;
    if (!aBrowser)
      return;

    aParams = aParams || {};

    let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
    let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null;
    let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
    let charset = "charset" in aParams ? aParams.charset : null;

    let tab = this.getTabForBrowser(aBrowser);
    if (tab) {
      if ("userRequested" in aParams) tab.userRequested = aParams.userRequested;
      tab.isSearch = ("isSearch" in aParams) ? aParams.isSearch : false;
    }

    try {
      aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
    } catch(e) {
      if (tab) {
        let message = {
          type: "Content:LoadError",
          tabID: tab.id
        };
        Messaging.sendRequest(message);
        dump("Handled load error: " + e)
      }
    }
  },

  addTab: function addTab(aURI, aParams) {
    aParams = aParams || {};

    let newTab = new Tab(aURI, aParams);

    if (typeof aParams.tabIndex == "number") {
      this._tabs.splice(aParams.tabIndex, 0, newTab);
    } else {
      this._tabs.push(newTab);
    }

    let selected = "selected" in aParams ? aParams.selected : true;
    if (selected)
      this.selectedTab = newTab;

    let pinned = "pinned" in aParams ? aParams.pinned : false;
    if (pinned) {
      let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
      ss.setTabValue(newTab, "appOrigin", aURI);
    }

    let evt = document.createEvent("UIEvents");
    evt.initUIEvent("TabOpen", true, false, window, null);
    newTab.browser.dispatchEvent(evt);

    return newTab;
  },

  // Use this method to close a tab from JS. This method sends a message
  // to Java to close the tab in the Java UI (we'll get a Tab:Closed message
  // back from Java when that happens).
  closeTab: function closeTab(aTab) {
    if (!aTab) {
      Cu.reportError("Error trying to close tab (tab doesn't exist)");
      return;
    }

    let message = {
      type: "Tab:Close",
      tabID: aTab.id
    };
    Messaging.sendRequest(message);
  },

  // Calling this will update the state in BrowserApp after a tab has been
  // closed in the Java UI.
  _handleTabClosed: function _handleTabClosed(aTab, aShowUndoSnackbar) {
    if (aTab == this.selectedTab)
      this.selectedTab = null;

    let tabIndex = this._tabs.indexOf(aTab);

    let evt = document.createEvent("UIEvents");
    evt.initUIEvent("TabClose", true, false, window, tabIndex);
    aTab.browser.dispatchEvent(evt);

    if (aShowUndoSnackbar) {
      // Get a title for the undo close snackbar. Fall back to the URL if there is no title.
      let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
      let closedTabData = ss.getClosedTabs(window)[0];

      let message;
      let title = closedTabData.entries[closedTabData.index - 1].title;
      let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(aTab.browser);

      if (isPrivate) {
        message = Strings.browser.GetStringFromName("privateClosedMessage.message");
      } else if (title) {
        message = Strings.browser.formatStringFromName("undoCloseToast.message", [title], 1);
      } else {
        message = Strings.browser.GetStringFromName("undoCloseToast.messageDefault");
      }

      Snackbars.show(message, Snackbars.LENGTH_LONG, {
        action: {
          label: Strings.browser.GetStringFromName("undoCloseToast.action2"),
          callback: function() {
            UITelemetry.addEvent("undo.1", "toast", null, "closetab");
            ss.undoCloseTab(window, closedTabData);
          }
        }
      });
    }

    aTab.destroy();
    this._tabs.splice(tabIndex, 1);
  },

  // Use this method to select a tab from JS. This method sends a message
  // to Java to select the tab in the Java UI (we'll get a Tab:Selected message
  // back from Java when that happens).
  selectTab: function selectTab(aTab) {
    if (!aTab) {
      Cu.reportError("Error trying to select tab (tab doesn't exist)");
      return;
    }

    // There's nothing to do if the tab is already selected
    if (aTab == this.selectedTab)
      return;

    let doc = this.selectedBrowser.contentDocument;
    if (doc.fullscreenElement) {
      // We'll finish the tab selection once the fullscreen transition has ended,
      // remember the new tab for this.
      this.fullscreenTransitionTab = aTab;
      doc.exitFullscreen();
    }

    let message = {
      type: "Tab:Select",
      tabID: aTab.id
    };
    Messaging.sendRequest(message);
  },

  /**
   * Gets an open tab with the given URL.
   *
   * @param  aURL URL to look for
   * @param  aOptions Options for the search. Currently supports:
   **  @option startsWith a Boolean indicating whether to search for a tab who's url starts with the
   *           requested url. Useful if you want to ignore hash codes on the end of a url. For instance
   *           to have about:downloads match about:downloads#123.
   * @return the tab with the given URL, or null if no such tab exists
   */
  getTabWithURL: function getTabWithURL(aURL, aOptions) {
    aOptions = aOptions || {};
    let uri = Services.io.newURI(aURL, null, null);
    for (let i = 0; i < this._tabs.length; ++i) {
      let tab = this._tabs[i];
      if (aOptions.startsWith) {
        if (tab.browser.currentURI.spec.startsWith(aURL)) {
          return tab;
        }
      } else {
        if (tab.browser.currentURI.equals(uri)) {
          return tab;
        }
      }
    }
    return null;
  },

  /**
   * If a tab with the given URL already exists, that tab is selected.
   * Otherwise, a new tab is opened with the given URL.
   *
   * @param aURL URL to open
   * @param aParam Options used if a tab is created
   * @param aFlags Options for the search. Currently supports:
   **  @option startsWith a Boolean indicating whether to search for a tab who's url starts with the
   *           requested url. Useful if you want to ignore hash codes on the end of a url. For instance
   *           to have about:downloads match about:downloads#123.
   */
  selectOrAddTab: function selectOrAddTab(aURL, aParams, aFlags) {
    let tab = this.getTabWithURL(aURL, aFlags);
    if (tab == null) {
      tab = this.addTab(aURL, aParams);
    } else {
      this.selectTab(tab);
    }

    return tab;
  },

  // This method updates the state in BrowserApp after a tab has been selected
  // in the Java UI.
  _handleTabSelected: function _handleTabSelected(aTab) {
    if (this.fullscreenTransitionTab) {
      // Defer updating to "fullscreenchange" if tab selection happened during
      // a fullscreen transition.
      return;
    }
    this.selectedTab = aTab;

    let evt = document.createEvent("UIEvents");
    evt.initUIEvent("TabSelect", true, false, window, null);
    aTab.browser.dispatchEvent(evt);
  },

  quit: function quit(aClear = { sanitize: {}, dontSaveSession: false }) {
    // Notify all windows that an application quit has been requested.
    let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
    Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);

    // Quit aborted.
    if (cancelQuit.data) {
      return;
    }

    Services.obs.notifyObservers(null, "quit-application-proceeding", null);

    // Tell session store to forget about this window
    if (aClear.dontSaveSession) {
      let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
      ss.removeWindow(window);
    }

    BrowserApp.sanitize(aClear.sanitize, function() {
      let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
      appStartup.quit(Ci.nsIAppStartup.eForceQuit);
    }, true);
  },

  saveAsPDF: function saveAsPDF(aBrowser) {
    RuntimePermissions.waitForPermissions(RuntimePermissions.WRITE_EXTERNAL_STORAGE).then(function(permissionGranted) {
      if (!permissionGranted) {
        return;
      }

      Task.spawn(function* () {
        let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
        fileName = fileName.trim() + ".pdf";

        let downloadsDir = yield Downloads.getPreferredDownloadsDirectory();
        let file = OS.Path.join(downloadsDir, fileName);

        // Force this to have a unique name.
        let openedFile = yield OS.File.openUnique(file, { humanReadable: true });
        file = openedFile.path;
        yield openedFile.file.close();

        let download = yield Downloads.createDownload({
          source: aBrowser.contentWindow,
          target: file,
          saver: "pdf",
          startTime: Date.now(),
        });

        let list = yield Downloads.getList(download.source.isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC)
        yield list.add(download);
        yield download.start();
      });
    });
  },

  // These values come from pref_tracking_protection_entries in arrays.xml.
  PREF_TRACKING_PROTECTION_ENABLED: "2",
  PREF_TRACKING_PROTECTION_ENABLED_PB: "1",
  PREF_TRACKING_PROTECTION_DISABLED: "0",

  /**
   * Returns the current state of the tracking protection pref.
   * (0 = Disabled, 1 = Enabled in PB, 2 = Enabled)
   */
  getTrackingProtectionState: function() {
    if (Services.prefs.getBoolPref("privacy.trackingprotection.enabled")) {
      return this.PREF_TRACKING_PROTECTION_ENABLED;
    }
    if (Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled")) {
      return this.PREF_TRACKING_PROTECTION_ENABLED_PB;
    }
    return this.PREF_TRACKING_PROTECTION_DISABLED;
  },

  sanitize: function (aItems, callback, aShutdown) {
    let success = true;
    var promises = [];

    for (let key in aItems) {
      if (!aItems[key])
        continue;

      key = key.replace("private.data.", "");

      switch (key) {
        case "cookies_sessions":
          promises.push(Sanitizer.clearItem("cookies"));
          promises.push(Sanitizer.clearItem("sessions"));
          break;
        default:
          promises.push(Sanitizer.clearItem(key));
      }
    }

    Promise.all(promises).then(function() {
      Messaging.sendRequest({
        type: "Sanitize:Finished",
        success: true,
        shutdown: aShutdown === true
      });

      if (callback) {
        callback();
      }
    }).catch(function(err) {
      Messaging.sendRequest({
        type: "Sanitize:Finished",
        error: err,
        success: false,
        shutdown: aShutdown === true
      });

      if (callback) {
        callback();
      }
    })
  },

  getFocusedInput: function(aBrowser, aOnlyInputElements = false) {
    if (!aBrowser)
      return null;

    let doc = aBrowser.contentDocument;
    if (!doc)
      return null;

    let focused = doc.activeElement;
    while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) {
      doc = focused.contentDocument;
      focused = doc.activeElement;
    }

    if (focused instanceof HTMLInputElement &&
        (focused.mozIsTextField(false) || focused.type === "number")) {
      return focused;
    }

    if (aOnlyInputElements)
      return null;

    if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) {

      if (focused instanceof HTMLBodyElement) {
        // we are putting focus into a contentEditable frame. scroll the frame into
        // view instead of the contentEditable document contained within, because that
        // results in a better user experience
        focused = focused.ownerDocument.defaultView.frameElement;
      }
      return focused;
    }
    return null;
  },

  scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
    let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
    if (formHelperMode == kFormHelperModeDisabled)
      return;

    let dwu = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
    if (!dwu) {
      return;
    }

    let apzFlushDone = function() {
      Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed", false);
      dwu.zoomToFocusedInput();
    };

    let paintDone = function() {
      window.removeEventListener("MozAfterPaint", paintDone, false);
      if (dwu.flushApzRepaints()) {
        Services.obs.addObserver(apzFlushDone, "apz-repaints-flushed", false);
      } else {
        apzFlushDone();
      }
    };

    let gotResizeWindow = false;
    let resizeWindow = function(e) {
      gotResizeWindow = true;
      aBrowser.contentWindow.removeEventListener("resize", resizeWindow, false);
      if (dwu.isMozAfterPaintPending) {
        window.addEventListener("MozAfterPaint", paintDone, false);
      } else {
        paintDone();
      }
    }

    aBrowser.contentWindow.addEventListener("resize", resizeWindow, false);

    // The "resize" event sometimes fails to fire, so set a timer to catch that case
    // and unregister the event listener. See Bug 1253469
    setTimeout(function(e) {
    if (!gotResizeWindow) {
        aBrowser.contentWindow.removeEventListener("resize", resizeWindow, false);
        dwu.zoomToFocusedInput();
      }
    }, 500);
  },

  getUALocalePref: function () {
    try {
      return Services.prefs.getComplexValue("general.useragent.locale", Ci.nsIPrefLocalizedString).data;
    } catch (e) {
      try {
        return Services.prefs.getCharPref("general.useragent.locale");
      } catch (ee) {
        return undefined;
      }
    }
  },

  getOSLocalePref: function () {
    try {
      return Services.prefs.getCharPref("intl.locale.os");
    } catch (e) {
      return undefined;
    }
  },

  setLocalizedPref: function (pref, value) {
    let pls = Cc["@mozilla.org/pref-localizedstring;1"]
                .createInstance(Ci.nsIPrefLocalizedString);
    pls.data = value;
    Services.prefs.setComplexValue(pref, Ci.nsIPrefLocalizedString, pls);
  },

  observe: function(aSubject, aTopic, aData) {
    let browser = this.selectedBrowser;

    switch (aTopic) {

      case "Session:Back":
        browser.goBack();
        break;

      case "Session:Forward":
        browser.goForward();
        break;

      case "Session:Navigate":
          let index = JSON.parse(aData);
          let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
          let historySize = webNav.sessionHistory.count;

          if (index < 0) {
            index = 0;
            Log.e("Browser", "Negative index truncated to zero");
          } else if (index >= historySize) {
            Log.e("Browser", "Incorrect index " + index + " truncated to " + historySize - 1);
            index = historySize - 1;
          }

          browser.gotoIndex(index);
          break;

      case "Session:Reload": {
        let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;

        // Check to see if this is a message to enable/disable mixed content blocking.
        if (aData) {
          let data = JSON.parse(aData);

          if (data.bypassCache) {
            flags |= Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
                     Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY;
          }

          if (data.contentType === "tracking") {
            // Convert document URI into the format used by
            // nsChannelClassifier::ShouldEnableTrackingProtection
            // (any scheme turned into https is correct)
            let normalizedUrl = Services.io.newURI("https://" + browser.currentURI.hostPort, null, null);
            if (data.allowContent) {
              // Add the current host in the 'trackingprotection' consumer of
              // the permission manager using a normalized URI. This effectively
              // places this host on the tracking protection white list.
              if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
                PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
              } else {
                Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
                Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
              }
            } else {
              // Remove the current host from the 'trackingprotection' consumer
              // of the permission manager. This effectively removes this host
              // from the tracking protection white list (any list actually).
              if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
                PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
              } else {
                Services.perms.remove(normalizedUrl, "trackingprotection");
                Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
              }
            }
          }
        }

        // Try to use the session history to reload so that framesets are
        // handled properly. If the window has no session history, fall back
        // to using the web navigation's reload method.
        let webNav = browser.webNavigation;
        try {
          let sh = webNav.sessionHistory;
          if (sh)
            webNav = sh.QueryInterface(Ci.nsIWebNavigation);
        } catch (e) {}
        webNav.reload(flags);
        break;
      }

      case "Session:Stop":
        browser.stop();
        break;

      case "Tab:Load": {
        let data = JSON.parse(aData);
        let url = data.url;
        let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP
                  | Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;

        // Pass LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL to prevent any loads from
        // inheriting the currently loaded document's principal.
        if (data.userEntered) {
          flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
        }

        let delayLoad = ("delayLoad" in data) ? data.delayLoad : false;
        let params = {
          selected: ("selected" in data) ? data.selected : !delayLoad,
          parentId: ("parentId" in data) ? data.parentId : -1,
          flags: flags,
          tabID: data.tabID,
          isPrivate: (data.isPrivate === true),
          pinned: (data.pinned === true),
          delayLoad: (delayLoad === true),
          desktopMode: (data.desktopMode === true)
        };

        params.userRequested = url;

        if (data.engine) {
          let engine = Services.search.getEngineByName(data.engine);
          if (engine) {
            let submission = engine.getSubmission(url);
            url = submission.uri.spec;
            params.postData = submission.postData;
            params.isSearch = true;
          }
        }

        if (data.newTab) {
          this.addTab(url, params);
        } else {
          if (data.tabId) {
            // Use a specific browser instead of the selected browser, if it exists
            let specificBrowser = this.getTabForId(data.tabId).browser;
            if (specificBrowser)
              browser = specificBrowser;
          }
          this.loadURI(url, browser, params);
        }
        break;
      }

      case "Tab:Selected":
        this._handleTabSelected(this.getTabForId(parseInt(aData)));
        break;

      case "Tab:Closed": {
        let data = JSON.parse(aData);
        this._handleTabClosed(this.getTabForId(data.tabId), data.showUndoToast);
        break;
      }

      case "keyword-search":
        // This event refers to a search via the URL bar, not a bookmarks
        // keyword search. Note that this code assumes that the user can only
        // perform a keyword search on the selected tab.
        this.selectedTab.isSearch = true;

        // Don't store queries in private browsing mode.
        let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(this.selectedTab.browser);
        let query = isPrivate ? "" : aData;

        let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
        Messaging.sendRequest({
          type: "Search:Keyword",
          identifier: engine.identifier,
          name: engine.name,
          query: query
        });
        break;

      case "Browser:Quit":
        // Add-ons like QuitNow and CleanQuit provide aData as an empty-string ("").
        // Pass undefined to invoke the methods default parms.
        this.quit(aData ? JSON.parse(aData) : undefined);
        break;

      case "SaveAs:PDF":
        this.saveAsPDF(browser);
        break;

      case "ScrollTo:FocusedInput":
        // these messages come from a change in the viewable area and not user interaction
        // we allow scrolling to the selected input, but not zooming the page
        this.scrollToFocusedInput(browser, false);
        break;

      case "Sanitize:ClearData":
        this.sanitize(JSON.parse(aData));
        break;

      case "FullScreen:Exit":
        browser.contentDocument.exitFullscreen();
        break;

      case "Passwords:Init": {
        let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"].
                      getService(Ci.nsILoginManagerStorage);
        storage.initialize();
        Services.obs.removeObserver(this, "Passwords:Init");
        break;
      }

      case "FormHistory:Init": {
        // Force creation/upgrade of formhistory.sqlite
        FormHistory.count({});
        Services.obs.removeObserver(this, "FormHistory:Init");
        break;
      }

      case "android-get-pref": {
        // These pref names are not "real" pref names. They are used in the
        // setting menu, and these are passed when initializing the setting
        // menu. aSubject is a nsIWritableVariant to hold the pref value.
        aSubject.QueryInterface(Ci.nsIWritableVariant);

        switch (aData) {
          // The plugin pref is actually two separate prefs, so
          // we need to handle it differently
          case "plugin.enable":
            aSubject.setAsAString(PluginHelper.getPluginPreference());
            break;

          // Handle master password
          case "privacy.masterpassword.enabled":
            aSubject.setAsBool(MasterPassword.enabled);
            break;

          case "privacy.trackingprotection.state": {
            aSubject.setAsAString(this.getTrackingProtectionState());
            break;
          }

          // Crash reporter submit pref must be fetched from nsICrashReporter
          // service.
          case "datareporting.crashreporter.submitEnabled":
            let crashReporterBuilt = "nsICrashReporter" in Ci &&
                Services.appinfo instanceof Ci.nsICrashReporter;
            if (crashReporterBuilt) {
              aSubject.setAsBool(Services.appinfo.submitReports);
            }
            break;
        }
        break;
      }

      case "android-set-pref": {
        // Pseudo-prefs. aSubject is an nsIWritableVariant that holds the pref
        // value. Set to empty to signal the pref was handled.
        aSubject.QueryInterface(Ci.nsIWritableVariant);
        let value = aSubject.QueryInterface(Ci.nsIVariant);

        switch (aData) {
          // The plugin pref is actually two separate prefs, so we need to
          // handle it differently.
          case "plugin.enable":
            PluginHelper.setPluginPreference(value);
            aSubject.setAsEmpty();
            break;

          // MasterPassword pref is not real, we just need take action and leave
          case "privacy.masterpassword.enabled":
            if (MasterPassword.enabled) {
              MasterPassword.removePassword(value);
            } else {
              MasterPassword.setPassword(value);
            }
            aSubject.setAsEmpty();
            break;

          // "privacy.trackingprotection.state" is not a "real" pref name, but
          // it's used in the setting menu.  By default
          // "privacy.trackingprotection.pbmode.enabled" is true, and
          // "privacy.trackingprotection.enabled" is false.
          case "privacy.trackingprotection.state": {
            switch (value) {
              // Tracking protection disabled.
              case this.PREF_TRACKING_PROTECTION_DISABLED:
                Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", false);
                Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
                break;
              // Tracking protection only in private browsing,
              case this.PREF_TRACKING_PROTECTION_ENABLED_PB:
                Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", true);
                Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
                break;
              // Tracking protection everywhere.
              case this.PREF_TRACKING_PROTECTION_ENABLED:
                Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", true);
                Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
                break;
            }
            aSubject.setAsEmpty();
            break;
          }

          // Crash reporter preference is in a service; set and return.
          case "datareporting.crashreporter.submitEnabled":
            let crashReporterBuilt = "nsICrashReporter" in Ci &&
                Services.appinfo instanceof Ci.nsICrashReporter;
            if (crashReporterBuilt) {
              Services.appinfo.submitReports = value;
              aSubject.setAsEmpty();
            }
            break;
        }
        break;
      }

      case "sessionstore-state-purge-complete":
        Messaging.sendRequest({ type: "Session:StatePurged" });
        break;

      case "gather-telemetry":
        Messaging.sendRequest({ type: "Telemetry:Gather" });
        break;

      case "Locale:OS":
        // We know the system locale. We use this for generating Accept-Language headers.
        console.log("Locale:OS: " + aData);
        let currentOSLocale = this.getOSLocalePref();
        if (currentOSLocale == aData) {
          break;
        }

        console.log("New OS locale.");

        // Ensure that this choice is immediately persisted, because
        // Gecko won't be told again if it forgets.
        Services.prefs.setCharPref("intl.locale.os", aData);
        Services.prefs.savePrefFile(null);

        let appLocale = this.getUALocalePref();

        this.computeAcceptLanguages(aData, appLocale);
        break;

      case "Locale:Changed":
        if (aData) {
          // The value provided to Locale:Changed should be a BCP47 language tag
          // understood by Gecko -- for example, "es-ES" or "de".
          console.log("Locale:Changed: " + aData);

          // We always write a localized pref, even though sometimes the value is a char pref.
          // (E.g., on desktop single-locale builds.)
          this.setLocalizedPref("general.useragent.locale", aData);
        } else {
          // Resetting.
          console.log("Switching to system locale.");
          Services.prefs.clearUserPref("general.useragent.locale");
        }

        Services.prefs.setBoolPref("intl.locale.matchOS", !aData);

        // Ensure that this choice is immediately persisted, because
        // Gecko won't be told again if it forgets.
        Services.prefs.savePrefFile(null);

        // Blow away the string cache so that future lookups get the
        // correct locale.
        Strings.flush();

        // Make sure we use the right Accept-Language header.
        let osLocale;
        try {
          // This should never not be set at this point, but better safe than sorry.
          osLocale = Services.prefs.getCharPref("intl.locale.os");
        } catch (e) {
        }

        this.computeAcceptLanguages(osLocale, aData);
        break;

      case "Fonts:Reload":
        FontEnumerator.updateFontList();
        break;

      case "Vibration:Request":
        if (aSubject instanceof Navigator) {
          let navigator = aSubject;
          let buttons = [
            {
              label: Strings.browser.GetStringFromName("vibrationRequest.denyButton"),
              callback: function() {
                navigator.setVibrationPermission(false);
              }
            },
            {
              label: Strings.browser.GetStringFromName("vibrationRequest.allowButton"),
              callback: function() {
                navigator.setVibrationPermission(true);
              },
              positive: true
            }
          ];
          let message = Strings.browser.GetStringFromName("vibrationRequest.message");
          let options = {};
          NativeWindow.doorhanger.show(message, "vibration-request", buttons,
              BrowserApp.selectedTab.id, options, "VIBRATION");
        }
        break;

      default:
        dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
        break;

    }
  },

  /**
   * Set intl.accept_languages accordingly.
   *
   * After Bug 881510 this will also accept a real Accept-Language choice as
   * input; all Accept-Language logic lives here.
   *
   * osLocale should never be null, but this method is safe regardless.
   * appLocale may explicitly be null.
   */
  computeAcceptLanguages(osLocale, appLocale) {
    let defaultBranch = Services.prefs.getDefaultBranch(null);
    let defaultAccept = defaultBranch.getComplexValue("intl.accept_languages", Ci.nsIPrefLocalizedString).data;
    console.log("Default intl.accept_languages = " + defaultAccept);

    // A guard for potential breakage. Bug 438031.
    // This should not be necessary, because we're reading from the default branch,
    // but better safe than sorry.
    if (defaultAccept && defaultAccept.startsWith("chrome://")) {
      defaultAccept = null;
    } else {
      // Ensure lowercase everywhere so we can compare.
      defaultAccept = defaultAccept.toLowerCase();
    }

    if (appLocale) {
      appLocale = appLocale.toLowerCase();
    }

    if (osLocale) {
      osLocale = osLocale.toLowerCase();
    }

    // Eliminate values if they're present in the default.
    let chosen;
    if (defaultAccept) {
      // intl.accept_languages is a comma-separated list, with no q-value params. Those
      // are added when the header is generated.
      chosen = defaultAccept.split(",")
                            .map(String.trim)
                            .filter((x) => (x != appLocale && x != osLocale));
    } else {
      chosen = [];
    }

    if (osLocale) {
      chosen.unshift(osLocale);
    }

    if (appLocale && appLocale != osLocale) {
      chosen.unshift(appLocale);
    }

    let result = chosen.join(",");
    console.log("Setting intl.accept_languages to " + result);
    this.setLocalizedPref("intl.accept_languages", result);
  },

  // nsIAndroidBrowserApp
  get selectedTab() {
    return this._selectedTab;
  },

  // nsIAndroidBrowserApp
  getBrowserTab: function(tabId) {
    return this.getTabForId(tabId);
  },

  getUITelemetryObserver: function() {
    return UITelemetry;
  },

  // This method will return a list of history items and toIndex based on the action provided from the fromIndex to toIndex,
  // optionally selecting selIndex (if fromIndex <= selIndex <= toIndex)
  getHistory: function(data) {
    let action = data.action;
    let webNav = BrowserApp.getTabForId(data.tabId).window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
    let historyIndex = webNav.sessionHistory.index;
    let historySize = webNav.sessionHistory.count;
    let canGoBack = webNav.canGoBack;
    let canGoForward = webNav.canGoForward;
    let listitems = [];
    let fromIndex = 0;
    let toIndex = historySize - 1;
    let selIndex = historyIndex;

    if (action == "BACK" && canGoBack) {
      fromIndex = Math.max(historyIndex - kMaxHistoryListSize, 0);
      toIndex = historyIndex;
      selIndex = historyIndex;
    } else if (action == "FORWARD" && canGoForward) {
      fromIndex = historyIndex;
      toIndex = Math.min(historySize - 1, historyIndex + kMaxHistoryListSize);
      selIndex = historyIndex;
    } else if (action == "ALL" && (canGoBack || canGoForward)){
      fromIndex = historyIndex - kMaxHistoryListSize / 2;
      toIndex = historyIndex + kMaxHistoryListSize / 2;
      if (fromIndex < 0) {
        toIndex -= fromIndex;
      }

      if (toIndex > historySize - 1) {
        fromIndex -= toIndex - (historySize - 1);
        toIndex = historySize - 1;
      }

      fromIndex = Math.max(fromIndex, 0);
      selIndex = historyIndex;
    } else {
      // return empty list immediately.
      return {
        "historyItems": listitems,
        "toIndex": toIndex
      };
    }

    let browser = this.selectedBrowser;
    let hist = browser.sessionHistory;
    for (let i = toIndex; i >= fromIndex; i--) {
      let entry = hist.getEntryAtIndex(i, false);
      let item = {
        title: entry.title || entry.URI.spec,
        url: entry.URI.spec,
        selected: (i == selIndex)
      };
      listitems.push(item);
    }

    return {
      "historyItems": listitems,
      "toIndex": toIndex
    };
  },
};

var NativeWindow = {
  init: function() {
    Services.obs.addObserver(this, "Menu:Clicked", false);
    Services.obs.addObserver(this, "Doorhanger:Reply", false);
    this.contextmenus.init();
  },

  loadDex: function(zipFile, implClass) {
    Messaging.sendRequest({
      type: "Dex:Load",
      zipfile: zipFile,
      impl: implClass || "Main"
    });
  },

  unloadDex: function(zipFile) {
    Messaging.sendRequest({
      type: "Dex:Unload",
      zipfile: zipFile
    });
  },

  menu: {
    _callbacks: [],
    _menuId: 1,
    toolsMenuID: -1,
    add: function() {
      let options;
      if (arguments.length == 1) {
        options = arguments[0];
      } else if (arguments.length == 3) {
          Log.w("Browser", "This menu addon API has been deprecated. Instead, use the options object API.");
          options = {
            name: arguments[0],
            callback: arguments[2]
          };
      } else {
         throw "Incorrect number of parameters";
      }

      options.type = "Menu:Add";
      options.id = this._menuId;

      Messaging.sendRequest(options);
      this._callbacks[this._menuId] = options.callback;
      this._menuId++;
      return this._menuId - 1;
    },

    remove: function(aId) {
      Messaging.sendRequest({ type: "Menu:Remove", id: aId });
    },

    update: function(aId, aOptions) {
      if (!aOptions)
        return;

      Messaging.sendRequest({
        type: "Menu:Update",
        id: aId,
        options: aOptions
      });
    }
  },

  doorhanger: {
    _callbacks: {},
    _callbacksId: 0,
    _promptId: 0,

  /**
   * @param aOptions
   *        An options JavaScript object holding additional properties for the
   *        notification. The following properties are currently supported:
   *        persistence: An integer. The notification will not automatically
   *                     dismiss for this many page loads. If persistence is set
   *                     to -1, the doorhanger will never automatically dismiss.
   *        persistWhileVisible:
   *                     A boolean. If true, a visible notification will always
   *                     persist across location changes.
   *        timeout:     A time in milliseconds. The notification will not
   *                     automatically dismiss before this time.
   *
   *        checkbox:    A string to appear next to a checkbox under the notification
   *                     message. The button callback functions will be called with
   *                     the checked state as an argument.
   *
   *        actionText:  An object that specifies a clickable string, a type of action,
   *                     and a bundle blob for the consumer to create a click action.
   *                     { text: <text>,
   *                       type: <type>,
   *                       bundle: <blob-object> }
   *
   * @param aCategory
   *        Doorhanger type to display (e.g., LOGIN)
   */
    show: function(aMessage, aValue, aButtons, aTabID, aOptions, aCategory) {
      if (aButtons == null) {
        aButtons = [];
      }

      if (aButtons.length > 2) {
        console.log("Doorhanger can have a maximum of two buttons!");
        aButtons.length = 2;
      }

      aButtons.forEach((function(aButton) {
        this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId };
        aButton.callback = this._callbacksId;
        this._callbacksId++;
      }).bind(this));

      this._promptId++;
      let json = {
        type: "Doorhanger:Add",
        message: aMessage,
        value: aValue,
        buttons: aButtons,
        // use the current tab if none is provided
        tabID: aTabID || BrowserApp.selectedTab.id,
        options: aOptions || {},
        category: aCategory
      };
      Messaging.sendRequest(json);
    },

    hide: function(aValue, aTabID) {
      Messaging.sendRequest({
        type: "Doorhanger:Remove",
        value: aValue,
        tabID: aTabID
      });
    }
  },

  observe: function(aSubject, aTopic, aData) {
    if (aTopic == "Menu:Clicked") {
      if (this.menu._callbacks[aData])
        this.menu._callbacks[aData]();
    } else if (aTopic == "Doorhanger:Reply") {
      let data = JSON.parse(aData);
      let reply_id = data["callback"];

      if (this.doorhanger._callbacks[reply_id]) {
        // Pass the value of the optional checkbox to the callback
        let checked = data["checked"];
        this.doorhanger._callbacks[reply_id].cb(checked, data.inputs);

        let prompt = this.doorhanger._callbacks[reply_id].prompt;
        for (let id in this.doorhanger._callbacks) {
          if (this.doorhanger._callbacks[id].prompt == prompt) {
            delete this.doorhanger._callbacks[id];
          }
        }
      }
    }
  },

  contextmenus: {
    items: {}, //  a list of context menu items that we may show
    DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items

    init: function() {
      // Accessing "NativeWindow.contextmenus" initializes context menus if needed.
      BrowserApp.deck.addEventListener(
          "contextmenu", (e) => NativeWindow.contextmenus.show(e), false);
    },

    add: function() {
      let args;
      if (arguments.length == 1) {
        args = arguments[0];
      } else if (arguments.length == 3) {
        args = {
          label : arguments[0],
          selector: arguments[1],
          callback: arguments[2]
        };
      } else {
        throw "Incorrect number of parameters";
      }

      if (!args.label)
        throw "Menu items must have a name";

      let cmItem = new ContextMenuItem(args);
      this.items[cmItem.id] = cmItem;
      return cmItem.id;
    },

    remove: function(aId) {
      delete this.items[aId];
    },

    // Although we do not use this ourselves anymore, add-ons may still
    // need it as it has been documented, so we shouldn't remove it.
    SelectorContext: function(aSelector) {
      return {
        matches: function(aElt) {
          if (aElt.matches)
            return aElt.matches(aSelector);
          return false;
        }
      };
    },

    linkOpenableNonPrivateContext: {
      matches: function linkOpenableNonPrivateContextMatches(aElement) {
        let doc = aElement.ownerDocument;
        if (!doc || PrivateBrowsingUtils.isContentWindowPrivate(doc.defaultView)) {
          return false;
        }

        return NativeWindow.contextmenus.linkOpenableContext.matches(aElement);
      }
    },

    linkOpenableContext: {
      matches: function linkOpenableContextMatches(aElement) {
        let uri = NativeWindow.contextmenus._getLink(aElement);
        if (uri) {
          let scheme = uri.scheme;
          let dontOpen = /^(javascript|mailto|news|snews|tel)$/;
          return (scheme && !dontOpen.test(scheme));
        }
        return false;
      }
    },

    linkCopyableContext: {
      matches: function linkCopyableContextMatches(aElement) {
        let uri = NativeWindow.contextmenus._getLink(aElement);
        if (uri) {
          let scheme = uri.scheme;
          let dontCopy = /^(mailto|tel)$/;
          return (scheme && !dontCopy.test(scheme));
        }
        return false;
      }
    },

    linkShareableContext: {
      matches: function linkShareableContextMatches(aElement) {
        let uri = NativeWindow.contextmenus._getLink(aElement);
        if (uri) {
          let scheme = uri.scheme;
          let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/;
          return (scheme && !dontShare.test(scheme));
        }
        return false;
      }
    },

    linkBookmarkableContext: {
      matches: function linkBookmarkableContextMatches(aElement) {
        let uri = NativeWindow.contextmenus._getLink(aElement);
        if (uri) {
          let scheme = uri.scheme;
          let dontBookmark = /^(mailto|tel)$/;
          return (scheme && !dontBookmark.test(scheme));
        }
        return false;
      }
    },

    emailLinkContext: {
      matches: function emailLinkContextMatches(aElement) {
        let uri = NativeWindow.contextmenus._getLink(aElement);
        if (uri)
          return uri.schemeIs("mailto");
        return false;
      }
    },

    phoneNumberLinkContext: {
      matches: function phoneNumberLinkContextMatches(aElement) {
        let uri = NativeWindow.contextmenus._getLink(aElement);
        if (uri)
          return uri.schemeIs("tel");
        return false;
      }
    },

    imageLocationCopyableContext: {
      matches: function imageLinkCopyableContextMatches(aElement) {
        if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
          // The image is blocked by Tap-to-load Images
          if (aElement.hasAttribute("data-ctv-src") && !aElement.hasAttribute("data-ctv-show")) {
            return false;
          }
        }
        return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI);
      }
    },

    imageSaveableContext: {
      matches: function imageSaveableContextMatches(aElement) {
        if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
          // The image is blocked by Tap-to-load Images
          if (aElement.hasAttribute("data-ctv-src") && !aElement.hasAttribute("data-ctv-show")) {
            return false;
          }
        }
        if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) {
          // The image must be loaded to allow saving
          let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
          return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE));
        }
        return false;
      }
    },

    imageShareableContext: {
      matches: function imageShareableContextMatches(aElement) {
        let imgSrc = '';
        if (aElement instanceof Ci.nsIDOMHTMLImageElement) {
          imgSrc = aElement.src;
        } else if (aElement instanceof Ci.nsIImageLoadingContent &&
            aElement.currentURI &&
            aElement.currentURI.spec) {
          imgSrc = aElement.currentURI.spec;
        }

        // In order to share an image, we need to pass the image src over IPC via an Intent (in
        // `ApplicationPackageManager.queryIntentActivities`). However, the transaction has a 1MB limit
        // (shared by all transactions in progress) - otherwise we crash! (bug 1243305)
        //   https://developer.android.com/reference/android/os/TransactionTooLargeException.html
        //
        // The transaction limit is 1MB and we arbitrarily choose to cap this transaction at 1/4 of that = 250,000 bytes.
        // In Java, a UTF-8 character is 1-4 bytes so, 250,000 bytes / 4 bytes/char = 62,500 char
        let MAX_IMG_SRC_LEN = 62500;
        let isTooLong = imgSrc.length >= MAX_IMG_SRC_LEN;
        return !isTooLong && this.NativeWindow.contextmenus.imageSaveableContext.matches(aElement);
      }.bind(this)
    },

    mediaSaveableContext: {
      matches: function mediaSaveableContextMatches(aElement) {
        return (aElement instanceof HTMLVideoElement ||
               aElement instanceof HTMLAudioElement);
      }
    },

    imageBlockingPolicyContext: {
      matches: function imageBlockingPolicyContextMatches(aElement) {
        if (aElement instanceof Ci.nsIDOMHTMLImageElement && aElement.getAttribute("data-ctv-src")) {
          // Only show the menuitem if we are blocking the image
          if (aElement.getAttribute("data-ctv-show") == "true") {
            return false;
          }
          return true;
        }
        return false;
      }
    },

    mediaContext: function(aMode) {
      return {
        matches: function(aElt) {
          if (aElt instanceof Ci.nsIDOMHTMLMediaElement) {
            let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE;
            if (hasError)
              return false;

            let paused = aElt.paused || aElt.ended;
            if (paused && aMode == "media-paused")
              return true;
            if (!paused && aMode == "media-playing")
              return true;
            let controls = aElt.controls;
            if (!controls && aMode == "media-hidingcontrols")
              return true;

            let muted = aElt.muted;
            if (muted && aMode == "media-muted")
              return true;
            else if (!muted && aMode == "media-unmuted")
              return true;
          }
          return false;
        }
      };
    },

    videoContext: function(aMode) {
      return {
        matches: function(aElt) {
          if (aElt instanceof HTMLVideoElement) {
            if (!aMode) {
              return true;
            }
            var isFullscreen = aElt.ownerDocument.fullscreenElement == aElt;
            if (aMode == "not-fullscreen") {
              return !isFullscreen;
            }
            if (aMode == "fullscreen") {
              return isFullscreen;
            }
          }
          return false;
        }
      };
    },

    /* Holds a WeakRef to the original target element this context menu was shown for.
     * Most API's will have to walk up the tree from this node to find the correct element
     * to act on
     */
    get _target() {
      if (this._targetRef)
        return this._targetRef.get();
      return null;
    },

    set _target(aTarget) {
      if (aTarget)
        this._targetRef = Cu.getWeakReference(aTarget);
      else this._targetRef = null;
    },

    get defaultContext() {
      delete this.defaultContext;
      return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default");
    },

    /* Gets menuitems for an arbitrary node
     * Parameters:
     *   element - The element to look at. If this element has a contextmenu attribute, the
     *             corresponding contextmenu will be used.
     */
    _getHTMLContextMenuItemsForElement: function(element) {
      let htmlMenu = element.contextMenu;
      if (!htmlMenu) {
        return [];
      }

      htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
      htmlMenu.sendShowEvent();

      return this._getHTMLContextMenuItemsForMenu(htmlMenu, element);
    },

    /* Add a menuitem for an HTML <menu> node
     * Parameters:
     *   menu - The <menu> element to iterate through for menuitems
     *   target - The target element these context menu items are attached to
     */
    _getHTMLContextMenuItemsForMenu: function(menu, target) {
      let items = [];
      for (let i = 0; i < menu.childNodes.length; i++) {
        let elt = menu.childNodes[i];
        if (!elt.label)
          continue;

        items.push(new HTMLContextMenuItem(elt, target));
      }

      return items;
    },

    // Searches the current list of menuitems to show for any that match this id
    _findMenuItem: function(aId) {
      if (!this.menus) {
        return null;
      }

      for (let context in this.menus) {
        let menu = this.menus[context];
        for (let i = 0; i < menu.length; i++) {
          if (menu[i].id === aId) {
            return menu[i];
          }
        }
      }
      return null;
    },

    // Returns true if there are any context menu items to show
    _shouldShow: function() {
      for (let context in this.menus) {
        let menu = this.menus[context];
        if (menu.length > 0) {
          return true;
        }
      }
      return false;
    },

    /* Returns a label to be shown in a tabbed ui if there are multiple "contexts". For instance, if this
     * is an image inside an <a> tag, we may have a "link" context and an "image" one.
     */
    _getContextType: function(element) {
      // For anchor nodes, we try to use the scheme to pick a string
      if (element instanceof Ci.nsIDOMHTMLAnchorElement) {
        let uri = this.makeURI(this._getLinkURL(element));
        try {
          return Strings.browser.GetStringFromName("browser.menu.context." + uri.scheme);
        } catch(ex) { }
      }

      // Otherwise we try the nodeName
      try {
        return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase());
      } catch(ex) { }

      // Fallback to the default
      return this.defaultContext;
    },

    // Adds context menu items added through the add-on api
    _getNativeContextMenuItems: function(element, x, y) {
      let res = [];
      for (let itemId of Object.keys(this.items)) {
        let item = this.items[itemId];

        if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
          res.push(item);
        }
      }

      return res;
    },

    /* Checks if there are context menu items to show, and if it finds them
     * sends a contextmenu event to content. We also send showing events to
     * any html5 context menus we are about to show, and fire some local notifications
     * for chrome consumers to do lazy menuitem construction
     */
    show: function(event) {
      // Android Long-press / contextmenu event provides clientX/Y data. This is not provided
      // by mochitest: test_browserElement_inproc_ContextmenuEvents.html.
      if (!event.clientX || !event.clientY) {
        return;
      }

      // If the event was already defaultPrevented by somebody (web content, or
      // some other part of gecko), then don't do anything with it.
      if (event.defaultPrevented) {
        return;
      }

      // Use the highlighted element for the context menu target. When accessibility is
      // enabled, elements may not be highlighted so use the event target instead.
      this._target = BrowserEventHandler._highlightElement || event.target;
      if (!this._target) {
        return;
      }

      // Try to build a list of contextmenu items. If successful, actually show the
      // native context menu by passing the list to Java.
      this._buildMenu(event.clientX, event.clientY);
      if (this._shouldShow()) {
        BrowserEventHandler._cancelTapHighlight();

        // Consume / preventDefault the event, and show the contextmenu.
        event.preventDefault();
        this._innerShow(this._target, event.clientX, event.clientY);
        this._target = null;

        return;
      }

      // If no context-menu for long-press event, it may be meant to trigger text-selection.
      this.menus = null;
      Services.obs.notifyObservers(
        {target: this._target, x: event.clientX, y: event.clientY}, "context-menu-not-shown", "");
    },

    // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
    _getTitle: function(node) {
      if (node.hasAttribute && node.hasAttribute("title")) {
        return node.getAttribute("title");
      }
      return this._getUrl(node);
    },

    // Returns a url associated with a node
    _getUrl: function(node) {
      if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) ||
          (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) {
        return this._getLinkURL(node);
      } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) {
        // The image is blocked by Tap-to-load Images
        let originalURL = node.getAttribute("data-ctv-src");
        if (originalURL) {
          return originalURL;
        }
        return node.currentURI.spec;
      } else if (node instanceof Ci.nsIDOMHTMLMediaElement) {
        let srcUrl = node.currentSrc || node.src;
        // If URL prepended with blob or mediasource, we'll remove it.
        return srcUrl.replace(/^(?:blob|mediasource):/, '');
      }

      return "";
    },

    // Adds an array of menuitems to the current list of items to show, in the correct context
    _addMenuItems: function(items, context) {
        if (!this.menus[context]) {
          this.menus[context] = [];
        }
        this.menus[context] = this.menus[context].concat(items);
    },

    /* Does the basic work of building a context menu to show. Will combine HTML and Native
     * context menus items, as well as sorting menuitems into different menus based on context.
     */
    _buildMenu: function(x, y) {
      // now walk up the tree and for each node look for any context menu items that apply
      let element = this._target;

      // this.menus holds a hashmap of "contexts" to menuitems associated with that context
      // For instance, if the user taps an image inside a link, we'll have something like:
      // {
      //   link:  [ ContextMenuItem, ContextMenuItem ]
      //   image: [ ContextMenuItem, ContextMenuItem ]
      // }
      this.menus = {};

      while (element) {
        let context = this._getContextType(element);

        // First check for any html5 context menus that might exist...
        var items = this._getHTMLContextMenuItemsForElement(element);
        if (items.length > 0) {
          this._addMenuItems(items, context);
        }

        // then check for any context menu items registered in the ui.
        items = this._getNativeContextMenuItems(element, x, y);
        if (items.length > 0) {
          this._addMenuItems(items, context);
        }

        // walk up the tree and find more items to show
        element = element.parentNode;
      }
    },

    // Walks the DOM tree to find a title from a node
    _findTitle: function(node) {
      let title = "";
      while(node && !title) {
        title = this._getTitle(node);
        node = node.parentNode;
      }
      return title;
    },

    /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm
     * If there is one menu, will return a flat array of menuitems. If there are multiple
     * menus, will return an array with appropriate tabs/items inside it. i.e. :
     * [
     *    { label: "link", items: [...] },
     *    { label: "image", items: [...] }
     * ]
     */
    _reformatList: function(target) {
      let contexts = Object.keys(this.menus);

      if (contexts.length === 1) {
        // If there's only one context, we'll only show a single flat single select list
        return this._reformatMenuItems(target, this.menus[contexts[0]]);
      }

      // If there are multiple contexts, we'll only show a tabbed ui with multiple lists
      return this._reformatListAsTabs(target, this.menus);
    },

    /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm's
     * addTabs method. i.e. :
     * { link: [...], image: [...] } becomes
     * [ { label: "link", items: [...] } ]
     *
     * Also reformats items and resolves any parmaeters that aren't known until display time
     * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
     */
    _reformatListAsTabs: function(target, menus) {
      let itemArray = [];

      // Sort the keys so that "link" is always first
      let contexts = Object.keys(this.menus);
      contexts.sort((context1, context2) => {
        if (context1 === this.defaultContext) {
          return -1;
        } else if (context2 === this.defaultContext) {
          return 1;
        }
        return 0;
      });

      contexts.forEach(context => {
        itemArray.push({
          label: context,
          items: this._reformatMenuItems(target, menus[context])
        });
      });

      return itemArray;
    },

    /* Reformats an array of ContextMenuItems into an array that can be handled by Prompt.jsm. Also reformats items
     * and resolves any parmaeters that aren't known until display time
     * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
     */
    _reformatMenuItems: function(target, menuitems) {
      let itemArray = [];

      for (let i = 0; i < menuitems.length; i++) {
        let t = target;
        while(t) {
          if (menuitems[i].matches(t)) {
            let val = menuitems[i].getValue(t);

            // hidden menu items will return null from getValue
            if (val) {
              itemArray.push(val);
              break;
            }
          }

          t = t.parentNode;
        }
      }

      return itemArray;
    },

    // Called where we're finally ready to actually show the contextmenu. Sorts the items and shows a prompt.
    _innerShow: function(target, x, y) {
      Haptic.performSimpleAction(Haptic.LongPress);

      // spin through the tree looking for a title for this context menu
      let title = this._findTitle(target);

      for (let context in this.menus) {
        let menu = this.menus[context];
        menu.sort((a,b) => {
          if (a.order === b.order) {
            return 0;
          }
          return (a.order > b.order) ? 1 : -1;
        });
      }

      let useTabs = Object.keys(this.menus).length > 1;
      let prompt = new Prompt({
        window: target.ownerDocument.defaultView,
        title: useTabs ? undefined : title
      });

      let items = this._reformatList(target);
      if (useTabs) {
        prompt.addTabs({
          id: "tabs",
          items: items
        });
      } else {
        prompt.setSingleChoiceItems(items);
      }

      prompt.show(this._promptDone.bind(this, target, x, y, items));
    },

    // Called when the contextmenu prompt is closed
    _promptDone: function(target, x, y, items, data) {
      if (data.button == -1) {
        // Prompt was cancelled, or an ActionView was used.
        return;
      }

      let selectedItemId;
      if (data.tabs) {
        let menu = items[data.tabs.tab];
        selectedItemId = menu.items[data.tabs.item].id;
      } else {
        selectedItemId = items[data.list[0]].id
      }

      let selectedItem = this._findMenuItem(selectedItemId);
      this.menus = null;

      if (!selectedItem || !selectedItem.matches || !selectedItem.callback) {
        return;
      }

      // for menuitems added using the native UI, pass the dom element that matched that item to the callback
      while (target) {
        if (selectedItem.matches(target, x, y)) {
          selectedItem.callback(target, x, y);
          break;
        }
        target = target.parentNode;
      }
    },

    // XXX - These are stolen from Util.js, we should remove them if we bring it back
    makeURLAbsolute: function makeURLAbsolute(base, url) {
      // Note:  makeURI() will throw if url is not a valid URI
      return this.makeURI(url, null, this.makeURI(base)).spec;
    },

    makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
      return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
    },

    _getLink: function(aElement) {
      if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
          ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
          (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) ||
          aElement instanceof Ci.nsIDOMHTMLLinkElement ||
          aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) {
        try {
          let url = this._getLinkURL(aElement);
          return Services.io.newURI(url, null, null);
        } catch (e) {}
      }
      return null;
    },

    _disableRestricted: function _disableRestricted(restriction, selector) {
      return {
        matches: function _disableRestrictedMatches(aElement, aX, aY) {
          if (!ParentalControls.isAllowed(ParentalControls[restriction])) {
            return false;
          }

          return selector.matches(aElement, aX, aY);
        }
      };
    },

    _getLinkURL: function ch_getLinkURL(aLink) {
      let href = aLink.href;
      if (href)
        return href;

      href = aLink.getAttribute("href") ||
             aLink.getAttributeNS(kXLinkNamespace, "href");
      if (!href || !href.match(/\S/)) {
        // Without this we try to save as the current doc,
        // for example, HTML case also throws if empty
        throw "Empty href";
      }

      return this.makeURLAbsolute(aLink.baseURI, href);
    },

    _copyStringToDefaultClipboard: function(aString) {
      let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
      clipboard.copyString(aString);
      Snackbars.show(Strings.browser.GetStringFromName("selectionHelper.textCopied"), Snackbars.LENGTH_LONG);
    },

    _stripScheme: function(aString) {
      let index = aString.indexOf(":");
      return aString.slice(index + 1);
    }
  }
};

XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
                                  "resource://gre/modules/PageActions.jsm");

// These alias to the old, deprecated NativeWindow interfaces
[
  ["pageactions", "resource://gre/modules/PageActions.jsm", "PageActions"],
  ["toast", "resource://gre/modules/Snackbars.jsm", "Snackbars"]
].forEach(item => {
  let [name, script, exprt] = item;

  XPCOMUtils.defineLazyGetter(NativeWindow, name, () => {
    var err = Strings.browser.formatStringFromName("nativeWindow.deprecated", ["NativeWindow." + name, script], 2);
    Cu.reportError(err);

    let sandbox = {};
    Cu.import(script, sandbox);
    return sandbox[exprt];
  });
});

var LightWeightThemeWebInstaller = {
  init: function sh_init() {
    let temp = {};
    Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp);
    let theme = new temp.LightweightThemeConsumer(document);
    BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
    BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
    BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);

    if (ParentalControls.parentalControlsEnabled &&
        !this._manager.currentTheme &&
        ParentalControls.isAllowed(ParentalControls.DEFAULT_THEME)) {
      // We are using the DEFAULT_THEME restriction to differentiate between restricted profiles & guest mode - Bug 1199596
      this._installParentalControlsTheme();
    }
  },

  handleEvent: function (event) {
    switch (event.type) {
      case "InstallBrowserTheme":
      case "PreviewBrowserTheme":
      case "ResetBrowserThemePreview":
        // ignore requests from background tabs
        if (event.target.ownerDocument.defaultView.top != content)
          return;
    }

    switch (event.type) {
      case "InstallBrowserTheme":
        this._installRequest(event);
        break;
      case "PreviewBrowserTheme":
        this._preview(event);
        break;
      case "ResetBrowserThemePreview":
        this._resetPreview(event);
        break;
      case "pagehide":
      case "TabSelect":
        this._resetPreview();
        break;
    }
  },

  get _manager () {
    let temp = {};
    Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
    delete this._manager;
    return this._manager = temp.LightweightThemeManager;
  },

  _installParentalControlsTheme: function() {
    let mgr = this._manager;
    let parentalControlsTheme = {
      "headerURL": "resource://android/assets/parental_controls_theme.png",
      "name": "Parental Controls Theme",
      "id": "parental-controls-theme@mozilla.org"
    };

    mgr.addBuiltInTheme(parentalControlsTheme);
    mgr.themeChanged(parentalControlsTheme);
  },

  _installRequest: function (event) {
    let node = event.target;
    let data = this._getThemeFromNode(node);
    if (!data)
      return;

    if (this._isAllowed(node)) {
      this._install(data);
      return;
    }

    let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton");
    let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1);
    let buttons = [{
      label: allowButtonText,
      callback: function () {
        LightWeightThemeWebInstaller._install(data);
      },
      positive: true
    }];

    NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id);
  },

  _install: function (newLWTheme) {
    this._manager.currentTheme = newLWTheme;
  },

  _previewWindow: null,
  _preview: function (event) {
    if (!this._isAllowed(event.target))
      return;
    let data = this._getThemeFromNode(event.target);
    if (!data)
      return;
    this._resetPreview();

    this._previewWindow = event.target.ownerDocument.defaultView;
    this._previewWindow.addEventListener("pagehide", this, true);
    BrowserApp.deck.addEventListener("TabSelect", this, false);
    this._manager.previewTheme(data);
  },

  _resetPreview: function (event) {
    if (!this._previewWindow ||
        event && !this._isAllowed(event.target))
      return;

    this._previewWindow.removeEventListener("pagehide", this, true);
    this._previewWindow = null;
    BrowserApp.deck.removeEventListener("TabSelect", this, false);

    this._manager.resetPreview();
  },

  _isAllowed: function (node) {
    // Make sure the whitelist has been imported to permissions
    PermissionsUtils.importFromPrefs("xpinstall.", "install");

    let pm = Services.perms;

    let uri = node.ownerDocument.documentURIObject;
    if (!uri.schemeIs("https")) {
      return false;
    }

    return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
  },

  _getThemeFromNode: function (node) {
    return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI);
  }
};

var DesktopUserAgent = {
  DESKTOP_UA: null,
  TCO_DOMAIN: "t.co",
  TCO_REPLACE: / Gecko.*/,

  init: function ua_init() {
    Services.obs.addObserver(this, "DesktopMode:Change", false);
    UserAgentOverrides.addComplexOverride(this.onRequest.bind(this));

    // See https://developer.mozilla.org/en/Gecko_user_agent_string_reference
    this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
                        .getService(Ci.nsIHttpProtocolHandler).userAgent
                        .replace(/Android \d.+?; [a-zA-Z]+/, "X11; Linux x86_64")
                        .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101");
  },

  onRequest: function(channel, defaultUA) {
    if (AppConstants.NIGHTLY_BUILD && this.TCO_DOMAIN == channel.URI.host) {
      // Force the referrer
      channel.referrer = channel.URI;

      // Send a bot-like UA to t.co to get a real redirect. We strip off the
      // "Gecko/x.y Firefox/x.y" part
      return defaultUA.replace(this.TCO_REPLACE, "");
    }

    let channelWindow = this._getWindowForRequest(channel);
    let tab = BrowserApp.getTabForWindow(channelWindow);
    if (tab) {
      return this.getUserAgentForTab(tab);
    }

    return null;
  },

  getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) {
    let tab = BrowserApp.getTabForWindow(aWindow.top);
    if (tab) {
      return this.getUserAgentForTab(tab);
    }

    return null;
  },

  getUserAgentForTab: function ua_getUserAgentForTab(aTab) {
    // Send desktop UA if "Request Desktop Site" is enabled.
    if (aTab.desktopMode) {
      return this.DESKTOP_UA;
    }

    return null;
  },

  _getRequestLoadContext: function ua_getRequestLoadContext(aRequest) {
    if (aRequest && aRequest.notificationCallbacks) {
      try {
        return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
      } catch (ex) { }
    }

    if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) {
      try {
        return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
      } catch (ex) { }
    }

    return null;
  },

  _getWindowForRequest: function ua_getWindowForRequest(aRequest) {
    let loadContext = this._getRequestLoadContext(aRequest);
    if (loadContext) {
      try {
        return loadContext.associatedWindow;
      } catch (e) {
        // loadContext.associatedWindow can throw when there's no window
      }
    }
    return null;
  },

  observe: function ua_observe(aSubject, aTopic, aData) {
    if (aTopic === "DesktopMode:Change") {
      let args = JSON.parse(aData);
      let tab = BrowserApp.getTabForId(args.tabId);
      if (tab) {
        tab.reloadWithMode(args.desktopMode);
      }
    }
  }
};


function nsBrowserAccess() {
}

nsBrowserAccess.prototype = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]),

  _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aFlags) {
    let isExternal = !!(aFlags & Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
    if (isExternal && aURI && aURI.schemeIs("chrome"))
      return null;

    let loadflags = isExternal ?
                      Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
                      Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
    if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
      if (isExternal) {
        aWhere = Services.prefs.getIntPref("browser.link.open_external");
      } else {
        aWhere = Services.prefs.getIntPref("browser.link.open_newwindow");
      }
    }

    Services.io.offline = false;

    let referrer;
    if (aOpener) {
      try {
        let location = aOpener.location;
        referrer = Services.io.newURI(location, null, null);
      } catch(e) { }
    }

    let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
    let pinned = false;

    if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
      pinned = true;
      let spec = aURI.spec;
      let tabs = BrowserApp.tabs;
      for (let i = 0; i < tabs.length; i++) {
        let appOrigin = ss.getTabValue(tabs[i], "appOrigin");
        if (appOrigin == spec) {
          let tab = tabs[i];
          BrowserApp.selectTab(tab);
          return tab.browser;
        }
      }
    }

    // If OPEN_SWITCHTAB was not handled above, we need to open a new tab,
    // along with other OPEN_ values that create a new tab.
    let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
                  aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
                  aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB);
    let isPrivate = false;

    if (newTab) {
      let parentId = -1;
      if (!isExternal && aOpener) {
        let parent = BrowserApp.getTabForWindow(aOpener.top);
        if (parent) {
          parentId = parent.id;
          isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
        }
      }

      let openerWindow = (aFlags & Ci.nsIBrowserDOMWindow.OPEN_NO_OPENER) ? null : aOpener;
      // BrowserApp.addTab calls loadURIWithFlags with the appropriate params
      let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags,
                                                                      referrerURI: referrer,
                                                                      external: isExternal,
                                                                      parentId: parentId,
                                                                      opener: openerWindow,
                                                                      selected: true,
                                                                      isPrivate: isPrivate,
                                                                      pinned: pinned });

      return tab.browser;
    }

    // OPEN_CURRENTWINDOW and illegal values
    let browser = BrowserApp.selectedBrowser;
    if (aURI && browser) {
      browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null);
    }

    return browser;
  },

  openURI: function browser_openURI(aURI, aOpener, aWhere, aFlags) {
    let browser = this._getBrowser(aURI, aOpener, aWhere, aFlags);
    return browser ? browser.contentWindow : null;
  },

  openURIInFrame: function browser_openURIInFrame(aURI, aParams, aWhere, aFlags) {
    let browser = this._getBrowser(aURI, null, aWhere, aFlags);
    return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
  },

  isTabContentWindow: function(aWindow) {
    return BrowserApp.getBrowserForWindow(aWindow) != null;
  },

  canClose() {
    return BrowserUtils.canCloseWindow(window);
  },
};


function Tab(aURL, aParams) {
  this.filter = null;
  this.browser = null;
  this.id = 0;
  this.lastTouchedAt = Date.now();
  this._zoom = 1.0;
  this._drawZoom = 1.0;
  this._restoreZoom = false;
  this.userScrollPos = { x: 0, y: 0 };
  this.contentDocumentIsDisplayed = true;
  this.pluginDoorhangerTimeout = null;
  this.shouldShowPluginDoorhanger = true;
  this.clickToPlayPluginsActivated = false;
  this.desktopMode = false;
  this.originalURI = null;
  this.hasTouchListener = false;
  this.playingAudio = false;

  this.create(aURL, aParams);
}

/*
 * Sanity limit for URIs passed to UI code.
 *
 * 2000 is the typical industry limit, largely due to older IE versions.
 *
 * We use 25000, so we'll allow almost any value through.
 *
 * Still, this truncation doesn't affect history, so this is only a practical
 * concern in two ways: the truncated value is used when editing URIs, and as
 * the key for favicon fetches.
 */
const MAX_URI_LENGTH = 25000;

/*
 * Similar restriction for titles. This is only a display concern.
 */
const MAX_TITLE_LENGTH = 255;

/**
 * Ensure that a string is of a sane length.
 */
function truncate(text, max) {
  if (!text || !max) {
    return text;
  }

  if (text.length <= max) {
    return text;
  }

  return text.slice(0, max) + "…";
}

Tab.prototype = {
  create: function(aURL, aParams) {
    if (this.browser)
      return;

    aParams = aParams || {};

    this.browser = document.createElement("browser");
    this.browser.setAttribute("type", "content-targetable");
    this.browser.setAttribute("messagemanagergroup", "browsers");

    if (Preferences.get("browser.tabs.remote.force-enable", false)) {
      this.browser.setAttribute("remote", "true");
    }

    this.browser.permanentKey = {};

    // Check if we have a "parent" window which we need to set as our opener
    if ("opener" in aParams) {
      this.browser.presetOpenerWindow(aParams.opener);
    }

    // Make sure the previously selected panel remains selected. The selected panel of a deck is
    // not stable when panels are added.
    let selectedPanel = BrowserApp.deck.selectedPanel;
    BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null);
    BrowserApp.deck.selectedPanel = selectedPanel;

    let attrs = {};
    if (BrowserApp.manifestUrl) {
      let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
      let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl);
      if (manifest) {
        let app = manifest.QueryInterface(Ci.mozIApplication);
        this.browser.docShell.frameType = Ci.nsIDocShell.FRAME_TYPE_APP;
        attrs['appId'] = app.localId;
      }
    }

    // Must be called after appendChild so the docShell has been created.
    this.setActive(false);

    let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate;
    if (isPrivate) {
      attrs['privateBrowsingId'] = 1;
    }

    this.browser.docShell.setOriginAttributes(attrs);

    // Set the new docShell load flags based on network state.
    if (Tabs.useCache) {
      this.browser.docShell.defaultLoadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
    }

    this.browser.stop();

    // Only set tab uri if uri is valid
    let uri = null;
    let title = aParams.title || aURL;
    try {
      uri = Services.io.newURI(aURL, null, null).spec;
    } catch (e) {}

    // When the tab is stubbed from Java, there's a window between the stub
    // creation and the tab creation in Gecko where the stub could be removed
    // or the selected tab can change (which is easiest to hit during startup).
    // To prevent these races, we need to differentiate between tab stubs from
    // Java and new tabs from Gecko.
    let stub = false;

    if (!aParams.zombifying) {
      if ("tabID" in aParams) {
        this.id = aParams.tabID;
        stub = true;
      } else {
        let jenv = JNI.GetForThread();
        let jTabs = JNI.LoadClass(jenv, "org.mozilla.gecko.Tabs", {
          static_methods: [
            { name: "getNextTabId", sig: "()I" }
          ],
        });
        this.id = jTabs.getNextTabId();
        JNI.UnloadClasses(jenv);
      }

      this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false;

      let message = {
        type: "Tab:Added",
        tabID: this.id,
        uri: truncate(uri, MAX_URI_LENGTH),
        parentId: ("parentId" in aParams) ? aParams.parentId : -1,
        tabIndex: ("tabIndex" in aParams) ? aParams.tabIndex : -1,
        external: ("external" in aParams) ? aParams.external : false,
        selected: ("selected" in aParams || aParams.cancelEditMode === true) ? aParams.selected : true,
        cancelEditMode: aParams.cancelEditMode === true,
        title: truncate(title, MAX_TITLE_LENGTH),
        delayLoad: aParams.delayLoad || false,
        desktopMode: this.desktopMode,
        isPrivate: isPrivate,
        stub: stub
      };
      Messaging.sendRequest(message);
    }

    let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
                Ci.nsIWebProgress.NOTIFY_LOCATION |
                Ci.nsIWebProgress.NOTIFY_SECURITY;
    this.filter = Cc["@mozilla.org/appshell/component/browser-status-filter;1"].createInstance(Ci.nsIWebProgress);
    this.filter.addProgressListener(this, flags)
    this.browser.addProgressListener(this.filter, flags);
    this.browser.sessionHistory.addSHistoryListener(this);

    this.browser.addEventListener("DOMContentLoaded", this, true);
    this.browser.addEventListener("DOMFormHasPassword", this, true);
    this.browser.addEventListener("DOMInputPasswordAdded", this, true);
    this.browser.addEventListener("DOMLinkAdded", this, true);
    this.browser.addEventListener("DOMLinkChanged", this, true);
    this.browser.addEventListener("DOMMetaAdded", this, false);
    this.browser.addEventListener("DOMTitleChanged", this, true);
    this.browser.addEventListener("DOMAudioPlaybackStarted", this, true);
    this.browser.addEventListener("DOMAudioPlaybackStopped", this, true);
    this.browser.addEventListener("DOMWindowClose", this, true);
    this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
    this.browser.addEventListener("DOMAutoComplete", this, true);
    this.browser.addEventListener("blur", this, true);
    this.browser.addEventListener("pageshow", this, true);
    this.browser.addEventListener("MozApplicationManifest", this, true);
    this.browser.addEventListener("TabPreZombify", this, true);

    // Note that the XBL binding is untrusted
    this.browser.addEventListener("PluginBindingAttached", this, true, true);
    this.browser.addEventListener("VideoBindingAttached", this, true, true);
    this.browser.addEventListener("VideoBindingCast", this, true, true);

    Services.obs.addObserver(this, "before-first-paint", false);
    Services.obs.addObserver(this, "media-playback", false);
    Services.obs.addObserver(this, "media-playback-resumed", false);

    // Always intialise new tabs with basic session store data to avoid
    // problems with functions that always expect it to be present
    this.browser.__SS_data = {
      entries: [{
        url: aURL,
        title: truncate(title, MAX_TITLE_LENGTH)
      }],
      index: 1,
      desktopMode: this.desktopMode,
      isPrivate: isPrivate
    };

    if (aParams.delayLoad) {
      // If this is a zombie tab, mark the browser for delay loading, which will
      // restore the tab when selected using the session data added above
      this.browser.__SS_restore = true;
    } else {
      let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
      let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
      let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
      let charset = "charset" in aParams ? aParams.charset : null;

      // The search term the user entered to load the current URL
      this.userRequested = "userRequested" in aParams ? aParams.userRequested : "";
      this.isSearch = "isSearch" in aParams ? aParams.isSearch : false;

      try {
        this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData);
      } catch(e) {
        let message = {
          type: "Content:LoadError",
          tabID: this.id
        };
        Messaging.sendRequest(message);
        dump("Handled load error: " + e);
      }
    }
  },

  /**
   * Reloads the tab with the desktop mode setting.
   */
  reloadWithMode: function (aDesktopMode) {
    // notify desktopmode for PIDOMWindow
    let win = this.browser.contentWindow;
    let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
    dwi.setDesktopModeViewport(aDesktopMode);

    // Set desktop mode for tab and send change to Java
    if (this.desktopMode != aDesktopMode) {
      this.desktopMode = aDesktopMode;
      Messaging.sendRequest({
        type: "DesktopMode:Changed",
        desktopMode: aDesktopMode,
        tabID: this.id
      });
    }

    // Only reload the page for http/https schemes
    let currentURI = this.browser.currentURI;
    if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https"))
      return;

    let url = currentURI.spec;
    // We need LOAD_FLAGS_BYPASS_CACHE here since we're changing the User-Agent
    // string, and servers typically don't use the Vary: User-Agent header, so
    // not doing this means that we'd get some of the previously cached content.
    let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
                Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
    if (this.originalURI && !this.originalURI.equals(currentURI)) {
      // We were redirected; reload the original URL
      url = this.originalURI.spec;
    }

    this.browser.docShell.loadURI(url, flags, null, null, null);
  },

  destroy: function() {
    if (!this.browser)
      return;

    this.browser.removeProgressListener(this.filter);
    this.filter.removeProgressListener(this);
    this.filter = null;
    this.browser.sessionHistory.removeSHistoryListener(this);

    this.browser.removeEventListener("DOMContentLoaded", this, true);
    this.browser.removeEventListener("DOMFormHasPassword", this, true);
    this.browser.removeEventListener("DOMInputPasswordAdded", this, true);
    this.browser.removeEventListener("DOMLinkAdded", this, true);
    this.browser.removeEventListener("DOMLinkChanged", this, true);
    this.browser.removeEventListener("DOMMetaAdded", this, false);
    this.browser.removeEventListener("DOMTitleChanged", this, true);
    this.browser.removeEventListener("DOMAudioPlaybackStarted", this, true);
    this.browser.removeEventListener("DOMAudioPlaybackStopped", this, true);
    this.browser.removeEventListener("DOMWindowClose", this, true);
    this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
    this.browser.removeEventListener("DOMAutoComplete", this, true);
    this.browser.removeEventListener("blur", this, true);
    this.browser.removeEventListener("pageshow", this, true);
    this.browser.removeEventListener("MozApplicationManifest", this, true);
    this.browser.removeEventListener("TabPreZombify", this, true);

    this.browser.removeEventListener("PluginBindingAttached", this, true, true);
    this.browser.removeEventListener("VideoBindingAttached", this, true, true);
    this.browser.removeEventListener("VideoBindingCast", this, true, true);

    Services.obs.removeObserver(this, "before-first-paint");
    Services.obs.removeObserver(this, "media-playback", false);
    Services.obs.removeObserver(this, "media-playback-resumed", false);

    // Make sure the previously selected panel remains selected. The selected panel of a deck is
    // not stable when panels are removed.
    let selectedPanel = BrowserApp.deck.selectedPanel;
    BrowserApp.deck.removeChild(this.browser);
    BrowserApp.deck.selectedPanel = selectedPanel;

    this.browser = null;
  },

  // This should be called to update the browser when the tab gets selected/unselected
  setActive: function setActive(aActive) {
    if (!this.browser || !this.browser.docShell)
      return;

    this.lastTouchedAt = Date.now();

    if (aActive) {
      this.browser.setAttribute("type", "content-primary");
      this.browser.focus();
      this.browser.docShellIsActive = true;
      Reader.updatePageAction(this);
      ExternalApps.updatePageAction(this.browser.currentURI, this.browser.contentDocument);
    } else {
      this.browser.setAttribute("type", "content-targetable");
      this.browser.docShellIsActive = false;
      this.browser.blur();
    }
  },

  getActive: function getActive() {
    return this.browser.docShellIsActive;
  },

  // These constants are used to prioritize high quality metadata over low quality data, so that
  // we can collect data as we find meta tags, and replace low quality metadata with higher quality
  // matches. For instance a msApplicationTile icon is a better tile image than an og:image tag.
  METADATA_GOOD_MATCH: 10,
  METADATA_NORMAL_MATCH: 1,

  addMetadata: function(type, value, quality = 1) {
    if (!this.metatags) {
      this.metatags = {
        url: this.browser.currentURI.specIgnoringRef
      };
    }

    if (type == "touchIconList") {
      if (!this.metatags['touchIconList']) {
        this.metatags['touchIconList'] = {};
      }
      this.metatags.touchIconList[quality] = value;
    } else if (!this.metatags[type] || this.metatags[type + "_quality"] < quality) {
      this.metatags[type] = value;
      this.metatags[type + "_quality"] = quality;
    }
  },

  sanitizeRelString: function(linkRel) {
    // Sanitize the rel string
    let list = [];
    if (linkRel) {
      list = linkRel.toLowerCase().split(/\s+/);
      let hash = {};
      list.forEach(function(value) { hash[value] = true; });
      list = [];
      for (let rel in hash)
      list.push("[" + rel + "]");
    }
    return list;
  },

  makeFaviconMessage: function(eventTarget) {
    // We want to get the largest icon size possible for our UI.
    let maxSize = 0;

    // We use the sizes attribute if available
    // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
    if (eventTarget.hasAttribute("sizes")) {
      let sizes = eventTarget.getAttribute("sizes").toLowerCase();

      if (sizes == "any") {
        // Since Java expects an integer, use -1 to represent icons with sizes="any"
        maxSize = -1;
      } else {
        let tokens = sizes.split(" ");
        tokens.forEach(function(token) {
          // TODO: check for invalid tokens
          let [w, h] = token.split("x");
          maxSize = Math.max(maxSize, Math.max(w, h));
        });
      }
    }
    return {
      type: "Link:Favicon",
      tabID: this.id,
      href: resolveGeckoURI(eventTarget.href),
      size: maxSize,
      mime: eventTarget.getAttribute("type") || ""
    };
  },

  makeFeedMessage: function(eventTarget, targetType) {
    try {
      // urlSecurityCeck will throw if things are not OK
      ContentAreaUtils.urlSecurityCheck(eventTarget.href,
            eventTarget.ownerDocument.nodePrincipal,
            Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);

      if (!this.browser.feeds)
        this.browser.feeds = [];

      this.browser.feeds.push({
        href: eventTarget.href,
        title: eventTarget.title,
        type: targetType
      });

      return {
        type: "Link:Feed",
        tabID: this.id
      };
    } catch (e) {
        return null;
    }
  },

  sendOpenSearchMessage: function(eventTarget) {
    let type = eventTarget.type && eventTarget.type.toLowerCase();
    // Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
    type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");

    // Check that type matches opensearch.
    let isOpenSearch = (type == "application/opensearchdescription+xml");
    if (isOpenSearch && eventTarget.title && /^(?:https?|ftp):/i.test(eventTarget.href)) {
      Services.search.init(() => {
        let visibleEngines = Services.search.getVisibleEngines();
        // NOTE: Engines are currently identified by name, but this can be changed
        // when Engines are identified by URL (see bug 335102).
        if (visibleEngines.some(function(e) {
          return e.name == eventTarget.title;
        })) {
          // This engine is already present, do nothing.
          return null;
        }

        if (this.browser.engines) {
          // This engine has already been handled, do nothing.
          if (this.browser.engines.some(function(e) {
            return e.url == eventTarget.href;
          })) {
            return null;
          }
        } else {
            this.browser.engines = [];
        }

        // Get favicon.
        let iconURL = eventTarget.ownerDocument.documentURIObject.prePath + "/favicon.ico";

        let newEngine = {
          title: eventTarget.title,
          url: eventTarget.href,
          iconURL: iconURL
        };

        this.browser.engines.push(newEngine);

        // Don't send a message to display engines if we've already handled an engine.
        if (this.browser.engines.length > 1)
          return null;

        // Broadcast message that this tab contains search engines that should be visible.
        Messaging.sendRequest({
          type: "Link:OpenSearch",
          tabID: this.id,
          visible: true
        });
      });
    }
  },

  handleEvent: function(aEvent) {
    switch (aEvent.type) {
      case "DOMContentLoaded": {
        let target = aEvent.originalTarget;

        // ignore on frames and other documents
        if (target != this.browser.contentDocument)
          return;

        // Sample the background color of the page and pass it along. (This is used to draw the
        // checkerboard.) Right now we don't detect changes in the background color after this
        // event fires; it's not clear that doing so is worth the effort.
        var backgroundColor = null;
        try {
          let { contentDocument, contentWindow } = this.browser;
          let computedStyle = contentWindow.getComputedStyle(contentDocument.body);
          backgroundColor = computedStyle.backgroundColor;
        } catch (e) {
          // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds.
        }

        let docURI = target.documentURI;
        let errorType = "";
        if (docURI.startsWith("about:certerror")) {
          errorType = "certerror";
        }
        else if (docURI.startsWith("about:blocked")) {
          errorType = "blocked";
        }
        else if (docURI.startsWith("about:neterror")) {
          let error = docURI.search(/e\=/);
          let duffUrl = docURI.search(/\&u\=/);
          let errorExtra = decodeURIComponent(docURI.slice(error + 2, duffUrl));
          // Here is a list of errorExtra types (et_*)
          // http://mxr.mozilla.org/mozilla-central/source/mobile/android/chrome/content/netError.xhtml#287
          UITelemetry.addEvent("neterror.1", "content", null, errorExtra);
          errorType = "neterror";
        }

        // Attach a listener to watch for "click" events bubbling up from error
        // pages and other similar page. This lets us fix bugs like 401575 which
        // require error page UI to do privileged things, without letting error
        // pages have any privilege themselves.
        if (docURI.startsWith("about:neterror")) {
          NetErrorHelper.attachToBrowser(this.browser);
        }

        Messaging.sendRequest({
          type: "DOMContentLoaded",
          tabID: this.id,
          bgColor: backgroundColor,
          errorType: errorType,
          metadata: this.metatags,
        });

        // Reset isSearch so that the userRequested term will be erased on next page load
        this.metatags = null;

        if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) {
          this.browser.addEventListener("click", ErrorPageEventHandler, true);
          let listener = function() {
            this.browser.removeEventListener("click", ErrorPageEventHandler, true);
            this.browser.removeEventListener("pagehide", listener, true);
          }.bind(this);

          this.browser.addEventListener("pagehide", listener, true);
        }

        if (AppConstants.NIGHTLY_BUILD || AppConstants.MOZ_ANDROID_ACTIVITY_STREAM) {
          WebsiteMetadata.parseAsynchronously(this.browser.contentDocument);
        }

        break;
      }

      case "DOMFormHasPassword": {
        LoginManagerContent.onDOMFormHasPassword(aEvent,
                                                 this.browser.contentWindow);

        // Send logins for this hostname to Java.
        let hostname = aEvent.target.baseURIObject.prePath;
        let foundLogins = Services.logins.findLogins({}, hostname, "", "");
        if (foundLogins.length > 0) {
          let displayHost = IdentityHandler.getEffectiveHost();
          let title = { text: displayHost, resource: hostname };
          let selectObj = { title: title, logins: foundLogins };
          Messaging.sendRequest({ type: "Doorhanger:Logins", data: selectObj });
        }
        break;
      }

      case "DOMInputPasswordAdded": {
        LoginManagerContent.onDOMInputPasswordAdded(aEvent,
                                                    this.browser.contentWindow);
      }

      case "DOMMetaAdded":
        let target = aEvent.originalTarget;
        let browser = BrowserApp.getBrowserForDocument(target.ownerDocument);

        switch (target.name) {
          case "msapplication-TileImage":
            this.addMetadata("tileImage", browser.currentURI.resolve(target.content), this.METADATA_GOOD_MATCH);
            break;
          case "msapplication-TileColor":
            this.addMetadata("tileColor", target.content, this.METADATA_GOOD_MATCH);
            break;
        }

        break;

      case "DOMLinkAdded":
      case "DOMLinkChanged": {
        let jsonMessage = null;
        let target = aEvent.originalTarget;
        if (!target.href || target.disabled)
          return;

        // Ignore on frames and other documents
        if (target.ownerDocument != this.browser.contentDocument)
          return;

        // Sanitize rel link
        let list = this.sanitizeRelString(target.rel);
        if (list.indexOf("[icon]") != -1) {
          jsonMessage = this.makeFaviconMessage(target);
        } else if (list.indexOf("[apple-touch-icon]") != -1 ||
            list.indexOf("[apple-touch-icon-precomposed]") != -1) {
          jsonMessage = this.makeFaviconMessage(target);
          jsonMessage['type'] = 'Link:Touchicon';
          this.addMetadata("touchIconList", jsonMessage.href, jsonMessage.size);
        } else if (list.indexOf("[alternate]") != -1 && aEvent.type == "DOMLinkAdded") {
          let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
          let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");

          if (!isFeed)
            return;

          jsonMessage = this.makeFeedMessage(target, type);
        } else if (list.indexOf("[search]") != -1 && aEvent.type == "DOMLinkAdded") {
          this.sendOpenSearchMessage(target);
        }
        if (!jsonMessage)
         return;

        Messaging.sendRequest(jsonMessage);
        break;
      }

      case "DOMTitleChanged": {
        if (!aEvent.isTrusted)
          return;

        // ignore on frames and other documents
        if (aEvent.originalTarget != this.browser.contentDocument)
          return;

        Messaging.sendRequest({
          type: "DOMTitleChanged",
          tabID: this.id,
          title: truncate(aEvent.target.title, MAX_TITLE_LENGTH)
        });
        break;
      }

      case "TabPreZombify": {
        if (!this.playingAudio) {
          return;
        }
        // Fall through to the DOMAudioPlayback events, so the
        // audio playback indicator gets reset upon zombification.
      }
      case "DOMAudioPlaybackStarted":
      case "DOMAudioPlaybackStopped": {
        if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
            !aEvent.isTrusted) {
          return;
        }

        let browser = aEvent.originalTarget;
        if (browser != this.browser) {
          return;
        }

        this.playingAudio = aEvent.type === "DOMAudioPlaybackStarted";

        Messaging.sendRequest({
          type: "Tab:AudioPlayingChange",
          tabID: this.id,
          isAudioPlaying: this.playingAudio
        });
        return;
      }

      case "DOMWindowClose": {
        if (!aEvent.isTrusted)
          return;

        // Find the relevant tab, and close it from Java
        if (this.browser.contentWindow == aEvent.target) {
          aEvent.preventDefault();

          Messaging.sendRequest({
            type: "Tab:Close",
            tabID: this.id
          });
        }
        break;
      }

      case "DOMWillOpenModalDialog": {
        if (!aEvent.isTrusted)
          return;

        // We're about to open a modal dialog, make sure the opening
        // tab is brought to the front.
        let tab = BrowserApp.getTabForWindow(aEvent.target.top);
        BrowserApp.selectTab(tab);
        break;
      }

      case "DOMAutoComplete":
      case "blur": {
        LoginManagerContent.onUsernameInput(aEvent);
        break;
      }

      case "PluginBindingAttached": {
        PluginHelper.handlePluginBindingAttached(this, aEvent);
        break;
      }

      case "VideoBindingAttached": {
        CastingApps.handleVideoBindingAttached(this, aEvent);
        break;
      }

      case "VideoBindingCast": {
        CastingApps.handleVideoBindingCast(this, aEvent);
        break;
      }

      case "MozApplicationManifest": {
        OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView);
        break;
      }

      case "pageshow": {
        LoginManagerContent.onPageShow(aEvent, this.browser.contentWindow);

        // The rest of this only handles pageshow for the top-level document.
        if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
          return;

        let target = aEvent.originalTarget;
        let docURI = target.documentURI;
        if (!docURI.startsWith("about:neterror") && !this.isSearch) {
          // If this wasn't an error page and the user isn't search, don't retain the typed entry
          this.userRequested = "";
        }

        Messaging.sendRequest({
          type: "Content:PageShow",
          tabID: this.id,
          userRequested: this.userRequested,
          fromCache: Tabs.useCache
        });

        this.isSearch = false;

        if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
          if (!this._linkifier)
            this._linkifier = new Linkifier();
          this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
        }

        // Update page actions for helper apps.
        let uri = this.browser.currentURI;
        if (BrowserApp.selectedTab == this) {
          if (ExternalApps.shouldCheckUri(uri)) {
            ExternalApps.updatePageAction(uri, this.browser.contentDocument);
          } else {
            ExternalApps.clearPageAction();
          }
        }
      }
    }
  },

  onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
    let contentWin = aWebProgress.DOMWindow;
    if (contentWin != contentWin.top)
        return;

    // Filter optimization: Only really send NETWORK state changes to Java listener
    if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
      if (AppConstants.NIGHTLY_BUILD && (aStateFlags & Ci.nsIWebProgressListener.STATE_START)) {
        Profiler.AddMarker("Load start: " + aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec);
      } else if (AppConstants.NIGHTLY_BUILD && (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && !aWebProgress.isLoadingDocument) {
        Profiler.AddMarker("Load stop: " + aRequest.QueryInterface(Ci.nsIChannel).originalURI.spec);
      }

      if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) {
        // We may receive a document stop event while a document is still loading
        // (such as when doing URI fixup). Don't notify Java UI in these cases.
        return;
      }

      // Clear page-specific opensearch engines and feeds for a new request.
      if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) {
        this.browser.engines = null;
        this.browser.feeds = null;
      }

      // true if the page loaded successfully (i.e., no 404s or other errors)
      let success = false;
      let uri = "";
      try {
        // Remember original URI for UA changes on redirected pages
        this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI;

        if (this.originalURI != null)
          uri = this.originalURI.spec;
      } catch (e) { }
      try {
        success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded;
      } catch (e) {
        // If the request does not handle the nsIHttpChannel interface, use nsIRequest's success
        // status. Used for local files. See bug 948849.
        success = aRequest.status == 0;
      }

      // Check to see if we restoring the content from a previous presentation (session)
      // since there should be no real network activity
      let restoring = (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) > 0;

      let message = {
        type: "Content:StateChange",
        tabID: this.id,
        uri: truncate(uri, MAX_URI_LENGTH),
        state: aStateFlags,
        restoring: restoring,
        success: success
      };
      Messaging.sendRequest(message);
    }
  },

  onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) {
    let contentWin = aWebProgress.DOMWindow;

    // Browser webapps may load content inside iframes that can not reach across the app/frame boundary
    // i.e. even though the page is loaded in an iframe window.top != webapp
    // Make cure this window is a top level tab before moving on.
    if (BrowserApp.getBrowserForWindow(contentWin) == null)
      return;

    this._hostChanged = true;

    let fixedURI = aLocationURI;
    try {
      fixedURI = URIFixup.createExposableURI(aLocationURI);
    } catch (ex) { }

    // In restricted profiles, we refuse to let you open various urls.
    if (!ParentalControls.isAllowed(ParentalControls.BROWSE, fixedURI)) {
      aRequest.cancel(Cr.NS_BINDING_ABORTED);

      this.browser.docShell.displayLoadError(Cr.NS_ERROR_UNKNOWN_PROTOCOL, fixedURI, null);
    }

    let contentType = contentWin.document.contentType;

    // If fixedURI matches browser.lastURI, we assume this isn't a real location
    // change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883.
    // Note that we have to ensure fixedURI is not the same as aLocationURI so we
    // don't false-positive page reloads as spurious additions.
    let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 ||
                       ((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI));
    this.browser.lastURI = fixedURI;

    // Let the reader logic know about same document changes because we won't get a DOMContentLoaded
    // or pageshow event, but we'll still want to update the reader view button to account for this change.
    // This mirrors the desktop logic in TabsProgressListener.
    if (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) {
      this.browser.messageManager.sendAsyncMessage("Reader:PushState", {isArticle: this.browser.isArticle});
    }

    // Reset state of click-to-play plugin notifications.
    clearTimeout(this.pluginDoorhangerTimeout);
    this.pluginDoorhangerTimeout = null;
    this.shouldShowPluginDoorhanger = true;
    this.clickToPlayPluginsActivated = false;

    let documentURI = contentWin.document.documentURIObject.spec;

    // If reader mode, get the base domain for the original url.
    let strippedURI = this._stripAboutReaderURL(documentURI);

    // Borrowed from desktop Firefox: http://hg.mozilla.org/mozilla-central/annotate/72835344333f/browser/base/content/urlbarBindings.xml#l236
    let matchedURL = strippedURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
    let baseDomain = "";
    if (matchedURL) {
      var domain = "";
      [, , domain] = matchedURL;

      try {
        baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
        if (!domain.endsWith(baseDomain)) {
          // getBaseDomainFromHost converts its resultant to ACE.
          let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
          baseDomain = IDNService.convertACEtoUTF8(baseDomain);
        }
      } catch (e) {}
    }

    // If we are navigating to a new location with a different host,
    // clear any URL origin that might have been pinned to this tab.
    let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
    let appOrigin = ss.getTabValue(this, "appOrigin");
    if (appOrigin) {
      let originHost = "";
      try {
        originHost = Services.io.newURI(appOrigin, null, null).host;
      } catch (e if (e.result == Cr.NS_ERROR_FAILURE)) {
        // NS_ERROR_FAILURE can be thrown by nsIURI.host if the URI scheme does not possess a host - in this case
        // we just act as if we have an empty host.
      }
      if (originHost != aLocationURI.host) {
        // Note: going 'back' will not make this tab pinned again
        ss.deleteTabValue(this, "appOrigin");
      }
    }

    // Update the page actions URI for helper apps.
    if (BrowserApp.selectedTab == this) {
      ExternalApps.updatePageActionUri(fixedURI);
    }

    let webNav = contentWin.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);

    let message = {
      type: "Content:LocationChange",
      tabID: this.id,
      uri: truncate(fixedURI.spec, MAX_URI_LENGTH),
      userRequested: this.userRequested || "",
      baseDomain: baseDomain,
      contentType: (contentType ? contentType : ""),
      sameDocument: sameDocument,

      historyIndex: webNav.sessionHistory.index,
      historySize: webNav.sessionHistory.count,
      canGoBack: webNav.canGoBack,
      canGoForward: webNav.canGoForward,
    };

    Messaging.sendRequest(message);

    if (!sameDocument) {
      // XXX This code assumes that this is the earliest hook we have at which
      // browser.contentDocument is changed to the new document we're loading
      this.contentDocumentIsDisplayed = false;
      this.hasTouchListener = false;
      Services.obs.notifyObservers(this.browser, "Session:NotifyLocationChange", null);
    }
  },

  _stripAboutReaderURL: function (url) {
    return ReaderMode.getOriginalUrl(url) || url;
  },

  // Properties used to cache security state used to update the UI
  _state: null,
  _hostChanged: false, // onLocationChange will flip this bit

  onSecurityChange: function(aWebProgress, aRequest, aState) {
    // Don't need to do anything if the data we use to update the UI hasn't changed
    if (this._state == aState && !this._hostChanged)
      return;

    this._state = aState;
    this._hostChanged = false;

    let identity = IdentityHandler.checkIdentity(aState, this.browser);

    let message = {
      type: "Content:SecurityChange",
      tabID: this.id,
      identity: identity
    };

    Messaging.sendRequest(message);
  },

  onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
    // Note: aWebProgess and aRequest will be NULL since we are filtering webprogress
    // notifications using nsBrowserStatusFilter.
  },

  onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
    // Note: aWebProgess and aRequest will be NULL since we are filtering webprogress
    // notifications using nsBrowserStatusFilter.
  },

  _getGeckoZoom: function() {
    let res = {};
    let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
    cwu.getResolution(res);
    let zoom = res.value * window.devicePixelRatio;
    return zoom;
  },

  saveSessionZoom: function(aZoom) {
    let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
    cwu.setResolutionAndScaleTo(aZoom / window.devicePixelRatio);
  },

  restoredSessionZoom: function() {
    let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);

    if (this._restoreZoom && cwu.isResolutionSet) {
      return this._getGeckoZoom();
    }
    return null;
  },

  _updateZoomFromHistoryEvent: function(aHistoryEventName) {
    // Restore zoom only when moving in session history, not for new page loads.
    this._restoreZoom = aHistoryEventName !== "New";
  },

  OnHistoryNewEntry: function(aUri) {
    this._updateZoomFromHistoryEvent("New");
  },

  OnHistoryGoBack: function(aUri) {
    this._updateZoomFromHistoryEvent("Back");
    return true;
  },

  OnHistoryGoForward: function(aUri) {
    this._updateZoomFromHistoryEvent("Forward");
    return true;
  },

  OnHistoryReload: function(aUri, aFlags) {
    // we don't do anything with this, so don't propagate it
    // for now anyway
    return true;
  },

  OnHistoryGotoIndex: function(aIndex, aUri) {
    this._updateZoomFromHistoryEvent("Goto");
    return true;
  },

  OnHistoryPurge: function(aNumEntries) {
    this._updateZoomFromHistoryEvent("Purge");
    return true;
  },

  OnHistoryReplaceEntry: function(aIndex) {
    // we don't do anything with this, so don't propogate it
    // for now anyway.
  },

  ShouldNotifyMediaPlaybackChange: function(activeState) {
    // If the media is active, we would check it's duration, because we don't
    // want to show the media control interface for the short sound which
    // duration is smaller than the threshold. The basic unit is second.
    // Note : the streaming format's duration is infinite.
    if (activeState === "inactive") {
      return true;
    }

    const mediaDurationThreshold = 1.0;

    let audioElements = this.browser.contentDocument.getElementsByTagName("audio");
    for each (let audio in audioElements) {
      if (!audio.paused && audio.duration < mediaDurationThreshold) {
        return false;
      }
    }

    let videoElements = this.browser.contentDocument.getElementsByTagName("video");
    for each (let video in videoElements) {
      if (!video.paused && video.duration < mediaDurationThreshold) {
        return false;
      }
    }

    return true;
  },

  observe: function(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "before-first-paint":
        // Is it on the top level?
        let contentDocument = aSubject;
        if (contentDocument == this.browser.contentDocument) {
          if (BrowserApp.selectedTab == this) {
            BrowserApp.contentDocumentChanged();
          }
          this.contentDocumentIsDisplayed = true;

          if (contentDocument instanceof Ci.nsIImageDocument) {
            contentDocument.shrinkToFit();
          }
        }
        break;

      case "media-playback":
      case "media-playback-resumed":
        if (!aSubject) {
          return;
        }

        let winId = aSubject.QueryInterface(Ci.nsISupportsPRUint64).data;
        if (this.browser.outerWindowID != winId) {
          return;
        }

        if (!this.ShouldNotifyMediaPlaybackChange(aData)) {
          return;
        }

        let status;
        if (aTopic == "media-playback") {
          status = (aData === "inactive") ? "end" : "start";
        } else if (aTopic == "media-playback-resumed") {
          status = "resume";
        }

        Messaging.sendRequest({
          type: "Tab:MediaPlaybackChange",
          tabID: this.id,
          status: status
        });
        break;
    }
  },

  // nsIBrowserTab
  get window() {
    if (!this.browser)
      return null;
    return this.browser.contentWindow;
  },

  get scale() {
    return this._zoom;
  },

  QueryInterface: XPCOMUtils.generateQI([
    Ci.nsIWebProgressListener,
    Ci.nsISHistoryListener,
    Ci.nsIObserver,
    Ci.nsISupportsWeakReference,
    Ci.nsIBrowserTab
  ])
};

var BrowserEventHandler = {
  init: function init() {
    this._clickInZoomedView = false;
    Services.obs.addObserver(this, "Gesture:SingleTap", false);
    Services.obs.addObserver(this, "Gesture:ClickInZoomedView", false);

    BrowserApp.deck.addEventListener("touchend", this, true);

    BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false);
    BrowserApp.deck.addEventListener("MozMouseHittest", this, true);
    BrowserApp.deck.addEventListener("OpenMediaWithExternalApp", this, true);

    InitLater(() => BrowserApp.deck.addEventListener("click", InputWidgetHelper, true));
    InitLater(() => BrowserApp.deck.addEventListener("click", SelectHelper, true));

    // ReaderViews support backPress listeners.
    Messaging.addListener(() => {
      return Reader.onBackPress(BrowserApp.selectedTab.id);
    }, "Browser:OnBackPressed");
  },

  handleEvent: function(aEvent) {
    switch (aEvent.type) {
      case 'touchend':
        if (this._inCluster) {
          aEvent.preventDefault();
        }
        break;
      case 'MozMouseHittest':
        this._handleRetargetedTouchStart(aEvent);
        break;
      case 'OpenMediaWithExternalApp': {
        let mediaSrc = aEvent.target.currentSrc || aEvent.target.src;
        let uuid = uuidgen.generateUUID().toString();
        Services.androidBridge.handleGeckoMessage({
          type: "Video:Play",
          uri: mediaSrc,
          uuid: uuid
        });
        break;
      }
    }
  },

  _handleRetargetedTouchStart: function(aEvent) {
    // we should only get this called just after a new touchstart with a single
    // touch point.
    if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.defaultPrevented) {
      return;
    }

    let target = aEvent.target;
    if (!target) {
      return;
    }

    this._inCluster = aEvent.hitCluster;
    if (this._inCluster) {
      return;  // No highlight for a cluster of links
    }

    let uri = this._getLinkURI(target);
    if (uri) {
      try {
        Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
      } catch (e) {}
    }
    this._doTapHighlight(target);
  },

  _getLinkURI: function(aElement) {
    if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
        ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
        (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) {
      try {
        return Services.io.newURI(aElement.href, null, null);
      } catch (e) {}
    }
    return null;
  },

  observe: function(aSubject, aTopic, aData) {
    // the remaining events are all dependent on the browser content document being the
    // same as the browser displayed document. if they are not the same, we should ignore
    // the event.
    if (BrowserApp.isBrowserContentDocumentDisplayed()) {
      this.handleUserEvent(aTopic, aData);
    }
  },

  handleUserEvent: function(aTopic, aData) {
    switch (aTopic) {

      case "Gesture:ClickInZoomedView":
        this._clickInZoomedView = true;
        break;

      case "Gesture:SingleTap": {
        let focusedElement = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser);
        let data = JSON.parse(aData);
        let {x, y} = data;

        if (this._inCluster && this._clickInZoomedView != true) {
          // If there is a focused element, the display of the zoomed view won't remove the focus.
          // In this case, the form assistant linked to the focused element will never be closed.
          // To avoid this situation, the focus is moved and the form assistant is closed.
          if (focusedElement) {
            try {
              Services.focus.moveFocus(BrowserApp.selectedBrowser.contentWindow, null, Services.focus.MOVEFOCUS_ROOT, 0);
            } catch(e) {
              Cu.reportError(e);
            }
            Messaging.sendRequest({ type: "FormAssist:Hide" });
          }
          this._clusterClicked(x, y);
        } else {
          if (this._clickInZoomedView != true) {
            this._closeZoomedView();
          }
        }
        this._clickInZoomedView = false;
        this._cancelTapHighlight();
        break;
      }

      default:
        dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
        break;
    }
  },

  _closeZoomedView: function() {
    Messaging.sendRequest({
      type: "Gesture:CloseZoomedView"
    });
  },

  _clusterClicked: function(aX, aY) {
    Messaging.sendRequest({
      type: "Gesture:clusteredLinksClicked",
      clickPosition: {
        x: aX,
        y: aY
      }
    });
  },

  _highlightElement: null,

  _doTapHighlight: function _doTapHighlight(aElement) {
    this._highlightElement = aElement;
  },

  _cancelTapHighlight: function _cancelTapHighlight() {
    if (!this._highlightElement)
      return;

    this._highlightElement = null;
  }
};

const ElementTouchHelper = {
  getBoundingContentRect: function(aElement) {
    if (!aElement)
      return {x: 0, y: 0, w: 0, h: 0};

    let document = aElement.ownerDocument;
    while (document.defaultView.frameElement)
      document = document.defaultView.frameElement.ownerDocument;

    let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
    let scrollX = {}, scrollY = {};
    cwu.getScrollXY(false, scrollX, scrollY);

    let r = aElement.getBoundingClientRect();

    // step out of iframes and frames, offsetting scroll values
    for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) {
      // adjust client coordinates' origin to be top left of iframe viewport
      let rect = frame.frameElement.getBoundingClientRect();
      let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
      let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
      scrollX.value += rect.left + parseInt(left);
      scrollY.value += rect.top + parseInt(top);
    }

    return {x: r.left + scrollX.value,
            y: r.top + scrollY.value,
            w: r.width,
            h: r.height };
  }
};

var ErrorPageEventHandler = {
  handleEvent: function(aEvent) {
    switch (aEvent.type) {
      case "click": {
        // Don't trust synthetic events
        if (!aEvent.isTrusted)
          return;

        let target = aEvent.originalTarget;
        let errorDoc = target.ownerDocument;

        // If the event came from an ssl error page, it is probably either the "Add
        // Exception…" or "Get me out of here!" button
        if (errorDoc.documentURI.startsWith("about:certerror?e=nssBadCert")) {
          let perm = errorDoc.getElementById("permanentExceptionButton");
          let temp = errorDoc.getElementById("temporaryExceptionButton");
          if (target == temp || target == perm) {
            // Handle setting an cert exception and reloading the page
            try {
              // Add a new SSL exception for this URL
              let uri = Services.io.newURI(errorDoc.location.href, null, null);
              let sslExceptions = new SSLExceptions();

              if (target == perm)
                sslExceptions.addPermanentException(uri, errorDoc.defaultView);
              else
                sslExceptions.addTemporaryException(uri, errorDoc.defaultView);
            } catch (e) {
              dump("Failed to set cert exception: " + e + "\n");
            }
            errorDoc.location.reload();
          } else if (target == errorDoc.getElementById("getMeOutOfHereButton")) {
            errorDoc.location = "about:home";
          }
        } else if (errorDoc.documentURI.startsWith("about:blocked")) {
          // The event came from a button on a malware/phishing block page
          // First check whether it's malware, phishing or unwanted, so that we
          // can use the right strings/links
          let bucketName = "";
          let sendTelemetry = false;
          if (errorDoc.documentURI.includes("e=malwareBlocked")) {
            sendTelemetry = true;
            bucketName = "WARNING_MALWARE_PAGE_";
          } else if (errorDoc.documentURI.includes("e=deceptiveBlocked")) {
            sendTelemetry = true;
            bucketName = "WARNING_PHISHING_PAGE_";
          } else if (errorDoc.documentURI.includes("e=unwantedBlocked")) {
            sendTelemetry = true;
            bucketName = "WARNING_UNWANTED_PAGE_";
          }
          let nsISecTel = Ci.nsISecurityUITelemetry;
          let isIframe = (errorDoc.defaultView.parent === errorDoc.defaultView);
          bucketName += isIframe ? "TOP_" : "FRAME_";

          let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);

          if (target == errorDoc.getElementById("getMeOutButton")) {
            if (sendTelemetry) {
              Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]);
            }
            errorDoc.location = "about:home";
          } else if (target == errorDoc.getElementById("reportButton")) {
            // We log even if malware/phishing info URL couldn't be found:
            // the measurement is for how many users clicked the WHY BLOCKED button
            if (sendTelemetry) {
              Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "WHY_BLOCKED"]);
            }

            // This is the "Why is this site blocked" button. We redirect
            // to the generic page describing phishing/malware protection.
            let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
            BrowserApp.selectedBrowser.loadURI(url + "phishing-malware");
          } else if (target == errorDoc.getElementById("ignoreWarningButton") &&
                     Services.prefs.getBoolPref("browser.safebrowsing.allowOverride")) {
            if (sendTelemetry) {
              Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "IGNORE_WARNING"]);
            }

            // Allow users to override and continue through to the site,
            let webNav = BrowserApp.selectedBrowser.docShell.QueryInterface(Ci.nsIWebNavigation);
            let location = BrowserApp.selectedBrowser.contentWindow.location;
            webNav.loadURI(location, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, null, null, null);

            // ....but add a notify bar as a reminder, so that they don't lose
            // track after, e.g., tab switching.
            NativeWindow.doorhanger.show(Strings.browser.GetStringFromName("safeBrowsingDoorhanger"), "safebrowsing-warning", [], BrowserApp.selectedTab.id);
          }
        }
        break;
      }
    }
  }
};

var FormAssistant = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),

  // Used to keep track of the element that corresponds to the current
  // autocomplete suggestions
  _currentInputElement: null,

  // The value of the currently focused input
  _currentInputValue: null,

  // Whether we're in the middle of an autocomplete
  _doingAutocomplete: false,

  // Keep track of whether or not an invalid form has been submitted
  _invalidSubmit: false,

  init: function() {
    Services.obs.addObserver(this, "FormAssist:AutoComplete", false);
    Services.obs.addObserver(this, "FormAssist:Hidden", false);
    Services.obs.addObserver(this, "FormAssist:Remove", false);
    Services.obs.addObserver(this, "invalidformsubmit", false);
    Services.obs.addObserver(this, "PanZoom:StateChange", false);

    // We need to use a capturing listener for focus events
    BrowserApp.deck.addEventListener("focus", this, true);
    BrowserApp.deck.addEventListener("blur", this, true);
    BrowserApp.deck.addEventListener("click", this, true);
    BrowserApp.deck.addEventListener("input", this, false);
    BrowserApp.deck.addEventListener("pageshow", this, false);
  },

  observe: function(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "PanZoom:StateChange":
        // If the user is just touching the screen and we haven't entered a pan or zoom state yet do nothing
        if (aData == "TOUCHING" || aData == "WAITING_LISTENERS")
          break;
        if (aData == "NOTHING") {
          // only look for input elements, not contentEditable or multiline text areas
          let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true);
          if (!focused)
            break;

          if (this._showValidationMessage(focused))
            break;
          let checkResultsClick = hasResults => {
            if (!hasResults) {
              this._hideFormAssistPopup();
            }
          };
          this._showAutoCompleteSuggestions(focused, checkResultsClick);
        } else {
          // temporarily hide the form assist popup while we're panning or zooming the page
          this._hideFormAssistPopup();
        }
        break;
      case "FormAssist:AutoComplete":
        if (!this._currentInputElement)
          break;

        let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);

        this._doingAutocomplete = true;

        // If we have an active composition string, commit it before sending
        // the autocomplete event with the text that will replace it.
        try {
          let imeEditor = editableElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
          if (imeEditor.composing)
            imeEditor.forceCompositionEnd();
        } catch (e) {}

        editableElement.setUserInput(aData);
        this._currentInputValue = aData;

        let event = this._currentInputElement.ownerDocument.createEvent("Events");
        event.initEvent("DOMAutoComplete", true, true);
        this._currentInputElement.dispatchEvent(event);

        this._doingAutocomplete = false;

        break;

      case "FormAssist:Hidden":
        this._currentInputElement = null;
        break;

      case "FormAssist:Remove":
        if (!this._currentInputElement) {
          break;
        }

        FormHistory.update({
          op: "remove",
          fieldname: this._currentInputElement.name,
          value: aData
        });
        break;
    }
  },

  notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
    if (!aInvalidElements.length)
      return;

    // Ignore this notificaiton if the current tab doesn't contain the invalid element
    let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
    if (BrowserApp.selectedBrowser.contentDocument !=
        currentElement.ownerDocument.defaultView.top.document)
      return;

    this._invalidSubmit = true;

    // Our focus listener will show the element's validation message
    currentElement.focus();
  },

  handleEvent: function(aEvent) {
    switch (aEvent.type) {
      case "focus": {
        let currentElement = aEvent.target;

        // Only show a validation message on focus.
        this._showValidationMessage(currentElement);
        break;
      }

      case "blur": {
        this._currentInputValue = null;
        break;
      }

      case "click": {
        let currentElement = aEvent.target;

        // Prioritize a form validation message over autocomplete suggestions
        // when the element is first focused (a form validation message will
        // only be available if an invalid form was submitted)
        if (this._showValidationMessage(currentElement))
          break;

        let checkResultsClick = hasResults => {
          if (!hasResults) {
            this._hideFormAssistPopup();
          }
        };

        this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
        break;
      }

      case "input": {
        let currentElement = aEvent.target;

        // If this element isn't focused, we're already in middle of an
        // autocomplete, or its value hasn't changed, don't show the
        // autocomplete popup.
        if (currentElement !== BrowserApp.getFocusedInput(BrowserApp.selectedBrowser) ||
            this._doingAutocomplete ||
            currentElement.value === this._currentInputValue) {
          break;
        }

        this._currentInputValue = currentElement.value;

        // Since we can only show one popup at a time, prioritze autocomplete
        // suggestions over a form validation message
        let checkResultsInput = hasResults => {
          if (hasResults)
            return;

          if (this._showValidationMessage(currentElement))
            return;

          // If we're not showing autocomplete suggestions, hide the form assist popup
          this._hideFormAssistPopup();
        };

        this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
        break;
      }

      // Reset invalid submit state on each pageshow
      case "pageshow": {
        if (!this._invalidSubmit)
          return;

        let selectedBrowser = BrowserApp.selectedBrowser;
        if (selectedBrowser) {
          let selectedDocument = selectedBrowser.contentDocument;
          let target = aEvent.originalTarget;
          if (target == selectedDocument || target.ownerDocument == selectedDocument)
            this._invalidSubmit = false;
        }
        break;
      }
    }
  },

  // We only want to show autocomplete suggestions for certain elements
  _isAutoComplete: function _isAutoComplete(aElement) {
    if (!(aElement instanceof HTMLInputElement) || aElement.readOnly || aElement.disabled ||
        (aElement.getAttribute("type") == "password") ||
        (aElement.hasAttribute("autocomplete") &&
         aElement.getAttribute("autocomplete").toLowerCase() == "off"))
      return false;

    return true;
  },

  // Retrieves autocomplete suggestions for an element from the form autocomplete service.
  // aCallback(array_of_suggestions) is called when results are available.
  _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) {
    // Cache the form autocomplete service for future use
    if (!this._formAutoCompleteService) {
      this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"]
          .getService(Ci.nsIFormAutoComplete);
    }

    let resultsAvailable = function (results) {
      let suggestions = [];
      for (let i = 0; i < results.matchCount; i++) {
        let value = results.getValueAt(i);

        // Do not show the value if it is the current one in the input field
        if (value == aSearchString)
          continue;

        // Supply a label and value, since they can differ for datalist suggestions
        suggestions.push({ label: value, value: value });
      }
      aCallback(suggestions);
    };

    this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
                                                          aSearchString, aElement, null,
                                                          null, resultsAvailable);
  },

  /**
   * (Copied from mobile/xul/chrome/content/forms.js)
   * This function is similar to getListSuggestions from
   * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
   * used by the autocomplete.xml binding which is not in used in fennec
   */
  _getListSuggestions: function _getListSuggestions(aElement) {
    if (!(aElement instanceof HTMLInputElement) || !aElement.list)
      return [];

    let suggestions = [];
    let filter = !aElement.hasAttribute("mozNoFilter");
    let lowerFieldValue = aElement.value.toLowerCase();

    let options = aElement.list.options;
    let length = options.length;
    for (let i = 0; i < length; i++) {
      let item = options.item(i);

      let label = item.value;
      if (item.label)
        label = item.label;
      else if (item.text)
        label = item.text;

      if (filter && !(label.toLowerCase().includes(lowerFieldValue)) )
        continue;
      suggestions.push({ label: label, value: item.value });
    }

    return suggestions;
  },

  // Retrieves autocomplete suggestions for an element from the form autocomplete service
  // and sends the suggestions to the Java UI, along with element position data. As
  // autocomplete queries are asynchronous, calls aCallback when done with a true
  // argument if results were found and false if no results were found.
  _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
    if (!this._isAutoComplete(aElement)) {
      aCallback(false);
      return;
    }
    if (this._isDisabledElement(aElement)) {
      aCallback(false);
      return;
    }

    let isEmpty = (aElement.value.length === 0);

    let resultsAvailable = autoCompleteSuggestions => {
      // On desktop, we show datalist suggestions below autocomplete suggestions,
      // without duplicates removed.
      let listSuggestions = this._getListSuggestions(aElement);
      let suggestions = autoCompleteSuggestions.concat(listSuggestions);

      // Return false if there are no suggestions to show
      if (!suggestions.length) {
        aCallback(false);
        return;
      }

      Messaging.sendRequest({
        type:  "FormAssist:AutoComplete",
        suggestions: suggestions,
        rect: ElementTouchHelper.getBoundingContentRect(aElement),
        isEmpty: isEmpty,
      });

      // Keep track of input element so we can fill it in if the user
      // selects an autocomplete suggestion
      this._currentInputElement = aElement;
      aCallback(true);
    };

    this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
  },

  // Only show a validation message if the user submitted an invalid form,
  // there's a non-empty message string, and the element is the correct type
  _isValidateable: function _isValidateable(aElement) {
    if (!this._invalidSubmit ||
        !aElement.validationMessage ||
        !(aElement instanceof HTMLInputElement ||
          aElement instanceof HTMLTextAreaElement ||
          aElement instanceof HTMLSelectElement ||
          aElement instanceof HTMLButtonElement))
      return false;

    return true;
  },

  // Sends a validation message and position data for an element to the Java UI.
  // Returns true if there's a validation message to show, false otherwise.
  _showValidationMessage: function _sendValidationMessage(aElement) {
    if (!this._isValidateable(aElement))
      return false;

    Messaging.sendRequest({
      type: "FormAssist:ValidationMessage",
      validationMessage: aElement.validationMessage,
      rect: ElementTouchHelper.getBoundingContentRect(aElement)
    });

    return true;
  },

  _hideFormAssistPopup: function _hideFormAssistPopup() {
    Messaging.sendRequest({ type: "FormAssist:Hide" });
  },

  _isDisabledElement : function(aElement) {
    let currentElement = aElement;
    while (currentElement) {
      if(currentElement.disabled)
	return true;

      currentElement = currentElement.parentElement;
    }
    return false;
  }
};

var XPInstallObserver = {
  init: function() {
    Services.obs.addObserver(this, "addon-install-origin-blocked", false);
    Services.obs.addObserver(this, "addon-install-disabled", false);
    Services.obs.addObserver(this, "addon-install-blocked", false);
    Services.obs.addObserver(this, "addon-install-started", false);
    Services.obs.addObserver(this, "xpi-signature-changed", false);
    Services.obs.addObserver(this, "browser-delayed-startup-finished", false);

    AddonManager.addInstallListener(this);
  },

  observe: function(aSubject, aTopic, aData) {
    let installInfo, tab, host;
    if (aSubject && aSubject instanceof Ci.amIWebInstallInfo) {
      installInfo = aSubject;
      tab = BrowserApp.getTabForBrowser(installInfo.browser);
      if (installInfo.originatingURI) {
        host = installInfo.originatingURI.host;
      }
    }

    let strings = Strings.browser;
    let brandShortName = Strings.brand.GetStringFromName("brandShortName");

    switch (aTopic) {
      case "addon-install-started":
        Snackbars.show(strings.GetStringFromName("alertAddonsDownloading"), Snackbars.LENGTH_LONG);
        break;
      case "addon-install-disabled": {
        if (!tab)
          return;

        let enabled = true;
        try {
          enabled = Services.prefs.getBoolPref("xpinstall.enabled");
        } catch (e) {}

        let buttons, message, callback;
        if (!enabled) {
          message = strings.GetStringFromName("xpinstallDisabledMessageLocked");
          buttons = [strings.GetStringFromName("unsignedAddonsDisabled.dismiss")];
          callback: (data) => {};
        } else {
          message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2);
          buttons = [
              strings.GetStringFromName("xpinstallDisabledButton"),
              strings.GetStringFromName("unsignedAddonsDisabled.dismiss")
          ];
          callback: (data) => {
            if (data.button === 1) {
              Services.prefs.setBoolPref("xpinstall.enabled", true)
            }
          };
        }

        new Prompt({
          title: Strings.browser.GetStringFromName("addonError.titleError"),
          message: message,
          buttons: buttons
        }).show(callback);
        break;
      }
      case "addon-install-blocked": {
        if (!tab)
          return;

        let message;
        if (host) {
          // We have a host which asked for the install.
          message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2);
        } else {
          // Without a host we address the add-on as the initiator of the install.
          let addon = null;
          if (installInfo.installs.length > 0) {
            addon = installInfo.installs[0].name;
          }
          if (addon) {
            // We have an addon name, show the regular message.
            message = strings.formatStringFromName("xpinstallPromptWarningLocal", [brandShortName, addon], 2);
          } else {
            // We don't have an addon name, show an alternative message.
            message = strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1);
          }
        }

        let buttons = [
            strings.GetStringFromName("xpinstallPromptAllowButton"),
            strings.GetStringFromName("unsignedAddonsDisabled.dismiss")
        ];
        new Prompt({
          title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
          message: message,
          buttons: buttons
        }).show((data) => {
          if (data.button === 0) {
            // Kick off the install
            installInfo.install();
          }
        });
        break;
      }
      case "addon-install-origin-blocked": {
        if (!tab)
          return;

        new Prompt({
          title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
          message: strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1),
          buttons: [strings.GetStringFromName("unsignedAddonsDisabled.dismiss")]
        }).show((data) => {});
        break;
      }
      case "xpi-signature-changed": {
        if (JSON.parse(aData).disabled.length) {
          this._notifyUnsignedAddonsDisabled();
        }
        break;
      }
      case "browser-delayed-startup-finished": {
        let disabledAddons = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
        for (let id of disabledAddons) {
          if (AddonManager.getAddonByID(id).signedState <= AddonManager.SIGNEDSTATE_MISSING) {
            this._notifyUnsignedAddonsDisabled();
            break;
          }
        }
        break;
      }
    }
  },

  _notifyUnsignedAddonsDisabled: function() {
    new Prompt({
      window: window,
      title: Strings.browser.GetStringFromName("unsignedAddonsDisabled.title"),
      message: Strings.browser.GetStringFromName("unsignedAddonsDisabled.message"),
      buttons: [
        Strings.browser.GetStringFromName("unsignedAddonsDisabled.viewAddons"),
        Strings.browser.GetStringFromName("unsignedAddonsDisabled.dismiss")
      ]
    }).show((data) => {
      if (data.button === 0) {
        // TODO: Open about:addons to show only unsigned add-ons?
        BrowserApp.selectOrAddTab("about:addons", { parentId: BrowserApp.selectedTab.id });
      }
    });
  },

  onInstallEnded: function(aInstall, aAddon) {
    // Don't create a notification for distribution add-ons.
    if (Distribution.pendingAddonInstalls.has(aInstall)) {
      Distribution.pendingAddonInstalls.delete(aInstall);
      return;
    }

    let needsRestart = false;
    if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
      needsRestart = true;
    else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
      needsRestart = true;

    if (needsRestart) {
      this.showRestartPrompt();
    } else {
      // Display completion message for new installs or updates not done Automatically
      if (!aInstall.existingAddon || !AddonManager.shouldAutoUpdate(aInstall.existingAddon)) {
        let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart.message");
        Snackbars.show(message, Snackbars.LENGTH_LONG, {
          action: {
            label: Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart.action2"),
            callback: () => {
              UITelemetry.addEvent("show.1", "toast", null, "addons");
              BrowserApp.selectOrAddTab("about:addons", { parentId: BrowserApp.selectedTab.id });
            },
          }
        });
      }
    }
  },

  onInstallFailed: function(aInstall) {
    this._showErrorMessage(aInstall);
  },

  onDownloadProgress: function(aInstall) {},

  onDownloadFailed: function(aInstall) {
    this._showErrorMessage(aInstall);
  },

  onDownloadCancelled: function(aInstall) {},

  _showErrorMessage: function(aInstall) {
    // Don't create a notification for distribution add-ons.
    if (Distribution.pendingAddonInstalls.has(aInstall)) {
      Cu.reportError("Error installing distribution add-on: " + aInstall.addon.id);
      Distribution.pendingAddonInstalls.delete(aInstall);
      return;
    }

    let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host;
    if (!host) {
      host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host;
    }

    let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError";
    if (aInstall.error < 0) {
      error += aInstall.error;
    } else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
      error += "Blocklisted";
    } else {
      error += "Incompatible";
    }

    let msg = Strings.browser.GetStringFromName(error);
    // TODO: formatStringFromName
    msg = msg.replace("#1", aInstall.name);
    if (host) {
      msg = msg.replace("#2", host);
    }
    msg = msg.replace("#3", Strings.brand.GetStringFromName("brandShortName"));
    msg = msg.replace("#4", Services.appinfo.version);

    if (aInstall.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
      new Prompt({
        window: window,
        title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
        message: msg,
        buttons: [Strings.browser.GetStringFromName("addonError.learnMore")]
      }).show((data) => {
        if (data.button === 0) {
          let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
          BrowserApp.addTab(url, { parentId: BrowserApp.selectedTab.id });
        }
      });
    } else {
      Services.prompt.alert(null, Strings.browser.GetStringFromName("addonError.titleError"), msg);
    }
  },

  showRestartPrompt: function() {
    let buttons = [{
      label: Strings.browser.GetStringFromName("notificationRestart.button"),
      callback: function() {
        // Notify all windows that an application quit has been requested
        let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
        Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");

        // If nothing aborted, quit the app
        if (cancelQuit.data == false) {
          Services.obs.notifyObservers(null, "quit-application-proceeding", null);
          let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
          appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
        }
      },
      positive: true
    }];

    let message = Strings.browser.GetStringFromName("notificationRestart.normal");
    NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 });
  },

  hideRestartPrompt: function() {
    NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id);
  }
};

var ViewportHandler = {
  init: function init() {
    Services.obs.addObserver(this, "Window:Resize", false);
  },

  observe: function(aSubject, aTopic, aData) {
    if (aTopic == "Window:Resize" && aData) {
      let scrollChange = JSON.parse(aData);
      let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
      windowUtils.setNextPaintSyncId(scrollChange.id);
    }
  }
};

/**
 * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml
 */
var PopupBlockerObserver = {
  onUpdatePageReport: function onUpdatePageReport(aEvent) {
    let browser = BrowserApp.selectedBrowser;
    if (aEvent.originalTarget != browser)
      return;

    if (!browser.pageReport)
      return;

    let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup");
    if (result == Ci.nsIPermissionManager.DENY_ACTION)
      return;

    // Only show the notification again if we've not already shown it. Since
    // notifications are per-browser, we don't need to worry about re-adding
    // it.
    if (!browser.pageReport.reported) {
      if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) {
        let brandShortName = Strings.brand.GetStringFromName("brandShortName");
        let popupCount = browser.pageReport.length;

        let strings = Strings.browser;
        let message = PluralForm.get(popupCount, strings.GetStringFromName("popup.message"))
                                .replace("#1", brandShortName)
                                .replace("#2", popupCount);

        let buttons = [
          {
            label: strings.GetStringFromName("popup.dontShow"),
            callback: function(aChecked) {
              if (aChecked)
                PopupBlockerObserver.allowPopupsForSite(false);
            }
          },
          {
            label: strings.GetStringFromName("popup.show"),
            callback: function(aChecked) {
              // Set permission before opening popup windows
              if (aChecked)
                PopupBlockerObserver.allowPopupsForSite(true);

              PopupBlockerObserver.showPopupsForSite();
            },
            positive: true
          }
        ];

        let options = { checkbox: Strings.browser.GetStringFromName("popup.dontAskAgain") };
        NativeWindow.doorhanger.show(message, "popup-blocked", buttons, null, options);
      }
      // Record the fact that we've reported this blocked popup, so we don't
      // show it again.
      browser.pageReport.reported = true;
    }
  },

  allowPopupsForSite: function allowPopupsForSite(aAllow) {
    let currentURI = BrowserApp.selectedBrowser.currentURI;
    Services.perms.add(currentURI, "popup", aAllow
                       ?  Ci.nsIPermissionManager.ALLOW_ACTION
                       :  Ci.nsIPermissionManager.DENY_ACTION);
    dump("Allowing popups for: " + currentURI);
  },

  showPopupsForSite: function showPopupsForSite() {
    let uri = BrowserApp.selectedBrowser.currentURI;
    let pageReport = BrowserApp.selectedBrowser.pageReport;
    if (pageReport) {
      for (let i = 0; i < pageReport.length; ++i) {
        let popupURIspec = pageReport[i].popupWindowURIspec;

        // Sometimes the popup URI that we get back from the pageReport
        // isn't useful (for instance, netscape.com's popup URI ends up
        // being "http://www.netscape.com", which isn't really the URI of
        // the popup they're trying to show).  This isn't going to be
        // useful to the user, so we won't create a menu item for it.
        if (popupURIspec == "" || popupURIspec == "about:blank" || popupURIspec == uri.spec)
          continue;

        let popupFeatures = pageReport[i].popupWindowFeatures;
        let popupName = pageReport[i].popupWindowName;

        let parent = BrowserApp.selectedTab;
        let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
        BrowserApp.addTab(popupURIspec, { parentId: parent.id, isPrivate: isPrivate });
      }
    }
  }
};


var IndexedDB = {
  _permissionsPrompt: "indexedDB-permissions-prompt",
  _permissionsResponse: "indexedDB-permissions-response",

  init: function IndexedDB_init() {
    Services.obs.addObserver(this, this._permissionsPrompt, false);
  },

  observe: function IndexedDB_observe(subject, topic, data) {
    if (topic != this._permissionsPrompt) {
      throw new Error("Unexpected topic!");
    }

    let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);

    let browser = requestor.getInterface(Ci.nsIDOMNode);
    let tab = BrowserApp.getTabForBrowser(browser);
    if (!tab)
      return;

    let host = browser.currentURI.asciiHost;

    let strings = Strings.browser;

    let message, responseTopic;
    if (topic == this._permissionsPrompt) {
      message = strings.formatStringFromName("offlineApps.ask", [host], 1);
      responseTopic = this._permissionsResponse;
    }

    const firstTimeoutDuration = 300000; // 5 minutes

    let timeoutId;

    let notificationID = responseTopic + host;
    let observer = requestor.getInterface(Ci.nsIObserver);

    // This will be set to the result of PopupNotifications.show() below, or to
    // the result of PopupNotifications.getNotification() if this is a
    // quotaCancel notification.
    let notification;

    function timeoutNotification() {
      // Remove the notification.
      NativeWindow.doorhanger.hide(notificationID, tab.id);

      // Clear all of our timeout stuff. We may be called directly, not just
      // when the timeout actually elapses.
      clearTimeout(timeoutId);

      // And tell the page that the popup timed out.
      observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
    }

    let buttons = [
    {
      label: strings.GetStringFromName("offlineApps.dontAllow2"),
      callback: function(aChecked) {
        clearTimeout(timeoutId);
        let action = aChecked ? Ci.nsIPermissionManager.DENY_ACTION : Ci.nsIPermissionManager.UNKNOWN_ACTION;
        observer.observe(null, responseTopic, action);
      }
    },
    {
      label: strings.GetStringFromName("offlineApps.allow"),
      callback: function() {
        clearTimeout(timeoutId);
        observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION);
      },
      positive: true
    }];

    let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
    NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);

    // Set the timeoutId after the popup has been created, and use the long
    // timeout value. If the user doesn't notice the popup after this amount of
    // time then it is most likely not visible and we want to alert the page.
    timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
  }
};

var CharacterEncoding = {
  _charsets: [],

  init: function init() {
    Services.obs.addObserver(this, "CharEncoding:Get", false);
    Services.obs.addObserver(this, "CharEncoding:Set", false);
    InitLater(() => this.sendState());
  },

  observe: function observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "CharEncoding:Get":
        this.getEncoding();
        break;
      case "CharEncoding:Set":
        this.setEncoding(aData);
        break;
    }
  },

  sendState: function sendState() {
    let showCharEncoding = "false";
    try {
      showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data;
    } catch (e) { /* Optional */ }

    Messaging.sendRequest({
      type: "CharEncoding:State",
      visible: showCharEncoding
    });
  },

  getEncoding: function getEncoding() {
    function infoToCharset(info) {
      return { code: info.value, title: info.label };
    }

    if (!this._charsets.length) {
      let data = CharsetMenu.getData();

      // In the desktop UI, the pinned charsets are shown above the rest.
      let pinnedCharsets = data.pinnedCharsets.map(infoToCharset);
      let otherCharsets = data.otherCharsets.map(infoToCharset)

      this._charsets = pinnedCharsets.concat(otherCharsets);
    }

    // Look for the index of the selected charset. Default to -1 if the
    // doc charset isn't found in the list of available charsets.
    let docCharset = BrowserApp.selectedBrowser.contentDocument.characterSet;
    let selected = -1;
    let charsetCount = this._charsets.length;

    for (let i = 0; i < charsetCount; i++) {
      if (this._charsets[i].code === docCharset) {
        selected = i;
        break;
      }
    }

    Messaging.sendRequest({
      type: "CharEncoding:Data",
      charsets: this._charsets,
      selected: selected
    });
  },

  setEncoding: function setEncoding(aEncoding) {
    let browser = BrowserApp.selectedBrowser;
    browser.docShell.gatherCharsetMenuTelemetry();
    browser.docShell.charset = aEncoding;
    browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
  }
};

var IdentityHandler = {
  // No trusted identity information. No site identity icon is shown.
  IDENTITY_MODE_UNKNOWN: "unknown",

  // Domain-Validation SSL CA-signed domain verification (DV).
  IDENTITY_MODE_IDENTIFIED: "identified",

  // Extended-Validation SSL CA-signed identity information (EV). A more rigorous validation process.
  IDENTITY_MODE_VERIFIED: "verified",

  // Part of the product's UI (built in about: pages)
  IDENTITY_MODE_CHROMEUI: "chromeUI",

  // The following mixed content modes are only used if "security.mixed_content.block_active_content"
  // is enabled. Our Java frontend coalesces them into one indicator.

  // No mixed content information. No mixed content icon is shown.
  MIXED_MODE_UNKNOWN: "unknown",

  // Blocked active mixed content.
  MIXED_MODE_CONTENT_BLOCKED: "blocked",

  // Loaded active mixed content.
  MIXED_MODE_CONTENT_LOADED: "loaded",

  // The following tracking content modes are only used if tracking protection
  // is enabled. Our Java frontend coalesces them into one indicator.

  // No tracking content information. No tracking content icon is shown.
  TRACKING_MODE_UNKNOWN: "unknown",

  // Blocked active tracking content. Shield icon is shown, with a popup option to load content.
  TRACKING_MODE_CONTENT_BLOCKED: "tracking_content_blocked",

  // Loaded active tracking content. Yellow triangle icon is shown.
  TRACKING_MODE_CONTENT_LOADED: "tracking_content_loaded",

  // Cache the most recent SSLStatus and Location seen in getIdentityStrings
  _lastStatus : null,
  _lastLocation : null,

  /**
   * Helper to parse out the important parts of _lastStatus (of the SSL cert in
   * particular) for use in constructing identity UI strings
  */
  getIdentityData : function() {
    let result = {};
    let status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus);
    let cert = status.serverCert;

    // Human readable name of Subject
    result.subjectOrg = cert.organization;

    // SubjectName fields, broken up for individual access
    if (cert.subjectName) {
      result.subjectNameFields = {};
      cert.subjectName.split(",").forEach(function(v) {
        let field = v.split("=");
        this[field[0]] = field[1];
      }, result.subjectNameFields);

      // Call out city, state, and country specifically
      result.city = result.subjectNameFields.L;
      result.state = result.subjectNameFields.ST;
      result.country = result.subjectNameFields.C;
    }

    // Human readable name of Certificate Authority
    result.caOrg =  cert.issuerOrganization || cert.issuerCommonName;
    result.cert = cert;

    return result;
  },

  /**
   * Determines the identity mode corresponding to the icon we show in the urlbar.
   */
  getIdentityMode: function getIdentityMode(aState, uri) {
    if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
      return this.IDENTITY_MODE_VERIFIED;
    }

    if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE) {
      return this.IDENTITY_MODE_IDENTIFIED;
    }

    // We also allow "about:" by allowing the selector to be empty (i.e. '(|.....|...|...)'
    let whitelist = /^about:($|about|accounts|addons|buildconfig|cache|config|crashes|devices|downloads|fennec|firefox|feedback|healthreport|home|license|logins|logo|memory|mozilla|networking|plugins|privatebrowsing|rights|serviceworkers|support|telemetry|webrtc)($|\?)/i;
    if (uri.schemeIs("about") && whitelist.test(uri.spec)) {
        return this.IDENTITY_MODE_CHROMEUI;
    }

    return this.IDENTITY_MODE_UNKNOWN;
  },

  getMixedDisplayMode: function getMixedDisplayMode(aState) {
    if (aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT) {
        return this.MIXED_MODE_CONTENT_LOADED;
    }

    if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_DISPLAY_CONTENT) {
        return this.MIXED_MODE_CONTENT_BLOCKED;
    }

    return this.MIXED_MODE_UNKNOWN;
  },

  getMixedActiveMode: function getActiveDisplayMode(aState) {
    // Only show an indicator for loaded mixed content if the pref to block it is enabled
    if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) &&
         !Services.prefs.getBoolPref("security.mixed_content.block_active_content")) {
      return this.MIXED_MODE_CONTENT_LOADED;
    }

    if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT) {
      return this.MIXED_MODE_CONTENT_BLOCKED;
    }

    return this.MIXED_MODE_UNKNOWN;
  },

  getTrackingMode: function getTrackingMode(aState, aBrowser) {
    if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_TRACKING_CONTENT) {
      this.shieldHistogramAdd(aBrowser, 2);
      return this.TRACKING_MODE_CONTENT_BLOCKED;
    }

    // Only show an indicator for loaded tracking content if the pref to block it is enabled
    let tpEnabled = Services.prefs.getBoolPref("privacy.trackingprotection.enabled") ||
                    (Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled") &&
                     PrivateBrowsingUtils.isBrowserPrivate(aBrowser));

    if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_TRACKING_CONTENT) && tpEnabled) {
      this.shieldHistogramAdd(aBrowser, 1);
      return this.TRACKING_MODE_CONTENT_LOADED;
    }

    this.shieldHistogramAdd(aBrowser, 0);
    return this.TRACKING_MODE_UNKNOWN;
  },

  shieldHistogramAdd: function(browser, value) {
    if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
      return;
    }
    Telemetry.addData("TRACKING_PROTECTION_SHIELD", value);
  },

  /**
   * Determine the identity of the page being displayed by examining its SSL cert
   * (if available). Return the data needed to update the UI.
   */
  checkIdentity: function checkIdentity(aState, aBrowser) {
    this._lastStatus = aBrowser.securityUI
                               .QueryInterface(Components.interfaces.nsISSLStatusProvider)
                               .SSLStatus;

    // Don't pass in the actual location object, since it can cause us to
    // hold on to the window object too long.  Just pass in the fields we
    // care about. (bug 424829)
    let locationObj = {};
    try {
      let location = aBrowser.contentWindow.location;
      locationObj.host = location.host;
      locationObj.hostname = location.hostname;
      locationObj.port = location.port;
      locationObj.origin = location.origin;
    } catch (ex) {
      // Can sometimes throw if the URL being visited has no host/hostname,
      // e.g. about:blank. The _state for these pages means we won't need these
      // properties anyways, though.
    }
    this._lastLocation = locationObj;

    let uri = aBrowser.currentURI;
    try {
      uri = Services.uriFixup.createExposableURI(uri);
    } catch (e) {}

    let identityMode = this.getIdentityMode(aState, uri);
    let mixedDisplay = this.getMixedDisplayMode(aState);
    let mixedActive = this.getMixedActiveMode(aState);
    let trackingMode = this.getTrackingMode(aState, aBrowser);
    let result = {
      origin: locationObj.origin,
      mode: {
        identity: identityMode,
        mixed_display: mixedDisplay,
        mixed_active: mixedActive,
        tracking: trackingMode
      }
    };

    // Don't show identity data for pages with an unknown identity or if any
    // mixed content is loaded (mixed display content is loaded by default).
    // We also return for CHROMEUI pages since they don't have any certificate
    // information to load either. result.secure specifically refers to connection
    // security, which is irrelevant for about: pages, as they're loaded locally.
    if (identityMode == this.IDENTITY_MODE_UNKNOWN ||
        identityMode == this.IDENTITY_MODE_CHROMEUI ||
        aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN) {
      result.secure = false;
      return result;
    }

    result.secure = true;

    result.host = this.getEffectiveHost();

    let iData = this.getIdentityData();
    result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);

    // If the cert is identified, then we can populate the results with credentials
    if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
      result.owner = iData.subjectOrg;

      // Build an appropriate supplemental block out of whatever location data we have
      let supplemental = "";
      if (iData.city) {
        supplemental += iData.city + "\n";
      }
      if (iData.state && iData.country) {
        supplemental += Strings.browser.formatStringFromName("identity.identified.state_and_country", [iData.state, iData.country], 2);
        result.country = iData.country;
      } else if (iData.state) { // State only
        supplemental += iData.state;
      } else if (iData.country) { // Country only
        supplemental += iData.country;
        result.country = iData.country;
      }
      result.supplemental = supplemental;

      return result;
    }

    // Cache the override service the first time we need to check it
    if (!this._overrideService)
      this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);

    // Check whether this site is a security exception. XPConnect does the right
    // thing here in terms of converting _lastLocation.port from string to int, but
    // the overrideService doesn't like undefined ports, so make sure we have
    // something in the default case (bug 432241).
    // .hostname can return an empty string in some exceptional cases -
    // hasMatchingOverride does not handle that, so avoid calling it.
    // Updating the tooltip value in those cases isn't critical.
    // FIXME: Fixing bug 646690 would probably makes this check unnecessary
    if (this._lastLocation.hostname &&
        this._overrideService.hasMatchingOverride(this._lastLocation.hostname,
                                                  (this._lastLocation.port || 443),
                                                  iData.cert, {}, {}))
      result.verifier = Strings.browser.GetStringFromName("identity.identified.verified_by_you");

    return result;
  },

  /**
   * Attempt to provide proper IDN treatment for host names
   */
  getEffectiveHost: function getEffectiveHost() {
    if (!this._IDNService)
      this._IDNService = Cc["@mozilla.org/network/idn-service;1"]
                         .getService(Ci.nsIIDNService);
    try {
      return this._IDNService.convertToDisplayIDN(this._uri.host, {});
    } catch (e) {
      // If something goes wrong (e.g. hostname is an IP address) just fail back
      // to the full domain.
      return this._lastLocation.hostname;
    }
  }
};

var SearchEngines = {
  _contextMenuId: null,
  PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
  PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",

  // Shared preference key used for search activity default engine.
  PREF_SEARCH_ACTIVITY_ENGINE_KEY: "search.engines.defaultname",

  init: function init() {
    Services.obs.addObserver(this, "SearchEngines:Add", false);
    Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
    Services.obs.addObserver(this, "SearchEngines:Remove", false);
    Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false);
    Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
    Services.obs.addObserver(this, "browser-search-engine-modified", false);
  },

  // Fetch list of search engines. all ? All engines : Visible engines only.
  _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) {
    if (!Components.isSuccessCode(rv)) {
      Cu.reportError("Could not initialize search service, bailing out.");
      return;
    }

    let engineData = Services.search.getVisibleEngines({});

    // Our Java UI assumes that the default engine is the first item in the array,
    // so we need to make sure that's the case.
    if (engineData[0] !== Services.search.defaultEngine) {
      engineData = engineData.filter(engine => engine !== Services.search.defaultEngine);
      engineData.unshift(Services.search.defaultEngine);
    }

    let searchEngines = engineData.map(function (engine) {
      return {
        name: engine.name,
        identifier: engine.identifier,
        iconURI: (engine.iconURI ? engine.iconURI.spec : null),
        hidden: engine.hidden
      };
    });

    let suggestTemplate = null;
    let suggestEngine = null;

    // Check to see if the default engine supports search suggestions. We only need to check
    // the default engine because we only show suggestions for the default engine in the UI.
    let engine = Services.search.defaultEngine;
    if (engine.supportsResponseType("application/x-suggestions+json")) {
      suggestEngine = engine.name;
      suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec;
    }

    // By convention, the currently configured default engine is at position zero in searchEngines.
    Messaging.sendRequest({
      type: "SearchEngines:Data",
      searchEngines: searchEngines,
      suggest: {
        engine: suggestEngine,
        template: suggestTemplate,
        enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED),
        prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
      }
    });

    // Send a speculative connection to the default engine.
    Services.search.defaultEngine.speculativeConnect({window: window});
  },

  // Helper method to extract the engine name from a JSON. Simplifies the observe function.
  _extractEngineFromJSON: function _extractEngineFromJSON(aData) {
    let data = JSON.parse(aData);
    return Services.search.getEngineByName(data.engine);
  },

  observe: function observe(aSubject, aTopic, aData) {
    let engine;
    switch(aTopic) {
      case "SearchEngines:Add":
        this.displaySearchEnginesList(aData);
        break;
      case "SearchEngines:GetVisible":
        Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
        break;
      case "SearchEngines:Remove":
        // Make sure the engine isn't hidden before removing it, to make sure it's
        // visible if the user later re-adds it (works around bug 341833)
        engine = this._extractEngineFromJSON(aData);
        engine.hidden = false;
        Services.search.removeEngine(engine);
        break;
      case "SearchEngines:RestoreDefaults":
        // Un-hides all default engines.
        Services.search.restoreDefaultEngines();
        break;
      case "SearchEngines:SetDefault":
        engine = this._extractEngineFromJSON(aData);
        // Move the new default search engine to the top of the search engine list.
        Services.search.moveEngine(engine, 0);
        Services.search.defaultEngine = engine;
        break;
      case "browser-search-engine-modified":
        if (aData == "engine-default") {
          this._setSearchActivityDefaultPref(aSubject.QueryInterface(Ci.nsISearchEngine));
        }
        break;
      default:
        dump("Unexpected message type observed: " + aTopic);
        break;
    }
  },

  migrateSearchActivityDefaultPref: function migrateSearchActivityDefaultPref() {
    Services.search.init(() => this._setSearchActivityDefaultPref(Services.search.defaultEngine));
  },

  // Updates the search activity pref when the default engine changes.
  _setSearchActivityDefaultPref: function _setSearchActivityDefaultPref(engine) {
    SharedPreferences.forApp().setCharPref(this.PREF_SEARCH_ACTIVITY_ENGINE_KEY, engine.name);
  },

  // Display context menu listing names of the search engines available to be added.
  displaySearchEnginesList: function displaySearchEnginesList(aData) {
    let data = JSON.parse(aData);
    let tab = BrowserApp.getTabForId(data.tabId);

    if (!tab)
      return;

    let browser = tab.browser;
    let engines = browser.engines;

    let p = new Prompt({
      window: browser.contentWindow
    }).setSingleChoiceItems(engines.map(function(e) {
      return { label: e.title };
    })).show((function(data) {
      if (data.button == -1)
        return;

      this.addOpenSearchEngine(engines[data.button]);
      engines.splice(data.button, 1);

      if (engines.length < 1) {
        // Broadcast message that there are no more add-able search engines.
        let newEngineMessage = {
          type: "Link:OpenSearch",
          tabID: tab.id,
          visible: false
        };

        Messaging.sendRequest(newEngineMessage);
      }
    }).bind(this));
  },

  addOpenSearchEngine: function addOpenSearchEngine(engine) {
    Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, {
      onSuccess: function() {
        // Display a toast confirming addition of new search engine.
        Snackbars.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), Snackbars.LENGTH_LONG);
      },

      onError: function(aCode) {
        let errorMessage;
        if (aCode == 2) {
          // Engine is a duplicate.
          errorMessage = "alertSearchEngineDuplicateToast";

        } else {
          // Unknown failure. Display general error message.
          errorMessage = "alertSearchEngineErrorToast";
        }

        Snackbars.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), Snackbars.LENGTH_LONG);
      }
    });
  },

  /**
   * Build and return an array of sorted form data / Query Parameters
   * for an element in a submission form.
   *
   * @param element
   *        A valid submission element of a form.
   */
  _getSortedFormData: function(element) {
    let formData = [];

    for (let formElement of element.form.elements) {
      if (!formElement.type) {
        continue;
      }

      // Make this text field a generic search parameter.
      if (element == formElement) {
        formData.push({ name: formElement.name, value: "{searchTerms}" });
        continue;
      }

      // Add other form elements as parameters.
      switch (formElement.type.toLowerCase()) {
        case "checkbox":
        case "radio":
          if (!formElement.checked) {
            break;
          }
        case "text":
        case "hidden":
        case "textarea":
          formData.push({ name: escape(formElement.name), value: escape(formElement.value) });
          break;

        case "select-one": {
          for (let option of formElement.options) {
            if (option.selected) {
              formData.push({ name: escape(formElement.name), value: escape(formElement.value) });
              break;
            }
          }
        }
      }
    };

    // Return valid, pre-sorted queryParams.
    return formData.filter(a => a.name && a.value).sort((a, b) => {
      // nsIBrowserSearchService.hasEngineWithURL() ensures sort, but this helps.
      if (a.name > b.name) {
        return 1;
      }
      if (b.name > a.name) {
        return -1;
      }

      if (a.value > b.value) {
        return 1;
      }
      if (b.value > a.value) {
        return -1;
      }

      return 0;
    });
  },

  /**
   * Check if any search engines already handle an EngineURL of type
   * URLTYPE_SEARCH_HTML, matching this request-method, formURL, and queryParams.
   */
  visibleEngineExists: function(element) {
    let formData = this._getSortedFormData(element);

    let form = element.form;
    let method = form.method.toUpperCase();

    let charset = element.ownerDocument.characterSet;
    let docURI = Services.io.newURI(element.ownerDocument.URL, charset, null);
    let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;

    return Services.search.hasEngineWithURL(method, formURL, formData);
  },

  /**
   * Adds a new search engine to the BrowserSearchService, based on its provided element. Prompts for an engine
   * name, and appends a simple version-number in case of collision with an existing name.
   *
   * @return callback to handle success value. Currently used for ActionBarHandler.js and UI updates.
   */
  addEngine: function addEngine(aElement, resultCallback) {
    let form = aElement.form;
    let charset = aElement.ownerDocument.characterSet;
    let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null);
    let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
    let method = form.method.toUpperCase();
    let formData = this._getSortedFormData(aElement);

    // prompt user for name of search engine
    let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine3");
    let title = { value: (aElement.ownerDocument.title || docURI.host) };
    if (!Services.prompt.prompt(null, promptTitle, null, title, null, {})) {
      if (resultCallback) {
        resultCallback(false);
      };
      return;
    }

    // fetch the favicon for this page
    let dbFile = FileUtils.getFile("ProfD", ["browser.db"]);
    let mDBConn = Services.storage.openDatabase(dbFile);
    let stmts = [];
    stmts[0] = mDBConn.createStatement("SELECT favicon FROM history_with_favicons WHERE url = ?");
    stmts[0].bindByIndex(0, docURI.spec);
    let favicon = null;

    Services.search.init(function addEngine_cb(rv) {
      if (!Components.isSuccessCode(rv)) {
        Cu.reportError("Could not initialize search service, bailing out.");
        if (resultCallback) {
          resultCallback(false);
        };
        return;
      }

      mDBConn.executeAsync(stmts, stmts.length, {
        handleResult: function (results) {
          let bytes = results.getNextRow().getResultByName("favicon");
          if (bytes && bytes.length) {
            favicon = "data:image/x-icon;base64," + btoa(String.fromCharCode.apply(null, bytes));
          }
        },
        handleCompletion: function (reason) {
          // if there's already an engine with this name, add a number to
          // make the name unique (e.g., "Google" becomes "Google 2")
          let name = title.value;
          for (let i = 2; Services.search.getEngineByName(name); i++)
            name = title.value + " " + i;

          Services.search.addEngineWithDetails(name, favicon, null, null, method, formURL);
          Snackbars.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [name], 1), Snackbars.LENGTH_LONG);

          let engine = Services.search.getEngineByName(name);
          engine.wrappedJSObject._queryCharset = charset;
          formData.forEach(param => { engine.addParam(param.name, param.value, null); });

          if (resultCallback) {
            return resultCallback(true);
          };
        }
      });
    });
  }
};

var ActivityObserver = {
  init: function ao_init() {
    Services.obs.addObserver(this, "application-background", false);
    Services.obs.addObserver(this, "application-foreground", false);
  },

  observe: function ao_observe(aSubject, aTopic, aData) {
    let isForeground = false;
    let tab = BrowserApp.selectedTab;

    UITelemetry.addEvent("show.1", "system", null, aTopic);

    switch (aTopic) {
      case "application-background" :
        let doc = (tab ? tab.browser.contentDocument : null);
        if (doc && doc.fullscreenElement) {
          doc.exitFullscreen();
        }
        isForeground = false;
        break;
      case "application-foreground" :
        isForeground = true;
        break;
    }

    if (tab && tab.getActive() != isForeground) {
      tab.setActive(isForeground);
    }
  }
};

var Telemetry = {
  addData: function addData(aHistogramId, aValue) {
    let histogram = Services.telemetry.getHistogramById(aHistogramId);
    histogram.add(aValue);
  },
};

var Experiments = {
  // Enable malware download protection (bug 936041)
  MALWARE_DOWNLOAD_PROTECTION: "malware-download-protection",

  // Try to load pages from disk cache when network is offline (bug 935190)
  OFFLINE_CACHE: "offline-cache",

  init() {
    Messaging.sendRequestForResult({
      type: "Experiments:GetActive"
    }).then(experiments => {
      let names = JSON.parse(experiments);
      for (let name of names) {
        switch (name) {
          case this.MALWARE_DOWNLOAD_PROTECTION: {
            // Apply experiment preferences on the default branch. This allows
            // us to avoid migrating user prefs when experiments are enabled/disabled,
            // and it also allows users to override these prefs in about:config.
            let defaults = Services.prefs.getDefaultBranch(null);
            defaults.setBoolPref("browser.safebrowsing.downloads.enabled", true);
            defaults.setBoolPref("browser.safebrowsing.downloads.remote.enabled", true);
            continue;
          }

          case this.OFFLINE_CACHE: {
            let defaults = Services.prefs.getDefaultBranch(null);
            defaults.setBoolPref("browser.tabs.useCache", true);
            continue;
          }
        }
      }
    });
  },

  setOverride(name, isEnabled) {
    Messaging.sendRequest({
      type: "Experiments:SetOverride",
      name: name,
      isEnabled: isEnabled
    });
  },

  clearOverride(name) {
    Messaging.sendRequest({
      type: "Experiments:ClearOverride",
      name: name
    });
  }
};

var ExternalApps = {
  _contextMenuId: null,

  // extend _getLink to pickup html5 media links.
  _getMediaLink: function(aElement) {
    let uri = NativeWindow.contextmenus._getLink(aElement);
    if (uri == null && aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement)) {
      try {
        let mediaSrc = aElement.currentSrc || aElement.src;
        uri = ContentAreaUtils.makeURI(mediaSrc, null, null);
      } catch (e) {}
    }
    return uri;
  },

  init: function helper_init() {
    this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) {
      let uri = null;
      var node = aElement;
      while (node && !uri) {
        uri = ExternalApps._getMediaLink(node);
        node = node.parentNode;
      }
      let apps = [];
      if (uri)
        apps = HelperApps.getAppsForUri(uri);

      return apps.length == 1 ? Strings.browser.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1) :
                                Strings.browser.GetStringFromName("helperapps.openWithList2");
    }, this.filter, this.openExternal);
  },

  filter: {
    matches: function(aElement) {
      let uri = ExternalApps._getMediaLink(aElement);
      let apps = [];
      if (uri) {
        apps = HelperApps.getAppsForUri(uri);
      }
      return apps.length > 0;
    }
  },

  openExternal: function(aElement) {
    if (aElement.pause) {
      aElement.pause();
    }
    let uri = ExternalApps._getMediaLink(aElement);
    HelperApps.launchUri(uri);
  },

  shouldCheckUri: function(uri) {
    if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
      return false;
    }

    return true;
  },

  updatePageAction: function updatePageAction(uri, contentDocument) {
    HelperApps.getAppsForUri(uri, { filterHttp: true }, (apps) => {
      this.clearPageAction();
      if (apps.length > 0)
        this._setUriForPageAction(uri, apps, contentDocument);
    });
  },

  updatePageActionUri: function updatePageActionUri(uri) {
    this._pageActionUri = uri;
  },

  _getMediaContentElement(contentDocument) {
    if (!contentDocument.contentType.startsWith("video/") &&
        !contentDocument.contentType.startsWith("audio/")) {
      return null;
    }

    let element = contentDocument.activeElement;

    if (element instanceof HTMLBodyElement) {
      element = element.firstChild;
    }

    if (element instanceof HTMLMediaElement) {
      return element;
    }

    return null;
  },

  _setUriForPageAction: function setUriForPageAction(uri, apps, contentDocument) {
    this.updatePageActionUri(uri);

    // If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
    if (this._pageActionId != undefined)
      return;

    let mediaElement = this._getMediaContentElement(contentDocument);

    this._pageActionId = PageActions.add({
      title: Strings.browser.GetStringFromName("openInApp.pageAction"),
      icon: "drawable://icon_openinapp",

      clickCallback: () => {
        UITelemetry.addEvent("launch.1", "pageaction", null, "helper");

        let wasPlaying = mediaElement && !mediaElement.paused && !mediaElement.ended;
        if (wasPlaying) {
          mediaElement.pause();
        }

        if (apps.length > 1) {
          // Use the HelperApps prompt here to filter out any Http handlers
          HelperApps.prompt(apps, {
            title: Strings.browser.GetStringFromName("openInApp.pageAction"),
            buttons: [
              Strings.browser.GetStringFromName("openInApp.ok"),
              Strings.browser.GetStringFromName("openInApp.cancel")
            ]
          }, (result) => {
            if (result.button != 0) {
              if (wasPlaying) {
                mediaElement.play();
              }

              return;
            }
            apps[result.icongrid0].launch(this._pageActionUri);
          });
        } else {
          apps[0].launch(this._pageActionUri);
        }
      }
    });
  },

  clearPageAction: function clearPageAction() {
    if(!this._pageActionId)
      return;

    PageActions.remove(this._pageActionId);
    delete this._pageActionId;
  },
};

var Distribution = {
  // File used to store campaign data
  _file: null,

  _preferencesJSON: null,

  init: function dc_init() {
    Services.obs.addObserver(this, "Distribution:Changed", false);
    Services.obs.addObserver(this, "Distribution:Set", false);
    Services.obs.addObserver(this, "prefservice:after-app-defaults", false);
    Services.obs.addObserver(this, "Campaign:Set", false);

    // Look for file outside the APK:
    // /data/data/org.mozilla.xxx/distribution.json
    this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
    this._file.append("distribution.json");
    this.readJSON(this._file, this.update);
  },

  observe: function dc_observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "Distribution:Changed":
        // Re-init the search service.
        try {
          Services.search._asyncReInit();
        } catch (e) {
          console.log("Unable to reinit search service.");
        }
        // Fall through.

      case "Distribution:Set":
        if (aData) {
          try {
            this._preferencesJSON = JSON.parse(aData);
          } catch (e) {
            console.log("Invalid distribution JSON.");
          }
        }
        // Reload the default prefs so we can observe "prefservice:after-app-defaults"
        Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
        this.installDistroAddons();
        break;

      case "prefservice:after-app-defaults":
        this.getPrefs();
        break;

      case "Campaign:Set": {
        // Update the prefs for this session
        try {
          this.update(JSON.parse(aData));
        } catch (ex) {
          Cu.reportError("Distribution: Could not parse JSON: " + ex);
          return;
        }

        // Asynchronously copy the data to the file.
        let array = new TextEncoder().encode(aData);
        OS.File.writeAtomic(this._file.path, array, { tmpPath: this._file.path + ".tmp" });
        break;
      }
    }
  },

  update: function dc_update(aData) {
    // Force the distribution preferences on the default branch
    let defaults = Services.prefs.getDefaultBranch(null);
    defaults.setCharPref("distribution.id", aData.id);
    defaults.setCharPref("distribution.version", aData.version);
  },

  getPrefs: function dc_getPrefs() {
    if (this._preferencesJSON) {
        this.applyPrefs(this._preferencesJSON);
        this._preferencesJSON = null;
        return;
    }

    // Get the distribution directory, and bail if it doesn't exist.
    let file = FileUtils.getDir("XREAppDist", [], false);
    if (!file.exists())
      return;

    file.append("preferences.json");
    this.readJSON(file, this.applyPrefs);
  },

  applyPrefs: function dc_applyPrefs(aData) {
    // Check for required Global preferences
    let global = aData["Global"];
    if (!(global && global["id"] && global["version"] && global["about"])) {
      Cu.reportError("Distribution: missing or incomplete Global preferences");
      return;
    }

    // Force the distribution preferences on the default branch
    let defaults = Services.prefs.getDefaultBranch(null);
    defaults.setCharPref("distribution.id", global["id"]);
    defaults.setCharPref("distribution.version", global["version"]);

    let locale = BrowserApp.getUALocalePref();
    let aboutString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
    aboutString.data = global["about." + locale] || global["about"];
    defaults.setComplexValue("distribution.about", Ci.nsISupportsString, aboutString);

    let prefs = aData["Preferences"];
    for (let key in prefs) {
      try {
        let value = prefs[key];
        switch (typeof value) {
          case "boolean":
            defaults.setBoolPref(key, value);
            break;
          case "number":
            defaults.setIntPref(key, value);
            break;
          case "string":
          case "undefined":
            defaults.setCharPref(key, value);
            break;
        }
      } catch (e) { /* ignore bad prefs and move on */ }
    }

    // Apply a lightweight theme if necessary
    if (prefs && prefs["lightweightThemes.selectedThemeID"]) {
      Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
    }

    let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
    let localizeablePrefs = aData["LocalizablePreferences"];
    for (let key in localizeablePrefs) {
      try {
        let value = localizeablePrefs[key];
        value = value.replace(/%LOCALE%/g, locale);
        localizedString.data = "data:text/plain," + key + "=" + value;
        defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
      } catch (e) { /* ignore bad prefs and move on */ }
    }

    let localizeablePrefsOverrides = aData["LocalizablePreferences." + locale];
    for (let key in localizeablePrefsOverrides) {
      try {
        let value = localizeablePrefsOverrides[key];
        localizedString.data = "data:text/plain," + key + "=" + value;
        defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
      } catch (e) { /* ignore bad prefs and move on */ }
    }

    Messaging.sendRequest({ type: "Distribution:Set:OK" });
  },

  // aFile is an nsIFile
  // aCallback takes the parsed JSON object as a parameter
  readJSON: function dc_readJSON(aFile, aCallback) {
    Task.spawn(function() {
      let bytes = yield OS.File.read(aFile.path);
      let raw = new TextDecoder().decode(bytes) || "";

      try {
        aCallback(JSON.parse(raw));
      } catch (e) {
        Cu.reportError("Distribution: Could not parse JSON: " + e);
      }
    }).then(null, function onError(reason) {
      if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
        Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file");
      }
    });
  },

  // Track pending installs so we can avoid showing notifications for them.
  pendingAddonInstalls: new Set(),

  installDistroAddons: Task.async(function* () {
    const PREF_ADDONS_INSTALLED = "distribution.addonsInstalled";
    try {
      let installed = Services.prefs.getBoolPref(PREF_ADDONS_INSTALLED);
      if (installed) {
        return;
      }
    } catch (e) {
      Services.prefs.setBoolPref(PREF_ADDONS_INSTALLED, true);
    }

    let distroPath;
    try {
      distroPath = FileUtils.getDir("XREAppDist", ["extensions"]).path;

      let info = yield OS.File.stat(distroPath);
      if (!info.isDir) {
        return;
      }
    } catch (e) {
      return;
    }

    let it = new OS.File.DirectoryIterator(distroPath);
    try {
      yield it.forEach(entry => {
        // Only support extensions that are zipped in .xpi files.
        if (entry.isDir || !entry.name.endsWith(".xpi")) {
          dump("Ignoring distribution add-on that isn't an XPI: " + entry.path);
          return;
        }

        new Promise((resolve, reject) => {
          AddonManager.getInstallForFile(new FileUtils.File(entry.path), resolve);
        }).then(install => {
          let id = entry.name.substring(0, entry.name.length - 4);
          if (install.addon.id !== id) {
            Cu.reportError("File entry " + entry.path + " contains an add-on with an incorrect ID");
            return;
          }
          this.pendingAddonInstalls.add(install);
          install.install();
        }).catch(e => {
          Cu.reportError("Error installing distribution add-on: " + entry.path + ": " + e);
        });
      });
    } finally {
      it.close();
    }
  })
};

var Tabs = {
  _enableTabExpiration: false,
  _useCache: false,
  _domains: new Set(),

  init: function() {
    // On low-memory platforms, always allow tab expiration. On high-mem
    // platforms, allow it to be turned on once we hit a low-mem situation.
    if (BrowserApp.isOnLowMemoryPlatform) {
      this._enableTabExpiration = true;
    } else {
      Services.obs.addObserver(this, "memory-pressure", false);
    }

    // Watch for opportunities to pre-connect to high probability targets.
    Services.obs.addObserver(this, "Session:Prefetch", false);

    // Track the network connection so we can efficiently use the cache
    // for possible offline rendering.
    Services.obs.addObserver(this, "network:link-status-changed", false);
    let network = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
    this.useCache = !network.isLinkUp;

    BrowserApp.deck.addEventListener("pageshow", this, false);
    BrowserApp.deck.addEventListener("TabOpen", this, false);
  },

  observe: function(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "memory-pressure":
        if (aData != "heap-minimize") {
          // We received a low-memory related notification. This will enable
          // expirations.
          this._enableTabExpiration = true;
          Services.obs.removeObserver(this, "memory-pressure");
        } else {
          // Use "heap-minimize" as a trigger to expire the most stale tab.
          this.expireLruTab();
        }
        break;
      case "Session:Prefetch":
        if (aData) {
          try {
            let uri = Services.io.newURI(aData, null, null);
            if (uri && !this._domains.has(uri.host)) {
              Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
              this._domains.add(uri.host);
            }
          } catch (e) {}
        }
        break;
      case "network:link-status-changed":
        if (["down", "unknown", "up"].indexOf(aData) == -1) {
          return;
        }
        this.useCache = (aData === "down");
        break;
    }
  },

  handleEvent: function(aEvent) {
    switch (aEvent.type) {
      case "pageshow":
        // Clear the domain cache whenever a page is loaded into any browser.
        this._domains.clear();

        break;
      case "TabOpen":
        // Use opening a new tab as a trigger to expire the most stale tab.
        this.expireLruTab();
        break;
    }
  },

  // Manage the most-recently-used list of tabs. Each tab has a timestamp
  // associated with it that indicates when it was last touched.
  expireLruTab: function() {
    if (!this._enableTabExpiration) {
      return false;
    }
    let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000;
    if (expireTimeMs < 0) {
      // This behaviour is disabled.
      return false;
    }
    let tabs = BrowserApp.tabs;
    let selected = BrowserApp.selectedTab;
    let lruTab = null;
    // Find the least recently used non-zombie tab.
    for (let i = 0; i < tabs.length; i++) {
      if (tabs[i] == selected ||
          tabs[i].browser.__SS_restore ||
          tabs[i].playingAudio) {
        // This tab is selected, is already a zombie, or is currently playing
        // audio, skip it.
        continue;
      }
      if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) {
        lruTab = tabs[i];
      }
    }
    // If the tab was last touched more than browser.tabs.expireTime seconds ago,
    // zombify it.
    if (lruTab) {
      if (Date.now() - lruTab.lastTouchedAt > expireTimeMs) {
        MemoryObserver.zombify(lruTab);
        return true;
      }
    }
    return false;
  },

  get useCache() {
    if (!Services.prefs.getBoolPref("browser.tabs.useCache")) {
      return false;
    }
    return this._useCache;
  },

  set useCache(aUseCache) {
    if (!Services.prefs.getBoolPref("browser.tabs.useCache")) {
      return;
    }

    if (this._useCache == aUseCache) {
      return;
    }

    BrowserApp.tabs.forEach(function(tab) {
      if (tab.browser && tab.browser.docShell) {
        if (aUseCache) {
          tab.browser.docShell.defaultLoadFlags |= Ci.nsIRequest.LOAD_FROM_CACHE;
        } else {
          tab.browser.docShell.defaultLoadFlags &= ~Ci.nsIRequest.LOAD_FROM_CACHE;
        }
      }
    });
    this._useCache = aUseCache;
  },

  // For debugging
  dump: function(aPrefix) {
    let tabs = BrowserApp.tabs;
    for (let i = 0; i < tabs.length; i++) {
      dump(aPrefix + " | " + "Tab [" + tabs[i].browser.contentWindow.location.href + "]: lastTouchedAt:" + tabs[i].lastTouchedAt + ", zombie:" + tabs[i].browser.__SS_restore);
    }
  },
};

function ContextMenuItem(args) {
  this.id = uuidgen.generateUUID().toString();
  this.args = args;
}

ContextMenuItem.prototype = {
  get order() {
    return this.args.order || 0;
  },

  matches: function(elt, x, y) {
    return this.args.selector.matches(elt, x, y);
  },

  callback: function(elt) {
    this.args.callback(elt);
  },

  addVal: function(name, elt, defaultValue) {
    if (!(name in this.args))
      return defaultValue;

    if (typeof this.args[name] == "function")
      return this.args[name](elt);

    return this.args[name];
  },

  getValue: function(elt) {
    return {
      id: this.id,
      label: this.addVal("label", elt),
      showAsActions: this.addVal("showAsActions", elt),
      icon: this.addVal("icon", elt),
      isGroup: this.addVal("isGroup", elt, false),
      inGroup: this.addVal("inGroup", elt, false),
      disabled: this.addVal("disabled", elt, false),
      selected: this.addVal("selected", elt, false),
      isParent: this.addVal("isParent", elt, false),
    };
  }
}

function HTMLContextMenuItem(elt, target) {
  ContextMenuItem.call(this, { });

  this.menuElementRef = Cu.getWeakReference(elt);
  this.targetElementRef = Cu.getWeakReference(target);
}

HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
  order: {
    value: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER
  },

  matches: {
    value: function(target) {
      let t = this.targetElementRef.get();
      return t === target;
    },
  },

  callback: {
    value: function(target) {
      let elt = this.menuElementRef.get();
      if (!elt) {
        return;
      }

      // If this is a menu item, show a new context menu with the submenu in it
      if (elt instanceof Ci.nsIDOMHTMLMenuElement) {
        try {
          NativeWindow.contextmenus.menus = {};

          let elt = this.menuElementRef.get();
          let target = this.targetElementRef.get();
          if (!elt) {
            return;
          }

          var items = NativeWindow.contextmenus._getHTMLContextMenuItemsForMenu(elt, target);
          // This menu will always only have one context, but we still make sure its the "right" one.
          var context = NativeWindow.contextmenus._getContextType(target);
          if (items.length > 0) {
            NativeWindow.contextmenus._addMenuItems(items, context);
          }

        } catch(ex) {
          Cu.reportError(ex);
        }
      } else {
        // otherwise just click the menu item
        elt.click();
      }
    },
  },

  getValue: {
    value: function(target) {
      let elt = this.menuElementRef.get();
      if (!elt) {
        return null;
      }

      if (elt.hasAttribute("hidden")) {
        return null;
      }

      return {
        id: this.id,
        icon: elt.icon,
        label: elt.label,
        disabled: elt.disabled,
        menu: elt instanceof Ci.nsIDOMHTMLMenuElement
      };
    }
  },
});