mobile/android/chrome/content/browser.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/chrome/content/browser.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,8558 @@
     1.4 +#filter substitution
     1.5 +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +"use strict";
    1.10 +
    1.11 +let Cc = Components.classes;
    1.12 +let Ci = Components.interfaces;
    1.13 +let Cu = Components.utils;
    1.14 +let Cr = Components.results;
    1.15 +
    1.16 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.17 +Cu.import("resource://gre/modules/Services.jsm");
    1.18 +Cu.import("resource://gre/modules/AddonManager.jsm");
    1.19 +Cu.import("resource://gre/modules/FileUtils.jsm");
    1.20 +Cu.import("resource://gre/modules/JNI.jsm");
    1.21 +Cu.import('resource://gre/modules/Payment.jsm');
    1.22 +Cu.import("resource://gre/modules/NotificationDB.jsm");
    1.23 +Cu.import("resource://gre/modules/SpatialNavigation.jsm");
    1.24 +Cu.import("resource://gre/modules/UITelemetry.jsm");
    1.25 +
    1.26 +#ifdef ACCESSIBILITY
    1.27 +Cu.import("resource://gre/modules/accessibility/AccessFu.jsm");
    1.28 +#endif
    1.29 +
    1.30 +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
    1.31 +                                  "resource://gre/modules/PluralForm.jsm");
    1.32 +
    1.33 +XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava",
    1.34 +                                  "resource://gre/modules/Messaging.jsm");
    1.35 +
    1.36 +XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
    1.37 +                                  "resource://gre/modules/devtools/dbg-server.jsm");
    1.38 +
    1.39 +XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides",
    1.40 +                                  "resource://gre/modules/UserAgentOverrides.jsm");
    1.41 +
    1.42 +XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
    1.43 +                                  "resource://gre/modules/LoginManagerContent.jsm");
    1.44 +
    1.45 +XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
    1.46 +XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
    1.47 +
    1.48 +#ifdef MOZ_SAFE_BROWSING
    1.49 +XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
    1.50 +                                  "resource://gre/modules/SafeBrowsing.jsm");
    1.51 +#endif
    1.52 +
    1.53 +XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    1.54 +                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
    1.55 +
    1.56 +XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
    1.57 +                                  "resource://gre/modules/Sanitizer.jsm");
    1.58 +
    1.59 +XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
    1.60 +                                  "resource://gre/modules/Prompt.jsm");
    1.61 +
    1.62 +XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
    1.63 +                                  "resource://gre/modules/HelperApps.jsm");
    1.64 +
    1.65 +XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
    1.66 +                                  "resource://gre/modules/SSLExceptions.jsm");
    1.67 +
    1.68 +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
    1.69 +                                  "resource://gre/modules/FormHistory.jsm");
    1.70 +
    1.71 +XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
    1.72 +                                   "@mozilla.org/uuid-generator;1",
    1.73 +                                   "nsIUUIDGenerator");
    1.74 +
    1.75 +XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
    1.76 +                                  "resource://gre/modules/SimpleServiceDiscovery.jsm");
    1.77 +
    1.78 +#ifdef NIGHTLY_BUILD
    1.79 +XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
    1.80 +                                  "resource://shumway/ShumwayUtils.jsm");
    1.81 +#endif
    1.82 +
    1.83 +#ifdef MOZ_ANDROID_SYNTHAPKS
    1.84 +XPCOMUtils.defineLazyModuleGetter(this, "WebappManager",
    1.85 +                                  "resource://gre/modules/WebappManager.jsm");
    1.86 +#endif
    1.87 +
    1.88 +XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
    1.89 +                                  "resource://gre/modules/CharsetMenu.jsm");
    1.90 +
    1.91 +// Lazily-loaded browser scripts:
    1.92 +[
    1.93 +  ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
    1.94 +  ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
    1.95 +  ["AboutReader", "chrome://browser/content/aboutReader.js"],
    1.96 +  ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
    1.97 +  ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
    1.98 +  ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
    1.99 +  ["Linkifier", "chrome://browser/content/Linkify.js"],
   1.100 +  ["ZoomHelper", "chrome://browser/content/ZoomHelper.js"],
   1.101 +  ["CastingApps", "chrome://browser/content/CastingApps.js"],
   1.102 +].forEach(function (aScript) {
   1.103 +  let [name, script] = aScript;
   1.104 +  XPCOMUtils.defineLazyGetter(window, name, function() {
   1.105 +    let sandbox = {};
   1.106 +    Services.scriptloader.loadSubScript(script, sandbox);
   1.107 +    return sandbox[name];
   1.108 +  });
   1.109 +});
   1.110 +
   1.111 +[
   1.112 +#ifdef MOZ_WEBRTC
   1.113 +  ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"],
   1.114 +#endif
   1.115 +  ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
   1.116 +  ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
   1.117 +  ["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
   1.118 +  ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
   1.119 +  ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
   1.120 +  ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
   1.121 +  ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
   1.122 +].forEach(function (aScript) {
   1.123 +  let [name, notifications, script] = aScript;
   1.124 +  XPCOMUtils.defineLazyGetter(window, name, function() {
   1.125 +    let sandbox = {};
   1.126 +    Services.scriptloader.loadSubScript(script, sandbox);
   1.127 +    return sandbox[name];
   1.128 +  });
   1.129 +  notifications.forEach(function (aNotification) {
   1.130 +    Services.obs.addObserver(function(s, t, d) {
   1.131 +        window[name].observe(s, t, d)
   1.132 +    }, aNotification, false);
   1.133 +  });
   1.134 +});
   1.135 +
   1.136 +// Lazily-loaded JS modules that use observer notifications
   1.137 +[
   1.138 +  ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView",
   1.139 +            "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
   1.140 +].forEach(module => {
   1.141 +  let [name, notifications, resource] = module;
   1.142 +  XPCOMUtils.defineLazyModuleGetter(this, name, resource);
   1.143 +  notifications.forEach(notification => {
   1.144 +    Services.obs.addObserver((s,t,d) => {
   1.145 +      this[name].observe(s,t,d)
   1.146 +    }, notification, false);
   1.147 +  });
   1.148 +});
   1.149 +
   1.150 +XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
   1.151 +  "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
   1.152 +
   1.153 +XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
   1.154 +  "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
   1.155 +
   1.156 +XPCOMUtils.defineLazyServiceGetter(window, "URIFixup",
   1.157 +  "@mozilla.org/docshell/urifixup;1", "nsIURIFixup");
   1.158 +
   1.159 +#ifdef MOZ_WEBRTC
   1.160 +XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
   1.161 +  "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService");
   1.162 +#endif
   1.163 +
   1.164 +const kStateActive = 0x00000001; // :active pseudoclass for elements
   1.165 +
   1.166 +const kXLinkNamespace = "http://www.w3.org/1999/xlink";
   1.167 +
   1.168 +const kDefaultCSSViewportWidth = 980;
   1.169 +const kDefaultCSSViewportHeight = 480;
   1.170 +
   1.171 +const kViewportRemeasureThrottle = 500;
   1.172 +
   1.173 +const kDoNotTrackPrefState = Object.freeze({
   1.174 +  NO_PREF: "0",
   1.175 +  DISALLOW_TRACKING: "1",
   1.176 +  ALLOW_TRACKING: "2",
   1.177 +});
   1.178 +
   1.179 +function dump(a) {
   1.180 +  Services.console.logStringMessage(a);
   1.181 +}
   1.182 +
   1.183 +function doChangeMaxLineBoxWidth(aWidth) {
   1.184 +  gReflowPending = null;
   1.185 +  let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
   1.186 +  let docShell = webNav.QueryInterface(Ci.nsIDocShell);
   1.187 +  let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
   1.188 +
   1.189 +  let range = null;
   1.190 +  if (BrowserApp.selectedTab._mReflozPoint) {
   1.191 +    range = BrowserApp.selectedTab._mReflozPoint.range;
   1.192 +  }
   1.193 +
   1.194 +  try {
   1.195 +    docViewer.pausePainting();
   1.196 +    docViewer.changeMaxLineBoxWidth(aWidth);
   1.197 +
   1.198 +    if (range) {
   1.199 +      ZoomHelper.zoomInAndSnapToRange(range);
   1.200 +    } else {
   1.201 +      // In this case, we actually didn't zoom into a specific range. It
   1.202 +      // probably happened from a page load reflow-on-zoom event, so we
   1.203 +      // need to make sure painting is re-enabled.
   1.204 +      BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
   1.205 +    }
   1.206 +  } finally {
   1.207 +    docViewer.resumePainting();
   1.208 +  }
   1.209 +}
   1.210 +
   1.211 +function fuzzyEquals(a, b) {
   1.212 +  return (Math.abs(a - b) < 1e-6);
   1.213 +}
   1.214 +
   1.215 +/**
   1.216 + * Convert a font size to CSS pixels (px) from twentieiths-of-a-point
   1.217 + * (twips).
   1.218 + */
   1.219 +function convertFromTwipsToPx(aSize) {
   1.220 +  return aSize/240 * 16.0;
   1.221 +}
   1.222 +
   1.223 +#ifdef MOZ_CRASHREPORTER
   1.224 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
   1.225 +XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
   1.226 +  "@mozilla.org/xre/app-info;1", "nsICrashReporter");
   1.227 +#endif
   1.228 +
   1.229 +XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
   1.230 +  let ContentAreaUtils = {};
   1.231 +  Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
   1.232 +  return ContentAreaUtils;
   1.233 +});
   1.234 +
   1.235 +XPCOMUtils.defineLazyModuleGetter(this, "Rect",
   1.236 +                                  "resource://gre/modules/Geometry.jsm");
   1.237 +
   1.238 +function resolveGeckoURI(aURI) {
   1.239 +  if (!aURI)
   1.240 +    throw "Can't resolve an empty uri";
   1.241 +
   1.242 +  if (aURI.startsWith("chrome://")) {
   1.243 +    let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
   1.244 +    return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
   1.245 +  } else if (aURI.startsWith("resource://")) {
   1.246 +    let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
   1.247 +    return handler.resolveURI(Services.io.newURI(aURI, null, null));
   1.248 +  }
   1.249 +  return aURI;
   1.250 +}
   1.251 +
   1.252 +/**
   1.253 + * Cache of commonly used string bundles.
   1.254 + */
   1.255 +var Strings = {};
   1.256 +[
   1.257 +  ["brand",      "chrome://branding/locale/brand.properties"],
   1.258 +  ["browser",    "chrome://browser/locale/browser.properties"]
   1.259 +].forEach(function (aStringBundle) {
   1.260 +  let [name, bundle] = aStringBundle;
   1.261 +  XPCOMUtils.defineLazyGetter(Strings, name, function() {
   1.262 +    return Services.strings.createBundle(bundle);
   1.263 +  });
   1.264 +});
   1.265 +
   1.266 +const kFormHelperModeDisabled = 0;
   1.267 +const kFormHelperModeEnabled = 1;
   1.268 +const kFormHelperModeDynamic = 2;   // disabled on tablets
   1.269 +
   1.270 +var BrowserApp = {
   1.271 +  _tabs: [],
   1.272 +  _selectedTab: null,
   1.273 +  _prefObservers: [],
   1.274 +  isGuest: false,
   1.275 +
   1.276 +  get isTablet() {
   1.277 +    let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
   1.278 +    delete this.isTablet;
   1.279 +    return this.isTablet = sysInfo.get("tablet");
   1.280 +  },
   1.281 +
   1.282 +  get isOnLowMemoryPlatform() {
   1.283 +    let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
   1.284 +    delete this.isOnLowMemoryPlatform;
   1.285 +    return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
   1.286 +  },
   1.287 +
   1.288 +  deck: null,
   1.289 +
   1.290 +  startup: function startup() {
   1.291 +    window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
   1.292 +    dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
   1.293 +
   1.294 +    this.deck = document.getElementById("browsers");
   1.295 +    this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
   1.296 +      try {
   1.297 +        BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
   1.298 +        Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
   1.299 +        sendMessageToJava({ type: "Gecko:DelayedStartup" });
   1.300 +      } catch(ex) { console.log(ex); }
   1.301 +    }, false);
   1.302 +
   1.303 +    BrowserEventHandler.init();
   1.304 +    ViewportHandler.init();
   1.305 +
   1.306 +    Services.androidBridge.browserApp = this;
   1.307 +
   1.308 +    Services.obs.addObserver(this, "Locale:Changed", false);
   1.309 +    Services.obs.addObserver(this, "Tab:Load", false);
   1.310 +    Services.obs.addObserver(this, "Tab:Selected", false);
   1.311 +    Services.obs.addObserver(this, "Tab:Closed", false);
   1.312 +    Services.obs.addObserver(this, "Session:Back", false);
   1.313 +    Services.obs.addObserver(this, "Session:ShowHistory", false);
   1.314 +    Services.obs.addObserver(this, "Session:Forward", false);
   1.315 +    Services.obs.addObserver(this, "Session:Reload", false);
   1.316 +    Services.obs.addObserver(this, "Session:Stop", false);
   1.317 +    Services.obs.addObserver(this, "SaveAs:PDF", false);
   1.318 +    Services.obs.addObserver(this, "Browser:Quit", false);
   1.319 +    Services.obs.addObserver(this, "Preferences:Set", false);
   1.320 +    Services.obs.addObserver(this, "ScrollTo:FocusedInput", false);
   1.321 +    Services.obs.addObserver(this, "Sanitize:ClearData", false);
   1.322 +    Services.obs.addObserver(this, "FullScreen:Exit", false);
   1.323 +    Services.obs.addObserver(this, "Viewport:Change", false);
   1.324 +    Services.obs.addObserver(this, "Viewport:Flush", false);
   1.325 +    Services.obs.addObserver(this, "Viewport:FixedMarginsChanged", false);
   1.326 +    Services.obs.addObserver(this, "Passwords:Init", false);
   1.327 +    Services.obs.addObserver(this, "FormHistory:Init", false);
   1.328 +    Services.obs.addObserver(this, "gather-telemetry", false);
   1.329 +    Services.obs.addObserver(this, "keyword-search", false);
   1.330 +#ifdef MOZ_ANDROID_SYNTHAPKS
   1.331 +    Services.obs.addObserver(this, "webapps-runtime-install", false);
   1.332 +    Services.obs.addObserver(this, "webapps-runtime-install-package", false);
   1.333 +    Services.obs.addObserver(this, "webapps-ask-install", false);
   1.334 +    Services.obs.addObserver(this, "webapps-launch", false);
   1.335 +    Services.obs.addObserver(this, "webapps-uninstall", false);
   1.336 +    Services.obs.addObserver(this, "Webapps:AutoInstall", false);
   1.337 +    Services.obs.addObserver(this, "Webapps:Load", false);
   1.338 +    Services.obs.addObserver(this, "Webapps:AutoUninstall", false);
   1.339 +#endif
   1.340 +    Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
   1.341 +
   1.342 +    function showFullScreenWarning() {
   1.343 +      NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
   1.344 +    }
   1.345 +
   1.346 +    window.addEventListener("fullscreen", function() {
   1.347 +      sendMessageToJava({
   1.348 +        type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide"
   1.349 +      });
   1.350 +    }, false);
   1.351 +
   1.352 +    window.addEventListener("mozfullscreenchange", function() {
   1.353 +      sendMessageToJava({
   1.354 +        type: document.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop"
   1.355 +      });
   1.356 +
   1.357 +      if (document.mozFullScreen)
   1.358 +        showFullScreenWarning();
   1.359 +    }, false);
   1.360 +
   1.361 +    // When a restricted key is pressed in DOM full-screen mode, we should display
   1.362 +    // the "Press ESC to exit" warning message.
   1.363 +    window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true);
   1.364 +
   1.365 +    NativeWindow.init();
   1.366 +    LightWeightThemeWebInstaller.init();
   1.367 +    Downloads.init();
   1.368 +    FormAssistant.init();
   1.369 +    IndexedDB.init();
   1.370 +    HealthReportStatusListener.init();
   1.371 +    XPInstallObserver.init();
   1.372 +    CharacterEncoding.init();
   1.373 +    ActivityObserver.init();
   1.374 +#ifdef MOZ_ANDROID_SYNTHAPKS
   1.375 +    // TODO: replace with Android implementation of WebappOSUtils.isLaunchable.
   1.376 +    Cu.import("resource://gre/modules/Webapps.jsm");
   1.377 +    DOMApplicationRegistry.allAppsLaunchable = true;
   1.378 +#else
   1.379 +    WebappsUI.init();
   1.380 +#endif
   1.381 +    RemoteDebugger.init();
   1.382 +    Reader.init();
   1.383 +    UserAgentOverrides.init();
   1.384 +    DesktopUserAgent.init();
   1.385 +    CastingApps.init();
   1.386 +    Distribution.init();
   1.387 +    Tabs.init();
   1.388 +#ifdef ACCESSIBILITY
   1.389 +    AccessFu.attach(window);
   1.390 +#endif
   1.391 +#ifdef NIGHTLY_BUILD
   1.392 +    ShumwayUtils.init();
   1.393 +#endif
   1.394 +
   1.395 +    // Init LoginManager
   1.396 +    Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
   1.397 +
   1.398 +    let url = null;
   1.399 +    let pinned = false;
   1.400 +    if ("arguments" in window) {
   1.401 +      if (window.arguments[0])
   1.402 +        url = window.arguments[0];
   1.403 +      if (window.arguments[1])
   1.404 +        gScreenWidth = window.arguments[1];
   1.405 +      if (window.arguments[2])
   1.406 +        gScreenHeight = window.arguments[2];
   1.407 +      if (window.arguments[3])
   1.408 +        pinned = window.arguments[3];
   1.409 +      if (window.arguments[4])
   1.410 +        this.isGuest = window.arguments[4];
   1.411 +    }
   1.412 +
   1.413 +    if (pinned) {
   1.414 +      this._initRuntime(this._startupStatus, url, aUrl => this.addTab(aUrl));
   1.415 +    } else {
   1.416 +      SearchEngines.init();
   1.417 +      this.initContextMenu();
   1.418 +    }
   1.419 +    // The order that context menu items are added is important
   1.420 +    // Make sure the "Open in App" context menu item appears at the bottom of the list
   1.421 +    ExternalApps.init();
   1.422 +
   1.423 +    // XXX maybe we don't do this if the launch was kicked off from external
   1.424 +    Services.io.offline = false;
   1.425 +
   1.426 +    // Broadcast a UIReady message so add-ons know we are finished with startup
   1.427 +    let event = document.createEvent("Events");
   1.428 +    event.initEvent("UIReady", true, false);
   1.429 +    window.dispatchEvent(event);
   1.430 +
   1.431 +    if (this._startupStatus)
   1.432 +      this.onAppUpdated();
   1.433 +
   1.434 +    // Store the low-precision buffer pref
   1.435 +    this.gUseLowPrecision = Services.prefs.getBoolPref("layers.low-precision-buffer");
   1.436 +
   1.437 +    // notify java that gecko has loaded
   1.438 +    sendMessageToJava({ type: "Gecko:Ready" });
   1.439 +
   1.440 +#ifdef MOZ_SAFE_BROWSING
   1.441 +    // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
   1.442 +    setTimeout(function() { SafeBrowsing.init(); }, 5000);
   1.443 +#endif
   1.444 +  },
   1.445 +
   1.446 +  get _startupStatus() {
   1.447 +    delete this._startupStatus;
   1.448 +
   1.449 +    let savedMilestone = null;
   1.450 +    try {
   1.451 +      savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
   1.452 +    } catch (e) {
   1.453 +    }
   1.454 +#expand    let ourMilestone = "__MOZ_APP_VERSION__";
   1.455 +    this._startupStatus = "";
   1.456 +    if (ourMilestone != savedMilestone) {
   1.457 +      Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone);
   1.458 +      this._startupStatus = savedMilestone ? "upgrade" : "new";
   1.459 +    }
   1.460 +
   1.461 +    return this._startupStatus;
   1.462 +  },
   1.463 +
   1.464 +  /**
   1.465 +   * Pass this a locale string, such as "fr" or "es_ES".
   1.466 +   */
   1.467 +  setLocale: function (locale) {
   1.468 +    console.log("browser.js: requesting locale set: " + locale);
   1.469 +    sendMessageToJava({ type: "Locale:Set", locale: locale });
   1.470 +  },
   1.471 +
   1.472 +  _initRuntime: function(status, url, callback) {
   1.473 +    let sandbox = {};
   1.474 +    Services.scriptloader.loadSubScript("chrome://browser/content/WebappRT.js", sandbox);
   1.475 +    window.WebappRT = sandbox.WebappRT;
   1.476 +    WebappRT.init(status, url, callback);
   1.477 +  },
   1.478 +
   1.479 +  initContextMenu: function ba_initContextMenu() {
   1.480 +    // TODO: These should eventually move into more appropriate classes
   1.481 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"),
   1.482 +      NativeWindow.contextmenus.linkOpenableNonPrivateContext,
   1.483 +      function(aTarget) {
   1.484 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
   1.485 +        UITelemetry.addEvent("loadurl.1", "contextmenu", null);
   1.486 +
   1.487 +        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   1.488 +        ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
   1.489 +        BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });
   1.490 +
   1.491 +        let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened");
   1.492 +        let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
   1.493 +        NativeWindow.toast.show(label, "short");
   1.494 +      });
   1.495 +
   1.496 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInPrivateTab"),
   1.497 +      NativeWindow.contextmenus.linkOpenableContext,
   1.498 +      function(aTarget) {
   1.499 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_private_tab");
   1.500 +        UITelemetry.addEvent("loadurl.1", "contextmenu", null);
   1.501 +
   1.502 +        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   1.503 +        ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
   1.504 +        BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true });
   1.505 +
   1.506 +        let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened");
   1.507 +        let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
   1.508 +        NativeWindow.toast.show(label, "short");
   1.509 +      });
   1.510 +
   1.511 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyLink"),
   1.512 +      NativeWindow.contextmenus.linkCopyableContext,
   1.513 +      function(aTarget) {
   1.514 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_link");
   1.515 +
   1.516 +        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   1.517 +        NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
   1.518 +      });
   1.519 +
   1.520 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyEmailAddress"),
   1.521 +      NativeWindow.contextmenus.emailLinkContext,
   1.522 +      function(aTarget) {
   1.523 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_email");
   1.524 +
   1.525 +        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   1.526 +        let emailAddr = NativeWindow.contextmenus._stripScheme(url);
   1.527 +        NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr);
   1.528 +      });
   1.529 +
   1.530 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyPhoneNumber"),
   1.531 +      NativeWindow.contextmenus.phoneNumberLinkContext,
   1.532 +      function(aTarget) {
   1.533 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_phone");
   1.534 +
   1.535 +        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   1.536 +        let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
   1.537 +        NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber);
   1.538 +      });
   1.539 +
   1.540 +    NativeWindow.contextmenus.add({
   1.541 +      label: Strings.browser.GetStringFromName("contextmenu.shareLink"),
   1.542 +      order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
   1.543 +      selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkShareableContext),
   1.544 +      showAsActions: function(aElement) {
   1.545 +        return {
   1.546 +          title: aElement.textContent.trim() || aElement.title.trim(),
   1.547 +          uri: NativeWindow.contextmenus._getLinkURL(aElement),
   1.548 +        };
   1.549 +      },
   1.550 +      icon: "drawable://ic_menu_share",
   1.551 +      callback: function(aTarget) {
   1.552 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link");
   1.553 +      }
   1.554 +    });
   1.555 +
   1.556 +    NativeWindow.contextmenus.add({
   1.557 +      label: Strings.browser.GetStringFromName("contextmenu.shareEmailAddress"),
   1.558 +      order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
   1.559 +      selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
   1.560 +      showAsActions: function(aElement) {
   1.561 +        let url = NativeWindow.contextmenus._getLinkURL(aElement);
   1.562 +        let emailAddr = NativeWindow.contextmenus._stripScheme(url);
   1.563 +        let title = aElement.textContent || aElement.title;
   1.564 +        return {
   1.565 +          title: title,
   1.566 +          uri: emailAddr,
   1.567 +        };
   1.568 +      },
   1.569 +      icon: "drawable://ic_menu_share",
   1.570 +      callback: function(aTarget) {
   1.571 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email");
   1.572 +      }
   1.573 +    });
   1.574 +
   1.575 +    NativeWindow.contextmenus.add({
   1.576 +      label: Strings.browser.GetStringFromName("contextmenu.sharePhoneNumber"),
   1.577 +      order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
   1.578 +      selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
   1.579 +      showAsActions: function(aElement) {
   1.580 +        let url = NativeWindow.contextmenus._getLinkURL(aElement);
   1.581 +        let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
   1.582 +        let title = aElement.textContent || aElement.title;
   1.583 +        return {
   1.584 +          title: title,
   1.585 +          uri: phoneNumber,
   1.586 +        };
   1.587 +      },
   1.588 +      icon: "drawable://ic_menu_share",
   1.589 +      callback: function(aTarget) {
   1.590 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone");
   1.591 +      }
   1.592 +    });
   1.593 +
   1.594 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
   1.595 +      NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
   1.596 +      function(aTarget) {
   1.597 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email");
   1.598 +
   1.599 +        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   1.600 +        sendMessageToJava({
   1.601 +          type: "Contact:Add",
   1.602 +          email: url
   1.603 +        });
   1.604 +      });
   1.605 +
   1.606 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
   1.607 +      NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
   1.608 +      function(aTarget) {
   1.609 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone");
   1.610 +
   1.611 +        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   1.612 +        sendMessageToJava({
   1.613 +          type: "Contact:Add",
   1.614 +          phone: url
   1.615 +        });
   1.616 +      });
   1.617 +
   1.618 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"),
   1.619 +      NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkBookmarkableContext),
   1.620 +      function(aTarget) {
   1.621 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark");
   1.622 +
   1.623 +        let url = NativeWindow.contextmenus._getLinkURL(aTarget);
   1.624 +        let title = aTarget.textContent || aTarget.title || url;
   1.625 +        sendMessageToJava({
   1.626 +          type: "Bookmark:Insert",
   1.627 +          url: url,
   1.628 +          title: title
   1.629 +        });
   1.630 +      });
   1.631 +
   1.632 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.playMedia"),
   1.633 +      NativeWindow.contextmenus.mediaContext("media-paused"),
   1.634 +      function(aTarget) {
   1.635 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_play");
   1.636 +        aTarget.play();
   1.637 +      });
   1.638 +
   1.639 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.pauseMedia"),
   1.640 +      NativeWindow.contextmenus.mediaContext("media-playing"),
   1.641 +      function(aTarget) {
   1.642 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_pause");
   1.643 +        aTarget.pause();
   1.644 +      });
   1.645 +
   1.646 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.showControls2"),
   1.647 +      NativeWindow.contextmenus.mediaContext("media-hidingcontrols"),
   1.648 +      function(aTarget) {
   1.649 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media");
   1.650 +        aTarget.setAttribute("controls", true);
   1.651 +      });
   1.652 +
   1.653 +    NativeWindow.contextmenus.add({
   1.654 +      label: Strings.browser.GetStringFromName("contextmenu.shareMedia"),
   1.655 +      order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
   1.656 +      selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.SelectorContext("video")),
   1.657 +      showAsActions: function(aElement) {
   1.658 +        let url = (aElement.currentSrc || aElement.src);
   1.659 +        let title = aElement.textContent || aElement.title;
   1.660 +        return {
   1.661 +          title: title,
   1.662 +          uri: url,
   1.663 +          type: "video/*",
   1.664 +        };
   1.665 +      },
   1.666 +      icon: "drawable://ic_menu_share",
   1.667 +      callback: function(aTarget) {
   1.668 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media");
   1.669 +      }
   1.670 +    });
   1.671 +
   1.672 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.fullScreen"),
   1.673 +      NativeWindow.contextmenus.SelectorContext("video:not(:-moz-full-screen)"),
   1.674 +      function(aTarget) {
   1.675 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen");
   1.676 +        aTarget.mozRequestFullScreen();
   1.677 +      });
   1.678 +
   1.679 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.mute"),
   1.680 +      NativeWindow.contextmenus.mediaContext("media-unmuted"),
   1.681 +      function(aTarget) {
   1.682 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute");
   1.683 +        aTarget.muted = true;
   1.684 +      });
   1.685 +  
   1.686 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"),
   1.687 +      NativeWindow.contextmenus.mediaContext("media-muted"),
   1.688 +      function(aTarget) {
   1.689 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute");
   1.690 +        aTarget.muted = false;
   1.691 +      });
   1.692 +
   1.693 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyImageLocation"),
   1.694 +      NativeWindow.contextmenus.imageLocationCopyableContext,
   1.695 +      function(aTarget) {
   1.696 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image");
   1.697 +
   1.698 +        let url = aTarget.src;
   1.699 +        NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
   1.700 +      });
   1.701 +
   1.702 +    NativeWindow.contextmenus.add({
   1.703 +      label: Strings.browser.GetStringFromName("contextmenu.shareImage"),
   1.704 +      selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
   1.705 +      order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
   1.706 +      showAsActions: function(aTarget) {
   1.707 +        let doc = aTarget.ownerDocument;
   1.708 +        let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
   1.709 +                                                         .getImgCacheForDocument(doc);
   1.710 +        let props = imageCache.findEntryProperties(aTarget.currentURI, doc.characterSet);
   1.711 +        let src = aTarget.src;
   1.712 +        return {
   1.713 +          title: src,
   1.714 +          uri: src,
   1.715 +          type: "image/*",
   1.716 +        };
   1.717 +      },
   1.718 +      icon: "drawable://ic_menu_share",
   1.719 +      menu: true,
   1.720 +      callback: function(aTarget) {
   1.721 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image");
   1.722 +      }
   1.723 +    });
   1.724 +
   1.725 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.saveImage"),
   1.726 +      NativeWindow.contextmenus.imageSaveableContext,
   1.727 +      function(aTarget) {
   1.728 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image");
   1.729 +
   1.730 +        ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
   1.731 +                                      false, true, aTarget.ownerDocument.documentURIObject,
   1.732 +                                      aTarget.ownerDocument);
   1.733 +      });
   1.734 +
   1.735 +    NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.setImageAs"),
   1.736 +      NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
   1.737 +      function(aTarget) {
   1.738 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image");
   1.739 +
   1.740 +        let src = aTarget.src;
   1.741 +        sendMessageToJava({
   1.742 +          type: "Image:SetAs",
   1.743 +          url: src
   1.744 +        });
   1.745 +      });
   1.746 +
   1.747 +    NativeWindow.contextmenus.add(
   1.748 +      function(aTarget) {
   1.749 +        if (aTarget instanceof HTMLVideoElement) {
   1.750 +          // If a video element is zero width or height, its essentially
   1.751 +          // an HTMLAudioElement.
   1.752 +          if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 )
   1.753 +            return Strings.browser.GetStringFromName("contextmenu.saveAudio");
   1.754 +          return Strings.browser.GetStringFromName("contextmenu.saveVideo");
   1.755 +        } else if (aTarget instanceof HTMLAudioElement) {
   1.756 +          return Strings.browser.GetStringFromName("contextmenu.saveAudio");
   1.757 +        }
   1.758 +        return Strings.browser.GetStringFromName("contextmenu.saveVideo");
   1.759 +      }, NativeWindow.contextmenus.mediaSaveableContext,
   1.760 +      function(aTarget) {
   1.761 +        UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media");
   1.762 +
   1.763 +        let url = aTarget.currentSrc || aTarget.src;
   1.764 +        let filePickerTitleKey = (aTarget instanceof HTMLVideoElement &&
   1.765 +                                  (aTarget.videoWidth != 0 && aTarget.videoHeight != 0))
   1.766 +                                  ? "SaveVideoTitle" : "SaveAudioTitle";
   1.767 +        // Skipped trying to pull MIME type out of cache for now
   1.768 +        ContentAreaUtils.internalSave(url, null, null, null, null, false,
   1.769 +                                      filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
   1.770 +                                      aTarget.ownerDocument, true, null);
   1.771 +      });
   1.772 +  },
   1.773 +
   1.774 +  onAppUpdated: function() {
   1.775 +    // initialize the form history and passwords databases on upgrades
   1.776 +    Services.obs.notifyObservers(null, "FormHistory:Init", "");
   1.777 +    Services.obs.notifyObservers(null, "Passwords:Init", "");
   1.778 +
   1.779 +    // Migrate user-set "plugins.click_to_play" pref. See bug 884694.
   1.780 +    // Because the default value is true, a user-set pref means that the pref was set to false.
   1.781 +    if (Services.prefs.prefHasUserValue("plugins.click_to_play")) {
   1.782 +      Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
   1.783 +      Services.prefs.clearUserPref("plugins.click_to_play");
   1.784 +    }
   1.785 +  },
   1.786 +
   1.787 +  shutdown: function shutdown() {
   1.788 +    NativeWindow.uninit();
   1.789 +    LightWeightThemeWebInstaller.uninit();
   1.790 +    FormAssistant.uninit();
   1.791 +    IndexedDB.uninit();
   1.792 +    ViewportHandler.uninit();
   1.793 +    XPInstallObserver.uninit();
   1.794 +    HealthReportStatusListener.uninit();
   1.795 +    CharacterEncoding.uninit();
   1.796 +    SearchEngines.uninit();
   1.797 +#ifndef MOZ_ANDROID_SYNTHAPKS
   1.798 +    WebappsUI.uninit();
   1.799 +#endif
   1.800 +    RemoteDebugger.uninit();
   1.801 +    Reader.uninit();
   1.802 +    UserAgentOverrides.uninit();
   1.803 +    DesktopUserAgent.uninit();
   1.804 +    ExternalApps.uninit();
   1.805 +    CastingApps.uninit();
   1.806 +    Distribution.uninit();
   1.807 +    Tabs.uninit();
   1.808 +  },
   1.809 +
   1.810 +  // This function returns false during periods where the browser displayed document is
   1.811 +  // different from the browser content document, so user actions and some kinds of viewport
   1.812 +  // updates should be ignored. This period starts when we start loading a new page or
   1.813 +  // switch tabs, and ends when the new browser content document has been drawn and handed
   1.814 +  // off to the compositor.
   1.815 +  isBrowserContentDocumentDisplayed: function() {
   1.816 +    try {
   1.817 +      if (!Services.androidBridge.isContentDocumentDisplayed())
   1.818 +        return false;
   1.819 +    } catch (e) {
   1.820 +      return false;
   1.821 +    }
   1.822 +
   1.823 +    let tab = this.selectedTab;
   1.824 +    if (!tab)
   1.825 +      return false;
   1.826 +    return tab.contentDocumentIsDisplayed;
   1.827 +  },
   1.828 +
   1.829 +  contentDocumentChanged: function() {
   1.830 +    window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true;
   1.831 +    Services.androidBridge.contentDocumentChanged();
   1.832 +  },
   1.833 +
   1.834 +  get tabs() {
   1.835 +    return this._tabs;
   1.836 +  },
   1.837 +
   1.838 +  get selectedTab() {
   1.839 +    return this._selectedTab;
   1.840 +  },
   1.841 +
   1.842 +  set selectedTab(aTab) {
   1.843 +    if (this._selectedTab == aTab)
   1.844 +      return;
   1.845 +
   1.846 +    if (this._selectedTab) {
   1.847 +      this._selectedTab.setActive(false);
   1.848 +    }
   1.849 +
   1.850 +    this._selectedTab = aTab;
   1.851 +    if (!aTab)
   1.852 +      return;
   1.853 +
   1.854 +    aTab.setActive(true);
   1.855 +    aTab.setResolution(aTab._zoom, true);
   1.856 +    this.contentDocumentChanged();
   1.857 +    this.deck.selectedPanel = aTab.browser;
   1.858 +    // Focus the browser so that things like selection will be styled correctly.
   1.859 +    aTab.browser.focus();
   1.860 +  },
   1.861 +
   1.862 +  get selectedBrowser() {
   1.863 +    if (this._selectedTab)
   1.864 +      return this._selectedTab.browser;
   1.865 +    return null;
   1.866 +  },
   1.867 +
   1.868 +  getTabForId: function getTabForId(aId) {
   1.869 +    let tabs = this._tabs;
   1.870 +    for (let i=0; i < tabs.length; i++) {
   1.871 +       if (tabs[i].id == aId)
   1.872 +         return tabs[i];
   1.873 +    }
   1.874 +    return null;
   1.875 +  },
   1.876 +
   1.877 +  getTabForBrowser: function getTabForBrowser(aBrowser) {
   1.878 +    let tabs = this._tabs;
   1.879 +    for (let i = 0; i < tabs.length; i++) {
   1.880 +      if (tabs[i].browser == aBrowser)
   1.881 +        return tabs[i];
   1.882 +    }
   1.883 +    return null;
   1.884 +  },
   1.885 +
   1.886 +  getTabForWindow: function getTabForWindow(aWindow) {
   1.887 +    let tabs = this._tabs;
   1.888 +    for (let i = 0; i < tabs.length; i++) {
   1.889 +      if (tabs[i].browser.contentWindow == aWindow)
   1.890 +        return tabs[i];
   1.891 +    }
   1.892 +    return null;
   1.893 +  },
   1.894 +
   1.895 +  getBrowserForWindow: function getBrowserForWindow(aWindow) {
   1.896 +    let tabs = this._tabs;
   1.897 +    for (let i = 0; i < tabs.length; i++) {
   1.898 +      if (tabs[i].browser.contentWindow == aWindow)
   1.899 +        return tabs[i].browser;
   1.900 +    }
   1.901 +    return null;
   1.902 +  },
   1.903 +
   1.904 +  getBrowserForDocument: function getBrowserForDocument(aDocument) {
   1.905 +    let tabs = this._tabs;
   1.906 +    for (let i = 0; i < tabs.length; i++) {
   1.907 +      if (tabs[i].browser.contentDocument == aDocument)
   1.908 +        return tabs[i].browser;
   1.909 +    }
   1.910 +    return null;
   1.911 +  },
   1.912 +
   1.913 +  loadURI: function loadURI(aURI, aBrowser, aParams) {
   1.914 +    aBrowser = aBrowser || this.selectedBrowser;
   1.915 +    if (!aBrowser)
   1.916 +      return;
   1.917 +
   1.918 +    aParams = aParams || {};
   1.919 +
   1.920 +    let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
   1.921 +    let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null;
   1.922 +    let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
   1.923 +    let charset = "charset" in aParams ? aParams.charset : null;
   1.924 +
   1.925 +    let tab = this.getTabForBrowser(aBrowser);
   1.926 +    if (tab) {
   1.927 +      if ("userSearch" in aParams) tab.userSearch = aParams.userSearch;
   1.928 +    }
   1.929 +
   1.930 +    try {
   1.931 +      aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
   1.932 +    } catch(e) {
   1.933 +      if (tab) {
   1.934 +        let message = {
   1.935 +          type: "Content:LoadError",
   1.936 +          tabID: tab.id
   1.937 +        };
   1.938 +        sendMessageToJava(message);
   1.939 +        dump("Handled load error: " + e)
   1.940 +      }
   1.941 +    }
   1.942 +  },
   1.943 +
   1.944 +  addTab: function addTab(aURI, aParams) {
   1.945 +    aParams = aParams || {};
   1.946 +
   1.947 +    let newTab = new Tab(aURI, aParams);
   1.948 +    this._tabs.push(newTab);
   1.949 +
   1.950 +    let selected = "selected" in aParams ? aParams.selected : true;
   1.951 +    if (selected)
   1.952 +      this.selectedTab = newTab;
   1.953 +
   1.954 +    let pinned = "pinned" in aParams ? aParams.pinned : false;
   1.955 +    if (pinned) {
   1.956 +      let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
   1.957 +      ss.setTabValue(newTab, "appOrigin", aURI);
   1.958 +    }
   1.959 +
   1.960 +    let evt = document.createEvent("UIEvents");
   1.961 +    evt.initUIEvent("TabOpen", true, false, window, null);
   1.962 +    newTab.browser.dispatchEvent(evt);
   1.963 +
   1.964 +    return newTab;
   1.965 +  },
   1.966 +
   1.967 +  // Use this method to close a tab from JS. This method sends a message
   1.968 +  // to Java to close the tab in the Java UI (we'll get a Tab:Closed message
   1.969 +  // back from Java when that happens).
   1.970 +  closeTab: function closeTab(aTab) {
   1.971 +    if (!aTab) {
   1.972 +      Cu.reportError("Error trying to close tab (tab doesn't exist)");
   1.973 +      return;
   1.974 +    }
   1.975 +
   1.976 +    let message = {
   1.977 +      type: "Tab:Close",
   1.978 +      tabID: aTab.id
   1.979 +    };
   1.980 +    sendMessageToJava(message);
   1.981 +  },
   1.982 +
   1.983 +#ifdef MOZ_ANDROID_SYNTHAPKS
   1.984 +  _loadWebapp: function(aMessage) {
   1.985 +
   1.986 +    this._initRuntime(this._startupStatus, aMessage.url, aUrl => {
   1.987 +      this.manifestUrl = aMessage.url;
   1.988 +      this.addTab(aUrl, { title: aMessage.name });
   1.989 +    });
   1.990 +  },
   1.991 +#endif
   1.992 +
   1.993 +  // Calling this will update the state in BrowserApp after a tab has been
   1.994 +  // closed in the Java UI.
   1.995 +  _handleTabClosed: function _handleTabClosed(aTab) {
   1.996 +    if (aTab == this.selectedTab)
   1.997 +      this.selectedTab = null;
   1.998 +
   1.999 +    let evt = document.createEvent("UIEvents");
  1.1000 +    evt.initUIEvent("TabClose", true, false, window, null);
  1.1001 +    aTab.browser.dispatchEvent(evt);
  1.1002 +
  1.1003 +    aTab.destroy();
  1.1004 +    this._tabs.splice(this._tabs.indexOf(aTab), 1);
  1.1005 +  },
  1.1006 +
  1.1007 +  // Use this method to select a tab from JS. This method sends a message
  1.1008 +  // to Java to select the tab in the Java UI (we'll get a Tab:Selected message
  1.1009 +  // back from Java when that happens).
  1.1010 +  selectTab: function selectTab(aTab) {
  1.1011 +    if (!aTab) {
  1.1012 +      Cu.reportError("Error trying to select tab (tab doesn't exist)");
  1.1013 +      return;
  1.1014 +    }
  1.1015 +
  1.1016 +    // There's nothing to do if the tab is already selected
  1.1017 +    if (aTab == this.selectedTab)
  1.1018 +      return;
  1.1019 +
  1.1020 +    let message = {
  1.1021 +      type: "Tab:Select",
  1.1022 +      tabID: aTab.id
  1.1023 +    };
  1.1024 +    sendMessageToJava(message);
  1.1025 +  },
  1.1026 +
  1.1027 +  /**
  1.1028 +   * Gets an open tab with the given URL.
  1.1029 +   *
  1.1030 +   * @param  aURL URL to look for
  1.1031 +   * @return the tab with the given URL, or null if no such tab exists
  1.1032 +   */
  1.1033 +  getTabWithURL: function getTabWithURL(aURL) {
  1.1034 +    let uri = Services.io.newURI(aURL, null, null);
  1.1035 +    for (let i = 0; i < this._tabs.length; ++i) {
  1.1036 +      let tab = this._tabs[i];
  1.1037 +      if (tab.browser.currentURI.equals(uri)) {
  1.1038 +        return tab;
  1.1039 +      }
  1.1040 +    }
  1.1041 +    return null;
  1.1042 +  },
  1.1043 +
  1.1044 +  /**
  1.1045 +   * If a tab with the given URL already exists, that tab is selected.
  1.1046 +   * Otherwise, a new tab is opened with the given URL.
  1.1047 +   *
  1.1048 +   * @param aURL URL to open
  1.1049 +   */
  1.1050 +  selectOrOpenTab: function selectOrOpenTab(aURL) {
  1.1051 +    let tab = this.getTabWithURL(aURL);
  1.1052 +    if (tab == null) {
  1.1053 +      this.addTab(aURL);
  1.1054 +    } else {
  1.1055 +      this.selectTab(tab);
  1.1056 +    }
  1.1057 +  },
  1.1058 +
  1.1059 +  // This method updates the state in BrowserApp after a tab has been selected
  1.1060 +  // in the Java UI.
  1.1061 +  _handleTabSelected: function _handleTabSelected(aTab) {
  1.1062 +    this.selectedTab = aTab;
  1.1063 +
  1.1064 +    let evt = document.createEvent("UIEvents");
  1.1065 +    evt.initUIEvent("TabSelect", true, false, window, null);
  1.1066 +    aTab.browser.dispatchEvent(evt);
  1.1067 +  },
  1.1068 +
  1.1069 +  quit: function quit() {
  1.1070 +    // Figure out if there's at least one other browser window around.
  1.1071 +    let lastBrowser = true;
  1.1072 +    let e = Services.wm.getEnumerator("navigator:browser");
  1.1073 +    while (e.hasMoreElements() && lastBrowser) {
  1.1074 +      let win = e.getNext();
  1.1075 +      if (!win.closed && win != window)
  1.1076 +        lastBrowser = false;
  1.1077 +    }
  1.1078 +
  1.1079 +    if (lastBrowser) {
  1.1080 +      // Let everyone know we are closing the last browser window
  1.1081 +      let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
  1.1082 +      Services.obs.notifyObservers(closingCanceled, "browser-lastwindow-close-requested", null);
  1.1083 +      if (closingCanceled.data)
  1.1084 +        return;
  1.1085 +
  1.1086 +      Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null);
  1.1087 +    }
  1.1088 +
  1.1089 +    window.QueryInterface(Ci.nsIDOMChromeWindow).minimize();
  1.1090 +    window.close();
  1.1091 +  },
  1.1092 +
  1.1093 +  saveAsPDF: function saveAsPDF(aBrowser) {
  1.1094 +    // Create the final destination file location
  1.1095 +    let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
  1.1096 +    fileName = fileName.trim() + ".pdf";
  1.1097 +
  1.1098 +    let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
  1.1099 +    let downloadsDir = dm.defaultDownloadsDirectory;
  1.1100 +
  1.1101 +    let file = downloadsDir.clone();
  1.1102 +    file.append(fileName);
  1.1103 +    file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8));
  1.1104 +
  1.1105 +    let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings;
  1.1106 +    printSettings.printSilent = true;
  1.1107 +    printSettings.showPrintProgress = false;
  1.1108 +    printSettings.printBGImages = true;
  1.1109 +    printSettings.printBGColors = true;
  1.1110 +    printSettings.printToFile = true;
  1.1111 +    printSettings.toFileName = file.path;
  1.1112 +    printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
  1.1113 +    printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
  1.1114 +
  1.1115 +    //XXX we probably need a preference here, the header can be useful
  1.1116 +    printSettings.footerStrCenter = "";
  1.1117 +    printSettings.footerStrLeft   = "";
  1.1118 +    printSettings.footerStrRight  = "";
  1.1119 +    printSettings.headerStrCenter = "";
  1.1120 +    printSettings.headerStrLeft   = "";
  1.1121 +    printSettings.headerStrRight  = "";
  1.1122 +
  1.1123 +    // Create a valid mimeInfo for the PDF
  1.1124 +    let ms = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
  1.1125 +    let mimeInfo = ms.getFromTypeAndExtension("application/pdf", "pdf");
  1.1126 +
  1.1127 +    let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
  1.1128 +                                                .getInterface(Ci.nsIWebBrowserPrint);
  1.1129 +
  1.1130 +    let cancelable = {
  1.1131 +      cancel: function (aReason) {
  1.1132 +        webBrowserPrint.cancel();
  1.1133 +      }
  1.1134 +    }
  1.1135 +    let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aBrowser.contentWindow);
  1.1136 +    let download = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,
  1.1137 +                                  aBrowser.currentURI,
  1.1138 +                                  Services.io.newFileURI(file), "", mimeInfo,
  1.1139 +                                  Date.now() * 1000, null, cancelable, isPrivate);
  1.1140 +
  1.1141 +    webBrowserPrint.print(printSettings, download);
  1.1142 +  },
  1.1143 +
  1.1144 +  notifyPrefObservers: function(aPref) {
  1.1145 +    this._prefObservers[aPref].forEach(function(aRequestId) {
  1.1146 +      this.getPreferences(aRequestId, [aPref], 1);
  1.1147 +    }, this);
  1.1148 +  },
  1.1149 +
  1.1150 +  handlePreferencesRequest: function handlePreferencesRequest(aRequestId,
  1.1151 +                                                              aPrefNames,
  1.1152 +                                                              aListen) {
  1.1153 +
  1.1154 +    let prefs = [];
  1.1155 +
  1.1156 +    for (let prefName of aPrefNames) {
  1.1157 +      let pref = {
  1.1158 +        name: prefName,
  1.1159 +        type: "",
  1.1160 +        value: null
  1.1161 +      };
  1.1162 +
  1.1163 +      if (aListen) {
  1.1164 +        if (this._prefObservers[prefName])
  1.1165 +          this._prefObservers[prefName].push(aRequestId);
  1.1166 +        else
  1.1167 +          this._prefObservers[prefName] = [ aRequestId ];
  1.1168 +        Services.prefs.addObserver(prefName, this, false);
  1.1169 +      }
  1.1170 +
  1.1171 +      // These pref names are not "real" pref names.
  1.1172 +      // They are used in the setting menu,
  1.1173 +      // and these are passed when initializing the setting menu.
  1.1174 +      switch (prefName) {
  1.1175 +        // The plugin pref is actually two separate prefs, so
  1.1176 +        // we need to handle it differently
  1.1177 +        case "plugin.enable":
  1.1178 +          pref.type = "string";// Use a string type for java's ListPreference
  1.1179 +          pref.value = PluginHelper.getPluginPreference();
  1.1180 +          prefs.push(pref);
  1.1181 +          continue;
  1.1182 +        // Handle master password
  1.1183 +        case "privacy.masterpassword.enabled":
  1.1184 +          pref.type = "bool";
  1.1185 +          pref.value = MasterPassword.enabled;
  1.1186 +          prefs.push(pref);
  1.1187 +          continue;
  1.1188 +        // Handle do-not-track preference
  1.1189 +        case "privacy.donottrackheader":
  1.1190 +          pref.type = "string";
  1.1191 +
  1.1192 +          let enableDNT = Services.prefs.getBoolPref("privacy.donottrackheader.enabled");
  1.1193 +          if (!enableDNT) {
  1.1194 +            pref.value = kDoNotTrackPrefState.NO_PREF;
  1.1195 +          } else {
  1.1196 +            let dntState = Services.prefs.getIntPref("privacy.donottrackheader.value");
  1.1197 +            pref.value = (dntState === 0) ? kDoNotTrackPrefState.ALLOW_TRACKING :
  1.1198 +                                            kDoNotTrackPrefState.DISALLOW_TRACKING;
  1.1199 +          }
  1.1200 +
  1.1201 +          prefs.push(pref);
  1.1202 +          continue;
  1.1203 +#ifdef MOZ_CRASHREPORTER
  1.1204 +        // Crash reporter submit pref must be fetched from nsICrashReporter service.
  1.1205 +        case "datareporting.crashreporter.submitEnabled":
  1.1206 +          pref.type = "bool";
  1.1207 +          pref.value = CrashReporter.submitReports;
  1.1208 +          prefs.push(pref);
  1.1209 +          continue;
  1.1210 +#endif
  1.1211 +      }
  1.1212 +
  1.1213 +      try {
  1.1214 +        switch (Services.prefs.getPrefType(prefName)) {
  1.1215 +          case Ci.nsIPrefBranch.PREF_BOOL:
  1.1216 +            pref.type = "bool";
  1.1217 +            pref.value = Services.prefs.getBoolPref(prefName);
  1.1218 +            break;
  1.1219 +          case Ci.nsIPrefBranch.PREF_INT:
  1.1220 +            pref.type = "int";
  1.1221 +            pref.value = Services.prefs.getIntPref(prefName);
  1.1222 +            break;
  1.1223 +          case Ci.nsIPrefBranch.PREF_STRING:
  1.1224 +          default:
  1.1225 +            pref.type = "string";
  1.1226 +            try {
  1.1227 +              // Try in case it's a localized string (will throw an exception if not)
  1.1228 +              pref.value = Services.prefs.getComplexValue(prefName, Ci.nsIPrefLocalizedString).data;
  1.1229 +            } catch (e) {
  1.1230 +              pref.value = Services.prefs.getCharPref(prefName);
  1.1231 +            }
  1.1232 +            break;
  1.1233 +        }
  1.1234 +      } catch (e) {
  1.1235 +        dump("Error reading pref [" + prefName + "]: " + e);
  1.1236 +        // preference does not exist; do not send it
  1.1237 +        continue;
  1.1238 +      }
  1.1239 +
  1.1240 +      // Some Gecko preferences use integers or strings to reference
  1.1241 +      // state instead of directly representing the value.
  1.1242 +      // Since the Java UI uses the type to determine which ui elements
  1.1243 +      // to show and how to handle them, we need to normalize these
  1.1244 +      // preferences to the correct type.
  1.1245 +      switch (prefName) {
  1.1246 +        // (string) index for determining which multiple choice value to display.
  1.1247 +        case "browser.chrome.titlebarMode":
  1.1248 +        case "network.cookie.cookieBehavior":
  1.1249 +        case "font.size.inflation.minTwips":
  1.1250 +        case "home.sync.updateMode":
  1.1251 +          pref.type = "string";
  1.1252 +          pref.value = pref.value.toString();
  1.1253 +          break;
  1.1254 +      }
  1.1255 +
  1.1256 +      prefs.push(pref);
  1.1257 +    }
  1.1258 +
  1.1259 +    sendMessageToJava({
  1.1260 +      type: "Preferences:Data",
  1.1261 +      requestId: aRequestId,    // opaque request identifier, can be any string/int/whatever
  1.1262 +      preferences: prefs
  1.1263 +    });
  1.1264 +  },
  1.1265 +
  1.1266 +  setPreferences: function setPreferences(aPref) {
  1.1267 +    let json = JSON.parse(aPref);
  1.1268 +
  1.1269 +    switch (json.name) {
  1.1270 +      // The plugin pref is actually two separate prefs, so
  1.1271 +      // we need to handle it differently
  1.1272 +      case "plugin.enable":
  1.1273 +        PluginHelper.setPluginPreference(json.value);
  1.1274 +        return;
  1.1275 +
  1.1276 +      // MasterPassword pref is not real, we just need take action and leave
  1.1277 +      case "privacy.masterpassword.enabled":
  1.1278 +        if (MasterPassword.enabled)
  1.1279 +          MasterPassword.removePassword(json.value);
  1.1280 +        else
  1.1281 +          MasterPassword.setPassword(json.value);
  1.1282 +        return;
  1.1283 +
  1.1284 +      // "privacy.donottrackheader" is not "real" pref name, it's used in the setting menu.
  1.1285 +      case "privacy.donottrackheader":
  1.1286 +        switch (json.value) {
  1.1287 +          // Don't tell anything about tracking me
  1.1288 +          case kDoNotTrackPrefState.NO_PREF:
  1.1289 +            Services.prefs.setBoolPref("privacy.donottrackheader.enabled", false);
  1.1290 +            Services.prefs.clearUserPref("privacy.donottrackheader.value");
  1.1291 +            break;
  1.1292 +          // Accept tracking me
  1.1293 +          case kDoNotTrackPrefState.ALLOW_TRACKING:
  1.1294 +            Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true);
  1.1295 +            Services.prefs.setIntPref("privacy.donottrackheader.value", 0);
  1.1296 +            break;
  1.1297 +          // Not accept tracking me
  1.1298 +          case kDoNotTrackPrefState.DISALLOW_TRACKING:
  1.1299 +            Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true);
  1.1300 +            Services.prefs.setIntPref("privacy.donottrackheader.value", 1);
  1.1301 +            break;
  1.1302 +        }
  1.1303 +        return;
  1.1304 +
  1.1305 +      // Enabling or disabling suggestions will prevent future prompts
  1.1306 +      case SearchEngines.PREF_SUGGEST_ENABLED:
  1.1307 +        Services.prefs.setBoolPref(SearchEngines.PREF_SUGGEST_PROMPTED, true);
  1.1308 +        break;
  1.1309 +
  1.1310 +#ifdef MOZ_CRASHREPORTER
  1.1311 +      // Crash reporter preference is in a service; set and return.
  1.1312 +      case "datareporting.crashreporter.submitEnabled":
  1.1313 +        CrashReporter.submitReports = json.value;
  1.1314 +        return;
  1.1315 +#endif
  1.1316 +      // When sending to Java, we normalized special preferences that use
  1.1317 +      // integers and strings to represent booleans. Here, we convert them back
  1.1318 +      // to their actual types so we can store them.
  1.1319 +      case "browser.chrome.titlebarMode":
  1.1320 +      case "network.cookie.cookieBehavior":
  1.1321 +      case "font.size.inflation.minTwips":
  1.1322 +      case "home.sync.updateMode":
  1.1323 +        json.type = "int";
  1.1324 +        json.value = parseInt(json.value);
  1.1325 +        break;
  1.1326 +    }
  1.1327 +
  1.1328 +    switch (json.type) {
  1.1329 +      case "bool":
  1.1330 +        Services.prefs.setBoolPref(json.name, json.value);
  1.1331 +        break;
  1.1332 +      case "int":
  1.1333 +        Services.prefs.setIntPref(json.name, json.value);
  1.1334 +        break;
  1.1335 +      default: {
  1.1336 +        let pref = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
  1.1337 +        pref.data = json.value;
  1.1338 +        Services.prefs.setComplexValue(json.name, Ci.nsISupportsString, pref);
  1.1339 +        break;
  1.1340 +      }
  1.1341 +    }
  1.1342 +  },
  1.1343 +
  1.1344 +  sanitize: function (aItems) {
  1.1345 +    let json = JSON.parse(aItems);
  1.1346 +    let success = true;
  1.1347 +
  1.1348 +    for (let key in json) {
  1.1349 +      if (!json[key])
  1.1350 +        continue;
  1.1351 +
  1.1352 +      try {
  1.1353 +        switch (key) {
  1.1354 +          case "cookies_sessions":
  1.1355 +            Sanitizer.clearItem("cookies");
  1.1356 +            Sanitizer.clearItem("sessions");
  1.1357 +            break;
  1.1358 +          default:
  1.1359 +            Sanitizer.clearItem(key);
  1.1360 +        }
  1.1361 +      } catch (e) {
  1.1362 +        dump("sanitize error: " + e);
  1.1363 +        success = false;
  1.1364 +      }
  1.1365 +    }
  1.1366 +
  1.1367 +    sendMessageToJava({
  1.1368 +      type: "Sanitize:Finished",
  1.1369 +      success: success
  1.1370 +    });
  1.1371 +  },
  1.1372 +
  1.1373 +  getFocusedInput: function(aBrowser, aOnlyInputElements = false) {
  1.1374 +    if (!aBrowser)
  1.1375 +      return null;
  1.1376 +
  1.1377 +    let doc = aBrowser.contentDocument;
  1.1378 +    if (!doc)
  1.1379 +      return null;
  1.1380 +
  1.1381 +    let focused = doc.activeElement;
  1.1382 +    while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) {
  1.1383 +      doc = focused.contentDocument;
  1.1384 +      focused = doc.activeElement;
  1.1385 +    }
  1.1386 +
  1.1387 +    if (focused instanceof HTMLInputElement && focused.mozIsTextField(false))
  1.1388 +      return focused;
  1.1389 +
  1.1390 +    if (aOnlyInputElements)
  1.1391 +      return null;
  1.1392 +
  1.1393 +    if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) {
  1.1394 +
  1.1395 +      if (focused instanceof HTMLBodyElement) {
  1.1396 +        // we are putting focus into a contentEditable frame. scroll the frame into
  1.1397 +        // view instead of the contentEditable document contained within, because that
  1.1398 +        // results in a better user experience
  1.1399 +        focused = focused.ownerDocument.defaultView.frameElement;
  1.1400 +      }
  1.1401 +      return focused;
  1.1402 +    }
  1.1403 +    return null;
  1.1404 +  },
  1.1405 +
  1.1406 +  scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
  1.1407 +    let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
  1.1408 +    if (formHelperMode == kFormHelperModeDisabled)
  1.1409 +      return;
  1.1410 +
  1.1411 +    let focused = this.getFocusedInput(aBrowser);
  1.1412 +
  1.1413 +    if (focused) {
  1.1414 +      let shouldZoom = Services.prefs.getBoolPref("formhelper.autozoom");
  1.1415 +      if (formHelperMode == kFormHelperModeDynamic && this.isTablet)
  1.1416 +        shouldZoom = false;
  1.1417 +      // ZoomHelper.zoomToElement will handle not sending any message if this input is already mostly filling the screen
  1.1418 +      ZoomHelper.zoomToElement(focused, -1, false,
  1.1419 +          aAllowZoom && shouldZoom && !ViewportHandler.getViewportMetadata(aBrowser.contentWindow).isSpecified);
  1.1420 +    }
  1.1421 +  },
  1.1422 +
  1.1423 +  observe: function(aSubject, aTopic, aData) {
  1.1424 +    let browser = this.selectedBrowser;
  1.1425 +
  1.1426 +    switch (aTopic) {
  1.1427 +
  1.1428 +      case "Session:Back":
  1.1429 +        browser.goBack();
  1.1430 +        break;
  1.1431 +
  1.1432 +      case "Session:Forward":
  1.1433 +        browser.goForward();
  1.1434 +        break;
  1.1435 +
  1.1436 +      case "Session:Reload": {
  1.1437 +        let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
  1.1438 +
  1.1439 +        // Check to see if this is a message to enable/disable mixed content blocking.
  1.1440 +        if (aData) {
  1.1441 +          let allowMixedContent = JSON.parse(aData).allowMixedContent;
  1.1442 +          if (allowMixedContent) {
  1.1443 +            // Set a flag to disable mixed content blocking.
  1.1444 +            flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
  1.1445 +          } else {
  1.1446 +            // Set mixedContentChannel to null to re-enable mixed content blocking.
  1.1447 +            let docShell = browser.webNavigation.QueryInterface(Ci.nsIDocShell);
  1.1448 +            docShell.mixedContentChannel = null;
  1.1449 +          }
  1.1450 +        }
  1.1451 +
  1.1452 +        // Try to use the session history to reload so that framesets are
  1.1453 +        // handled properly. If the window has no session history, fall back
  1.1454 +        // to using the web navigation's reload method.
  1.1455 +        let webNav = browser.webNavigation;
  1.1456 +        try {
  1.1457 +          let sh = webNav.sessionHistory;
  1.1458 +          if (sh)
  1.1459 +            webNav = sh.QueryInterface(Ci.nsIWebNavigation);
  1.1460 +        } catch (e) {}
  1.1461 +        webNav.reload(flags);
  1.1462 +        break;
  1.1463 +      }
  1.1464 +
  1.1465 +      case "Session:Stop":
  1.1466 +        browser.stop();
  1.1467 +        break;
  1.1468 +
  1.1469 +      case "Session:ShowHistory": {
  1.1470 +        let data = JSON.parse(aData);
  1.1471 +        this.showHistory(data.fromIndex, data.toIndex, data.selIndex);
  1.1472 +        break;
  1.1473 +      }
  1.1474 +
  1.1475 +      case "Tab:Load": {
  1.1476 +        let data = JSON.parse(aData);
  1.1477 +
  1.1478 +        // Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from
  1.1479 +        // inheriting the currently loaded document's principal.
  1.1480 +        let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
  1.1481 +                    Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
  1.1482 +        if (data.userEntered) {
  1.1483 +          flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER;
  1.1484 +        }
  1.1485 +
  1.1486 +        let delayLoad = ("delayLoad" in data) ? data.delayLoad : false;
  1.1487 +        let params = {
  1.1488 +          selected: ("selected" in data) ? data.selected : !delayLoad,
  1.1489 +          parentId: ("parentId" in data) ? data.parentId : -1,
  1.1490 +          flags: flags,
  1.1491 +          tabID: data.tabID,
  1.1492 +          isPrivate: (data.isPrivate === true),
  1.1493 +          pinned: (data.pinned === true),
  1.1494 +          delayLoad: (delayLoad === true),
  1.1495 +          desktopMode: (data.desktopMode === true)
  1.1496 +        };
  1.1497 +
  1.1498 +        let url = data.url;
  1.1499 +        if (data.engine) {
  1.1500 +          let engine = Services.search.getEngineByName(data.engine);
  1.1501 +          if (engine) {
  1.1502 +            params.userSearch = url;
  1.1503 +            let submission = engine.getSubmission(url);
  1.1504 +            url = submission.uri.spec;
  1.1505 +            params.postData = submission.postData;
  1.1506 +          }
  1.1507 +        }
  1.1508 +
  1.1509 +        if (data.newTab) {
  1.1510 +          this.addTab(url, params);
  1.1511 +        } else {
  1.1512 +          if (data.tabId) {
  1.1513 +            // Use a specific browser instead of the selected browser, if it exists
  1.1514 +            let specificBrowser = this.getTabForId(data.tabId).browser;
  1.1515 +            if (specificBrowser)
  1.1516 +              browser = specificBrowser;
  1.1517 +          }
  1.1518 +          this.loadURI(url, browser, params);
  1.1519 +        }
  1.1520 +        break;
  1.1521 +      }
  1.1522 +
  1.1523 +      case "Tab:Selected":
  1.1524 +        this._handleTabSelected(this.getTabForId(parseInt(aData)));
  1.1525 +        break;
  1.1526 +
  1.1527 +      case "Tab:Closed":
  1.1528 +        this._handleTabClosed(this.getTabForId(parseInt(aData)));
  1.1529 +        break;
  1.1530 +
  1.1531 +      case "keyword-search":
  1.1532 +        // This event refers to a search via the URL bar, not a bookmarks
  1.1533 +        // keyword search. Note that this code assumes that the user can only
  1.1534 +        // perform a keyword search on the selected tab.
  1.1535 +        this.selectedTab.userSearch = aData;
  1.1536 +
  1.1537 +        let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
  1.1538 +        sendMessageToJava({
  1.1539 +          type: "Search:Keyword",
  1.1540 +          identifier: engine.identifier,
  1.1541 +          name: engine.name,
  1.1542 +        });
  1.1543 +        break;
  1.1544 +
  1.1545 +      case "Browser:Quit":
  1.1546 +        this.quit();
  1.1547 +        break;
  1.1548 +
  1.1549 +      case "SaveAs:PDF":
  1.1550 +        this.saveAsPDF(browser);
  1.1551 +        break;
  1.1552 +
  1.1553 +      case "Preferences:Set":
  1.1554 +        this.setPreferences(aData);
  1.1555 +        break;
  1.1556 +
  1.1557 +      case "ScrollTo:FocusedInput":
  1.1558 +        // these messages come from a change in the viewable area and not user interaction
  1.1559 +        // we allow scrolling to the selected input, but not zooming the page
  1.1560 +        this.scrollToFocusedInput(browser, false);
  1.1561 +        break;
  1.1562 +
  1.1563 +      case "Sanitize:ClearData":
  1.1564 +        this.sanitize(aData);
  1.1565 +        break;
  1.1566 +
  1.1567 +      case "FullScreen:Exit":
  1.1568 +        browser.contentDocument.mozCancelFullScreen();
  1.1569 +        break;
  1.1570 +
  1.1571 +      case "Viewport:Change":
  1.1572 +        if (this.isBrowserContentDocumentDisplayed())
  1.1573 +          this.selectedTab.setViewport(JSON.parse(aData));
  1.1574 +        break;
  1.1575 +
  1.1576 +      case "Viewport:Flush":
  1.1577 +        this.contentDocumentChanged();
  1.1578 +        break;
  1.1579 +
  1.1580 +      case "Passwords:Init": {
  1.1581 +        let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"].
  1.1582 +                      getService(Ci.nsILoginManagerStorage);
  1.1583 +        storage.init();
  1.1584 +        Services.obs.removeObserver(this, "Passwords:Init");
  1.1585 +        break;
  1.1586 +      }
  1.1587 +
  1.1588 +      case "FormHistory:Init": {
  1.1589 +        // Force creation/upgrade of formhistory.sqlite
  1.1590 +        FormHistory.count({});
  1.1591 +        Services.obs.removeObserver(this, "FormHistory:Init");
  1.1592 +        break;
  1.1593 +      }
  1.1594 +
  1.1595 +      case "sessionstore-state-purge-complete":
  1.1596 +        sendMessageToJava({ type: "Session:StatePurged" });
  1.1597 +        break;
  1.1598 +
  1.1599 +      case "gather-telemetry":
  1.1600 +        sendMessageToJava({ type: "Telemetry:Gather" });
  1.1601 +        break;
  1.1602 +
  1.1603 +      case "Viewport:FixedMarginsChanged":
  1.1604 +        gViewportMargins = JSON.parse(aData);
  1.1605 +        this.selectedTab.updateViewportSize(gScreenWidth);
  1.1606 +        break;
  1.1607 +
  1.1608 +      case "nsPref:changed":
  1.1609 +        this.notifyPrefObservers(aData);
  1.1610 +        break;
  1.1611 +
  1.1612 +#ifdef MOZ_ANDROID_SYNTHAPKS
  1.1613 +      case "webapps-runtime-install":
  1.1614 +        WebappManager.install(JSON.parse(aData), aSubject);
  1.1615 +        break;
  1.1616 +
  1.1617 +      case "webapps-runtime-install-package":
  1.1618 +        WebappManager.installPackage(JSON.parse(aData), aSubject);
  1.1619 +        break;
  1.1620 +
  1.1621 +      case "webapps-ask-install":
  1.1622 +        WebappManager.askInstall(JSON.parse(aData));
  1.1623 +        break;
  1.1624 +
  1.1625 +      case "webapps-launch": {
  1.1626 +        WebappManager.launch(JSON.parse(aData));
  1.1627 +        break;
  1.1628 +      }
  1.1629 +
  1.1630 +      case "webapps-uninstall": {
  1.1631 +        WebappManager.uninstall(JSON.parse(aData));
  1.1632 +        break;
  1.1633 +      }
  1.1634 +
  1.1635 +      case "Webapps:AutoInstall":
  1.1636 +        WebappManager.autoInstall(JSON.parse(aData));
  1.1637 +        break;
  1.1638 +
  1.1639 +      case "Webapps:Load":
  1.1640 +        this._loadWebapp(JSON.parse(aData));
  1.1641 +        break;
  1.1642 +
  1.1643 +      case "Webapps:AutoUninstall":
  1.1644 +        WebappManager.autoUninstall(JSON.parse(aData));
  1.1645 +        break;
  1.1646 +#endif
  1.1647 +
  1.1648 +      case "Locale:Changed":
  1.1649 +        // The value provided to Locale:Changed should be a BCP47 language tag
  1.1650 +        // understood by Gecko -- for example, "es-ES" or "de".
  1.1651 +        console.log("Locale:Changed: " + aData);
  1.1652 +
  1.1653 +        // TODO: do we need to be more nuanced here -- e.g., checking for the
  1.1654 +        // OS locale -- or should it always be false on Fennec?
  1.1655 +        Services.prefs.setBoolPref("intl.locale.matchOS", false);
  1.1656 +        Services.prefs.setCharPref("general.useragent.locale", aData);
  1.1657 +        break;
  1.1658 +
  1.1659 +      default:
  1.1660 +        dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
  1.1661 +        break;
  1.1662 +
  1.1663 +    }
  1.1664 +  },
  1.1665 +
  1.1666 +  get defaultBrowserWidth() {
  1.1667 +    delete this.defaultBrowserWidth;
  1.1668 +    let width = Services.prefs.getIntPref("browser.viewport.desktopWidth");
  1.1669 +    return this.defaultBrowserWidth = width;
  1.1670 +  },
  1.1671 +
  1.1672 +  // nsIAndroidBrowserApp
  1.1673 +  getBrowserTab: function(tabId) {
  1.1674 +    return this.getTabForId(tabId);
  1.1675 +  },
  1.1676 +
  1.1677 +  getUITelemetryObserver: function() {
  1.1678 +    return UITelemetry;
  1.1679 +  },
  1.1680 +
  1.1681 +  getPreferences: function getPreferences(requestId, prefNames, count) {
  1.1682 +    this.handlePreferencesRequest(requestId, prefNames, false);
  1.1683 +  },
  1.1684 +
  1.1685 +  observePreferences: function observePreferences(requestId, prefNames, count) {
  1.1686 +    this.handlePreferencesRequest(requestId, prefNames, true);
  1.1687 +  },
  1.1688 +
  1.1689 +  removePreferenceObservers: function removePreferenceObservers(aRequestId) {
  1.1690 +    let newPrefObservers = [];
  1.1691 +    for (let prefName in this._prefObservers) {
  1.1692 +      let requestIds = this._prefObservers[prefName];
  1.1693 +      // Remove the requestID from the preference handlers
  1.1694 +      let i = requestIds.indexOf(aRequestId);
  1.1695 +      if (i >= 0) {
  1.1696 +        requestIds.splice(i, 1);
  1.1697 +      }
  1.1698 +
  1.1699 +      // If there are no more request IDs, remove the observer
  1.1700 +      if (requestIds.length == 0) {
  1.1701 +        Services.prefs.removeObserver(prefName, this);
  1.1702 +      } else {
  1.1703 +        newPrefObservers[prefName] = requestIds;
  1.1704 +      }
  1.1705 +    }
  1.1706 +    this._prefObservers = newPrefObservers;
  1.1707 +  },
  1.1708 +
  1.1709 +  // This method will print a list from fromIndex to toIndex, optionally
  1.1710 +  // selecting selIndex(if fromIndex<=selIndex<=toIndex)
  1.1711 +  showHistory: function(fromIndex, toIndex, selIndex) {
  1.1712 +    let browser = this.selectedBrowser;
  1.1713 +    let hist = browser.sessionHistory;
  1.1714 +    let listitems = [];
  1.1715 +    for (let i = toIndex; i >= fromIndex; i--) {
  1.1716 +      let entry = hist.getEntryAtIndex(i, false);
  1.1717 +      let item = {
  1.1718 +        label: entry.title || entry.URI.spec,
  1.1719 +        selected: (i == selIndex)
  1.1720 +      };
  1.1721 +      listitems.push(item);
  1.1722 +    }
  1.1723 +
  1.1724 +    let p = new Prompt({
  1.1725 +      window: browser.contentWindow
  1.1726 +    }).setSingleChoiceItems(listitems).show(function(data) {
  1.1727 +        let selected = data.button;
  1.1728 +        if (selected == -1)
  1.1729 +          return;
  1.1730 +
  1.1731 +        browser.gotoIndex(toIndex-selected);
  1.1732 +    });
  1.1733 +  },
  1.1734 +};
  1.1735 +
  1.1736 +var NativeWindow = {
  1.1737 +  init: function() {
  1.1738 +    Services.obs.addObserver(this, "Menu:Clicked", false);
  1.1739 +    Services.obs.addObserver(this, "PageActions:Clicked", false);
  1.1740 +    Services.obs.addObserver(this, "PageActions:LongClicked", false);
  1.1741 +    Services.obs.addObserver(this, "Doorhanger:Reply", false);
  1.1742 +    Services.obs.addObserver(this, "Toast:Click", false);
  1.1743 +    Services.obs.addObserver(this, "Toast:Hidden", false);
  1.1744 +    this.contextmenus.init();
  1.1745 +  },
  1.1746 +
  1.1747 +  uninit: function() {
  1.1748 +    Services.obs.removeObserver(this, "Menu:Clicked");
  1.1749 +    Services.obs.removeObserver(this, "PageActions:Clicked");
  1.1750 +    Services.obs.removeObserver(this, "PageActions:LongClicked");
  1.1751 +    Services.obs.removeObserver(this, "Doorhanger:Reply");
  1.1752 +    Services.obs.removeObserver(this, "Toast:Click", false);
  1.1753 +    Services.obs.removeObserver(this, "Toast:Hidden", false);
  1.1754 +    this.contextmenus.uninit();
  1.1755 +  },
  1.1756 +
  1.1757 +  loadDex: function(zipFile, implClass) {
  1.1758 +    sendMessageToJava({
  1.1759 +      type: "Dex:Load",
  1.1760 +      zipfile: zipFile,
  1.1761 +      impl: implClass || "Main"
  1.1762 +    });
  1.1763 +  },
  1.1764 +
  1.1765 +  unloadDex: function(zipFile) {
  1.1766 +    sendMessageToJava({
  1.1767 +      type: "Dex:Unload",
  1.1768 +      zipfile: zipFile
  1.1769 +    });
  1.1770 +  },
  1.1771 +
  1.1772 +  toast: {
  1.1773 +    _callbacks: {},
  1.1774 +    show: function(aMessage, aDuration, aOptions) {
  1.1775 +      let msg = {
  1.1776 +        type: "Toast:Show",
  1.1777 +        message: aMessage,
  1.1778 +        duration: aDuration
  1.1779 +      };
  1.1780 +
  1.1781 +      if (aOptions && aOptions.button) {
  1.1782 +        msg.button = {
  1.1783 +          label: aOptions.button.label,
  1.1784 +          id: uuidgen.generateUUID().toString(),
  1.1785 +          // If the caller specified a button, make sure we convert any chrome urls
  1.1786 +          // to jar:jar urls so that the frontend can show them
  1.1787 +          icon: aOptions.button.icon ? resolveGeckoURI(aOptions.button.icon) : null,
  1.1788 +        };
  1.1789 +        this._callbacks[msg.button.id] = aOptions.button.callback;
  1.1790 +      }
  1.1791 +
  1.1792 +      sendMessageToJava(msg);
  1.1793 +    }
  1.1794 +  },
  1.1795 +
  1.1796 +  pageactions: {
  1.1797 +    _items: { },
  1.1798 +    add: function(aOptions) {
  1.1799 +      let id = uuidgen.generateUUID().toString();
  1.1800 +      sendMessageToJava({
  1.1801 +        type: "PageActions:Add",
  1.1802 +        id: id,
  1.1803 +        title: aOptions.title,
  1.1804 +        icon: resolveGeckoURI(aOptions.icon),
  1.1805 +        important: "important" in aOptions ? aOptions.important : false
  1.1806 +      });
  1.1807 +      this._items[id] = {
  1.1808 +        clickCallback: aOptions.clickCallback,
  1.1809 +        longClickCallback: aOptions.longClickCallback
  1.1810 +      };
  1.1811 +      return id;
  1.1812 +    },
  1.1813 +    remove: function(id) {
  1.1814 +      sendMessageToJava({
  1.1815 +        type: "PageActions:Remove",
  1.1816 +        id: id
  1.1817 +      });
  1.1818 +      delete this._items[id];
  1.1819 +    }
  1.1820 +  },
  1.1821 +
  1.1822 +  menu: {
  1.1823 +    _callbacks: [],
  1.1824 +    _menuId: 1,
  1.1825 +    toolsMenuID: -1,
  1.1826 +    add: function() {
  1.1827 +      let options;
  1.1828 +      if (arguments.length == 1) {
  1.1829 +        options = arguments[0];
  1.1830 +      } else if (arguments.length == 3) {
  1.1831 +          options = {
  1.1832 +            name: arguments[0],
  1.1833 +            icon: arguments[1],
  1.1834 +            callback: arguments[2]
  1.1835 +          };
  1.1836 +      } else {
  1.1837 +         throw "Incorrect number of parameters";
  1.1838 +      }
  1.1839 +
  1.1840 +      options.type = "Menu:Add";
  1.1841 +      options.id = this._menuId;
  1.1842 +
  1.1843 +      sendMessageToJava(options);
  1.1844 +      this._callbacks[this._menuId] = options.callback;
  1.1845 +      this._menuId++;
  1.1846 +      return this._menuId - 1;
  1.1847 +    },
  1.1848 +
  1.1849 +    remove: function(aId) {
  1.1850 +      sendMessageToJava({ type: "Menu:Remove", id: aId });
  1.1851 +    },
  1.1852 +
  1.1853 +    update: function(aId, aOptions) {
  1.1854 +      if (!aOptions)
  1.1855 +        return;
  1.1856 +
  1.1857 +      sendMessageToJava({
  1.1858 +        type: "Menu:Update", 
  1.1859 +        id: aId,
  1.1860 +        options: aOptions
  1.1861 +      });
  1.1862 +    }
  1.1863 +  },
  1.1864 +
  1.1865 +  doorhanger: {
  1.1866 +    _callbacks: {},
  1.1867 +    _callbacksId: 0,
  1.1868 +    _promptId: 0,
  1.1869 +
  1.1870 +  /**
  1.1871 +   * @param aOptions
  1.1872 +   *        An options JavaScript object holding additional properties for the
  1.1873 +   *        notification. The following properties are currently supported:
  1.1874 +   *        persistence: An integer. The notification will not automatically
  1.1875 +   *                     dismiss for this many page loads. If persistence is set
  1.1876 +   *                     to -1, the doorhanger will never automatically dismiss.
  1.1877 +   *        persistWhileVisible:
  1.1878 +   *                     A boolean. If true, a visible notification will always
  1.1879 +   *                     persist across location changes.
  1.1880 +   *        timeout:     A time in milliseconds. The notification will not
  1.1881 +   *                     automatically dismiss before this time.
  1.1882 +   *        checkbox:    A string to appear next to a checkbox under the notification
  1.1883 +   *                     message. The button callback functions will be called with
  1.1884 +   *                     the checked state as an argument.                   
  1.1885 +   */
  1.1886 +    show: function(aMessage, aValue, aButtons, aTabID, aOptions) {
  1.1887 +      if (aButtons == null) {
  1.1888 +        aButtons = [];
  1.1889 +      }
  1.1890 +
  1.1891 +      aButtons.forEach((function(aButton) {
  1.1892 +        this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId };
  1.1893 +        aButton.callback = this._callbacksId;
  1.1894 +        this._callbacksId++;
  1.1895 +      }).bind(this));
  1.1896 +
  1.1897 +      this._promptId++;
  1.1898 +      let json = {
  1.1899 +        type: "Doorhanger:Add",
  1.1900 +        message: aMessage,
  1.1901 +        value: aValue,
  1.1902 +        buttons: aButtons,
  1.1903 +        // use the current tab if none is provided
  1.1904 +        tabID: aTabID || BrowserApp.selectedTab.id,
  1.1905 +        options: aOptions || {}
  1.1906 +      };
  1.1907 +      sendMessageToJava(json);
  1.1908 +    },
  1.1909 +
  1.1910 +    hide: function(aValue, aTabID) {
  1.1911 +      sendMessageToJava({
  1.1912 +        type: "Doorhanger:Remove",
  1.1913 +        value: aValue,
  1.1914 +        tabID: aTabID
  1.1915 +      });
  1.1916 +    }
  1.1917 +  },
  1.1918 +
  1.1919 +  observe: function(aSubject, aTopic, aData) {
  1.1920 +    if (aTopic == "Menu:Clicked") {
  1.1921 +      if (this.menu._callbacks[aData])
  1.1922 +        this.menu._callbacks[aData]();
  1.1923 +    } else if (aTopic == "PageActions:Clicked") {
  1.1924 +        if (this.pageactions._items[aData].clickCallback)
  1.1925 +          this.pageactions._items[aData].clickCallback();
  1.1926 +    } else if (aTopic == "PageActions:LongClicked") {
  1.1927 +        if (this.pageactions._items[aData].longClickCallback)
  1.1928 +          this.pageactions._items[aData].longClickCallback();
  1.1929 +    } else if (aTopic == "Toast:Click") {
  1.1930 +      if (this.toast._callbacks[aData]) {
  1.1931 +        this.toast._callbacks[aData]();
  1.1932 +        delete this.toast._callbacks[aData];
  1.1933 +      }
  1.1934 +    } else if (aTopic == "Toast:Hidden") {
  1.1935 +      if (this.toast._callbacks[aData])
  1.1936 +        delete this.toast._callbacks[aData];
  1.1937 +    } else if (aTopic == "Doorhanger:Reply") {
  1.1938 +      let data = JSON.parse(aData);
  1.1939 +      let reply_id = data["callback"];
  1.1940 +
  1.1941 +      if (this.doorhanger._callbacks[reply_id]) {
  1.1942 +        // Pass the value of the optional checkbox to the callback
  1.1943 +        let checked = data["checked"];
  1.1944 +        this.doorhanger._callbacks[reply_id].cb(checked, data.inputs);
  1.1945 +
  1.1946 +        let prompt = this.doorhanger._callbacks[reply_id].prompt;
  1.1947 +        for (let id in this.doorhanger._callbacks) {
  1.1948 +          if (this.doorhanger._callbacks[id].prompt == prompt) {
  1.1949 +            delete this.doorhanger._callbacks[id];
  1.1950 +          }
  1.1951 +        }
  1.1952 +      }
  1.1953 +    }
  1.1954 +  },
  1.1955 +  contextmenus: {
  1.1956 +    items: {}, //  a list of context menu items that we may show
  1.1957 +    DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items
  1.1958 +
  1.1959 +    init: function() {
  1.1960 +      Services.obs.addObserver(this, "Gesture:LongPress", false);
  1.1961 +    },
  1.1962 +
  1.1963 +    uninit: function() {
  1.1964 +      Services.obs.removeObserver(this, "Gesture:LongPress");
  1.1965 +    },
  1.1966 +
  1.1967 +    add: function() {
  1.1968 +      let args;
  1.1969 +      if (arguments.length == 1) {
  1.1970 +        args = arguments[0];
  1.1971 +      } else if (arguments.length == 3) {
  1.1972 +        args = {
  1.1973 +          label : arguments[0],
  1.1974 +          selector: arguments[1],
  1.1975 +          callback: arguments[2]
  1.1976 +        };
  1.1977 +      } else {
  1.1978 +        throw "Incorrect number of parameters";
  1.1979 +      }
  1.1980 +
  1.1981 +      if (!args.label)
  1.1982 +        throw "Menu items must have a name";
  1.1983 +
  1.1984 +      let cmItem = new ContextMenuItem(args);
  1.1985 +      this.items[cmItem.id] = cmItem;
  1.1986 +      return cmItem.id;
  1.1987 +    },
  1.1988 +
  1.1989 +    remove: function(aId) {
  1.1990 +      delete this.items[aId];
  1.1991 +    },
  1.1992 +
  1.1993 +    SelectorContext: function(aSelector) {
  1.1994 +      return {
  1.1995 +        matches: function(aElt) {
  1.1996 +          if (aElt.mozMatchesSelector)
  1.1997 +            return aElt.mozMatchesSelector(aSelector);
  1.1998 +          return false;
  1.1999 +        }
  1.2000 +      };
  1.2001 +    },
  1.2002 +
  1.2003 +    linkOpenableNonPrivateContext: {
  1.2004 +      matches: function linkOpenableNonPrivateContextMatches(aElement) {
  1.2005 +        let doc = aElement.ownerDocument;
  1.2006 +        if (!doc || PrivateBrowsingUtils.isWindowPrivate(doc.defaultView)) {
  1.2007 +          return false;
  1.2008 +        }
  1.2009 +
  1.2010 +        return NativeWindow.contextmenus.linkOpenableContext.matches(aElement);
  1.2011 +      }
  1.2012 +    },
  1.2013 +
  1.2014 +    linkOpenableContext: {
  1.2015 +      matches: function linkOpenableContextMatches(aElement) {
  1.2016 +        let uri = NativeWindow.contextmenus._getLink(aElement);
  1.2017 +        if (uri) {
  1.2018 +          let scheme = uri.scheme;
  1.2019 +          let dontOpen = /^(javascript|mailto|news|snews|tel)$/;
  1.2020 +          return (scheme && !dontOpen.test(scheme));
  1.2021 +        }
  1.2022 +        return false;
  1.2023 +      }
  1.2024 +    },
  1.2025 +
  1.2026 +    linkCopyableContext: {
  1.2027 +      matches: function linkCopyableContextMatches(aElement) {
  1.2028 +        let uri = NativeWindow.contextmenus._getLink(aElement);
  1.2029 +        if (uri) {
  1.2030 +          let scheme = uri.scheme;
  1.2031 +          let dontCopy = /^(mailto|tel)$/;
  1.2032 +          return (scheme && !dontCopy.test(scheme));
  1.2033 +        }
  1.2034 +        return false;
  1.2035 +      }
  1.2036 +    },
  1.2037 +
  1.2038 +    linkShareableContext: {
  1.2039 +      matches: function linkShareableContextMatches(aElement) {
  1.2040 +        let uri = NativeWindow.contextmenus._getLink(aElement);
  1.2041 +        if (uri) {
  1.2042 +          let scheme = uri.scheme;
  1.2043 +          let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/;
  1.2044 +          return (scheme && !dontShare.test(scheme));
  1.2045 +        }
  1.2046 +        return false;
  1.2047 +      }
  1.2048 +    },
  1.2049 +
  1.2050 +    linkBookmarkableContext: {
  1.2051 +      matches: function linkBookmarkableContextMatches(aElement) {
  1.2052 +        let uri = NativeWindow.contextmenus._getLink(aElement);
  1.2053 +        if (uri) {
  1.2054 +          let scheme = uri.scheme;
  1.2055 +          let dontBookmark = /^(mailto|tel)$/;
  1.2056 +          return (scheme && !dontBookmark.test(scheme));
  1.2057 +        }
  1.2058 +        return false;
  1.2059 +      }
  1.2060 +    },
  1.2061 +
  1.2062 +    emailLinkContext: {
  1.2063 +      matches: function emailLinkContextMatches(aElement) {
  1.2064 +        let uri = NativeWindow.contextmenus._getLink(aElement);
  1.2065 +        if (uri)
  1.2066 +          return uri.schemeIs("mailto");
  1.2067 +        return false;
  1.2068 +      }
  1.2069 +    },
  1.2070 +
  1.2071 +    phoneNumberLinkContext: {
  1.2072 +      matches: function phoneNumberLinkContextMatches(aElement) {
  1.2073 +        let uri = NativeWindow.contextmenus._getLink(aElement);
  1.2074 +        if (uri)
  1.2075 +          return uri.schemeIs("tel");
  1.2076 +        return false;
  1.2077 +      }
  1.2078 +    },
  1.2079 +
  1.2080 +    imageLocationCopyableContext: {
  1.2081 +      matches: function imageLinkCopyableContextMatches(aElement) {
  1.2082 +        return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI);
  1.2083 +      }
  1.2084 +    },
  1.2085 +
  1.2086 +    imageSaveableContext: {
  1.2087 +      matches: function imageSaveableContextMatches(aElement) {
  1.2088 +        if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) {
  1.2089 +          // The image must be loaded to allow saving
  1.2090 +          let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
  1.2091 +          return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE));
  1.2092 +        }
  1.2093 +        return false;
  1.2094 +      }
  1.2095 +    },
  1.2096 +
  1.2097 +    mediaSaveableContext: {
  1.2098 +      matches: function mediaSaveableContextMatches(aElement) {
  1.2099 +        return (aElement instanceof HTMLVideoElement ||
  1.2100 +               aElement instanceof HTMLAudioElement);
  1.2101 +      }
  1.2102 +    },
  1.2103 +
  1.2104 +    mediaContext: function(aMode) {
  1.2105 +      return {
  1.2106 +        matches: function(aElt) {
  1.2107 +          if (aElt instanceof Ci.nsIDOMHTMLMediaElement) {
  1.2108 +            let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE;
  1.2109 +            if (hasError)
  1.2110 +              return false;
  1.2111 +
  1.2112 +            let paused = aElt.paused || aElt.ended;
  1.2113 +            if (paused && aMode == "media-paused")
  1.2114 +              return true;
  1.2115 +            if (!paused && aMode == "media-playing")
  1.2116 +              return true;
  1.2117 +            let controls = aElt.controls;
  1.2118 +            if (!controls && aMode == "media-hidingcontrols")
  1.2119 +              return true;
  1.2120 +
  1.2121 +            let muted = aElt.muted;
  1.2122 +            if (muted && aMode == "media-muted")
  1.2123 +              return true;
  1.2124 +            else if (!muted && aMode == "media-unmuted")
  1.2125 +              return true;
  1.2126 +          }
  1.2127 +          return false;
  1.2128 +        }
  1.2129 +      };
  1.2130 +    },
  1.2131 +
  1.2132 +    /* Holds a WeakRef to the original target element this context menu was shown for.
  1.2133 +     * Most API's will have to walk up the tree from this node to find the correct element
  1.2134 +     * to act on
  1.2135 +     */
  1.2136 +    get _target() {
  1.2137 +      if (this._targetRef)
  1.2138 +        return this._targetRef.get();
  1.2139 +      return null;
  1.2140 +    },
  1.2141 +
  1.2142 +    set _target(aTarget) {
  1.2143 +      if (aTarget)
  1.2144 +        this._targetRef = Cu.getWeakReference(aTarget);
  1.2145 +      else this._targetRef = null;
  1.2146 +    },
  1.2147 +
  1.2148 +    get defaultContext() {
  1.2149 +      delete this.defaultContext;
  1.2150 +      return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default");
  1.2151 +    },
  1.2152 +
  1.2153 +    /* Gets menuitems for an arbitrary node
  1.2154 +     * Parameters:
  1.2155 +     *   element - The element to look at. If this element has a contextmenu attribute, the
  1.2156 +     *             corresponding contextmenu will be used.
  1.2157 +     */
  1.2158 +    _getHTMLContextMenuItemsForElement: function(element) {
  1.2159 +      let htmlMenu = element.contextMenu;
  1.2160 +      if (!htmlMenu) {
  1.2161 +        return [];
  1.2162 +      }
  1.2163 +
  1.2164 +      htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
  1.2165 +      htmlMenu.sendShowEvent();
  1.2166 +
  1.2167 +      return this._getHTMLContextMenuItemsForMenu(htmlMenu, element);
  1.2168 +    },
  1.2169 +
  1.2170 +    /* Add a menuitem for an HTML <menu> node
  1.2171 +     * Parameters:
  1.2172 +     *   menu - The <menu> element to iterate through for menuitems
  1.2173 +     *   target - The target element these context menu items are attached to
  1.2174 +     */
  1.2175 +    _getHTMLContextMenuItemsForMenu: function(menu, target) {
  1.2176 +      let items = [];
  1.2177 +      for (let i = 0; i < menu.childNodes.length; i++) {
  1.2178 +        let elt = menu.childNodes[i];
  1.2179 +        if (!elt.label)
  1.2180 +          continue;
  1.2181 +
  1.2182 +        items.push(new HTMLContextMenuItem(elt, target));
  1.2183 +      }
  1.2184 +
  1.2185 +      return items;
  1.2186 +    },
  1.2187 +
  1.2188 +    // Searches the current list of menuitems to show for any that match this id
  1.2189 +    _findMenuItem: function(aId) {
  1.2190 +      if (!this.menus) {
  1.2191 +        return null;
  1.2192 +      }
  1.2193 +
  1.2194 +      for (let context in this.menus) {
  1.2195 +        let menu = this.menus[context];
  1.2196 +        for (let i = 0; i < menu.length; i++) {
  1.2197 +          if (menu[i].id === aId) {
  1.2198 +            return menu[i];
  1.2199 +          }
  1.2200 +        }
  1.2201 +      }
  1.2202 +      return null;
  1.2203 +    },
  1.2204 +
  1.2205 +    // Returns true if there are any context menu items to show
  1.2206 +    shouldShow: function() {
  1.2207 +      for (let context in this.menus) {
  1.2208 +        let menu = this.menus[context];
  1.2209 +        if (menu.length > 0) {
  1.2210 +          return true;
  1.2211 +        }
  1.2212 +      }
  1.2213 +      return false;
  1.2214 +    },
  1.2215 +
  1.2216 +    /* Returns a label to be shown in a tabbed ui if there are multiple "contexts". For instance, if this
  1.2217 +     * is an image inside an <a> tag, we may have a "link" context and an "image" one.
  1.2218 +     */
  1.2219 +    _getContextType: function(element) {
  1.2220 +      // For anchor nodes, we try to use the scheme to pick a string
  1.2221 +      if (element instanceof Ci.nsIDOMHTMLAnchorElement) {
  1.2222 +        let uri = this.makeURI(this._getLinkURL(element));
  1.2223 +        try {
  1.2224 +          return Strings.browser.GetStringFromName("browser.menu.context." + uri.scheme);
  1.2225 +        } catch(ex) { }
  1.2226 +      }
  1.2227 +
  1.2228 +      // Otherwise we try the nodeName
  1.2229 +      try {
  1.2230 +        return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase());
  1.2231 +      } catch(ex) { }
  1.2232 +
  1.2233 +      // Fallback to the default
  1.2234 +      return this.defaultContext;
  1.2235 +    },
  1.2236 +
  1.2237 +    // Adds context menu items added through the add-on api
  1.2238 +    _getNativeContextMenuItems: function(element, x, y) {
  1.2239 +      let res = [];
  1.2240 +      for (let itemId of Object.keys(this.items)) {
  1.2241 +        let item = this.items[itemId];
  1.2242 +
  1.2243 +        if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
  1.2244 +          res.push(item);
  1.2245 +        }
  1.2246 +      }
  1.2247 +
  1.2248 +      return res;
  1.2249 +    },
  1.2250 +
  1.2251 +    /* Checks if there are context menu items to show, and if it finds them
  1.2252 +     * sends a contextmenu event to content. We also send showing events to
  1.2253 +     * any html5 context menus we are about to show, and fire some local notifications
  1.2254 +     * for chrome consumers to do lazy menuitem construction
  1.2255 +     */
  1.2256 +    _sendToContent: function(x, y) {
  1.2257 +      let target = BrowserEventHandler._highlightElement || ElementTouchHelper.elementFromPoint(x, y);
  1.2258 +      if (!target)
  1.2259 +        target = ElementTouchHelper.anyElementFromPoint(x, y);
  1.2260 +
  1.2261 +      if (!target)
  1.2262 +        return;
  1.2263 +
  1.2264 +      this._target = target;
  1.2265 +
  1.2266 +      Services.obs.notifyObservers(null, "before-build-contextmenu", "");
  1.2267 +      this._buildMenu(x, y);
  1.2268 +
  1.2269 +      // only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap)
  1.2270 +      if (this.shouldShow()) {
  1.2271 +        let event = target.ownerDocument.createEvent("MouseEvent");
  1.2272 +        event.initMouseEvent("contextmenu", true, true, target.defaultView,
  1.2273 +                             0, x, y, x, y, false, false, false, false,
  1.2274 +                             0, null);
  1.2275 +        target.ownerDocument.defaultView.addEventListener("contextmenu", this, false);
  1.2276 +        target.dispatchEvent(event);
  1.2277 +      } else {
  1.2278 +        this.menus = null;
  1.2279 +        Services.obs.notifyObservers({target: target, x: x, y: y}, "context-menu-not-shown", "");
  1.2280 +
  1.2281 +        if (SelectionHandler.canSelect(target)) {
  1.2282 +          if (!SelectionHandler.startSelection(target, {
  1.2283 +            mode: SelectionHandler.SELECT_AT_POINT,
  1.2284 +            x: x,
  1.2285 +            y: y
  1.2286 +          })) { 
  1.2287 +            SelectionHandler.attachCaret(target);
  1.2288 +          }
  1.2289 +        }
  1.2290 +      }
  1.2291 +    },
  1.2292 +
  1.2293 +    // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
  1.2294 +    _getTitle: function(node) {
  1.2295 +      if (node.hasAttribute && node.hasAttribute("title")) {
  1.2296 +        return node.getAttribute("title");
  1.2297 +      }
  1.2298 +      return this._getUrl(node);
  1.2299 +    },
  1.2300 +
  1.2301 +    // Returns a url associated with a node
  1.2302 +    _getUrl: function(node) {
  1.2303 +      if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) ||
  1.2304 +          (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) {
  1.2305 +        return this._getLinkURL(node);
  1.2306 +      } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) {
  1.2307 +        return node.currentURI.spec;
  1.2308 +      } else if (node instanceof Ci.nsIDOMHTMLMediaElement) {
  1.2309 +        return (node.currentSrc || node.src);
  1.2310 +      }
  1.2311 +
  1.2312 +      return "";
  1.2313 +    },
  1.2314 +
  1.2315 +    // Adds an array of menuitems to the current list of items to show, in the correct context
  1.2316 +    _addMenuItems: function(items, context) {
  1.2317 +        if (!this.menus[context]) {
  1.2318 +          this.menus[context] = [];
  1.2319 +        }
  1.2320 +        this.menus[context] = this.menus[context].concat(items);
  1.2321 +    },
  1.2322 +
  1.2323 +    /* Does the basic work of building a context menu to show. Will combine HTML and Native
  1.2324 +     * context menus items, as well as sorting menuitems into different menus based on context.
  1.2325 +     */
  1.2326 +    _buildMenu: function(x, y) {
  1.2327 +      // now walk up the tree and for each node look for any context menu items that apply
  1.2328 +      let element = this._target;
  1.2329 +
  1.2330 +      // this.menus holds a hashmap of "contexts" to menuitems associated with that context
  1.2331 +      // For instance, if the user taps an image inside a link, we'll have something like:
  1.2332 +      // {
  1.2333 +      //   link:  [ ContextMenuItem, ContextMenuItem ]
  1.2334 +      //   image: [ ContextMenuItem, ContextMenuItem ]
  1.2335 +      // }
  1.2336 +      this.menus = {};
  1.2337 +
  1.2338 +      while (element) {
  1.2339 +        let context = this._getContextType(element);
  1.2340 +
  1.2341 +        // First check for any html5 context menus that might exist...
  1.2342 +        var items = this._getHTMLContextMenuItemsForElement(element);
  1.2343 +        if (items.length > 0) {
  1.2344 +          this._addMenuItems(items, context);
  1.2345 +        }
  1.2346 +
  1.2347 +        // then check for any context menu items registered in the ui.
  1.2348 +        items = this._getNativeContextMenuItems(element, x, y);
  1.2349 +        if (items.length > 0) {
  1.2350 +          this._addMenuItems(items, context);
  1.2351 +        }
  1.2352 +
  1.2353 +        // walk up the tree and find more items to show
  1.2354 +        element = element.parentNode;
  1.2355 +      }
  1.2356 +    },
  1.2357 +
  1.2358 +    // Actually shows the native context menu by passing a list of context menu items to
  1.2359 +    // show to the Java.
  1.2360 +    _show: function(aEvent) {
  1.2361 +      let popupNode = this._target;
  1.2362 +      this._target = null;
  1.2363 +      if (aEvent.defaultPrevented || !popupNode) {
  1.2364 +        return;
  1.2365 +      }
  1.2366 +      this._innerShow(popupNode, aEvent.clientX, aEvent.clientY);
  1.2367 +    },
  1.2368 +
  1.2369 +    // Walks the DOM tree to find a title from a node
  1.2370 +    _findTitle: function(node) {
  1.2371 +      let title = "";
  1.2372 +      while(node && !title) {
  1.2373 +        title = this._getTitle(node);
  1.2374 +        node = node.parentNode;
  1.2375 +      }
  1.2376 +      return title;
  1.2377 +    },
  1.2378 +
  1.2379 +    /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm
  1.2380 +     * If there is one menu, will return a flat array of menuitems. If there are multiple
  1.2381 +     * menus, will return an array with appropriate tabs/items inside it. i.e. :
  1.2382 +     * [
  1.2383 +     *    { label: "link", items: [...] },
  1.2384 +     *    { label: "image", items: [...] }
  1.2385 +     * ]
  1.2386 +     */
  1.2387 +    _reformatList: function(target) {
  1.2388 +      let contexts = Object.keys(this.menus);
  1.2389 +
  1.2390 +      if (contexts.length === 1) {
  1.2391 +        // If there's only one context, we'll only show a single flat single select list
  1.2392 +        return this._reformatMenuItems(target, this.menus[contexts[0]]);
  1.2393 +      }
  1.2394 +
  1.2395 +      // If there are multiple contexts, we'll only show a tabbed ui with multiple lists
  1.2396 +      return this._reformatListAsTabs(target, this.menus);
  1.2397 +    },
  1.2398 +
  1.2399 +    /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm's
  1.2400 +     * addTabs method. i.e. :
  1.2401 +     * { link: [...], image: [...] } becomes
  1.2402 +     * [ { label: "link", items: [...] } ]
  1.2403 +     *
  1.2404 +     * Also reformats items and resolves any parmaeters that aren't known until display time
  1.2405 +     * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
  1.2406 +     */
  1.2407 +    _reformatListAsTabs: function(target, menus) {
  1.2408 +      let itemArray = [];
  1.2409 +
  1.2410 +      // Sort the keys so that "link" is always first
  1.2411 +      let contexts = Object.keys(this.menus);
  1.2412 +      contexts.sort((context1, context2) => {
  1.2413 +        if (context1 === this.defaultContext) {
  1.2414 +          return -1;
  1.2415 +        } else if (context2 === this.defaultContext) {
  1.2416 +          return 1;
  1.2417 +        }
  1.2418 +        return 0;
  1.2419 +      });
  1.2420 +
  1.2421 +      contexts.forEach(context => {
  1.2422 +        itemArray.push({
  1.2423 +          label: context,
  1.2424 +          items: this._reformatMenuItems(target, menus[context])
  1.2425 +        });
  1.2426 +      });
  1.2427 +
  1.2428 +      return itemArray;
  1.2429 +    },
  1.2430 +
  1.2431 +    /* Reformats an array of ContextMenuItems into an array that can be handled by Prompt.jsm. Also reformats items
  1.2432 +     * and resolves any parmaeters that aren't known until display time
  1.2433 +     * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
  1.2434 +     */
  1.2435 +    _reformatMenuItems: function(target, menuitems) {
  1.2436 +      let itemArray = [];
  1.2437 +
  1.2438 +      for (let i = 0; i < menuitems.length; i++) {
  1.2439 +        let t = target;
  1.2440 +        while(t) {
  1.2441 +          if (menuitems[i].matches(t)) {
  1.2442 +            let val = menuitems[i].getValue(t);
  1.2443 +
  1.2444 +            // hidden menu items will return null from getValue
  1.2445 +            if (val) {
  1.2446 +              itemArray.push(val);
  1.2447 +              break;
  1.2448 +            }
  1.2449 +          }
  1.2450 +
  1.2451 +          t = t.parentNode;
  1.2452 +        }
  1.2453 +      }
  1.2454 +
  1.2455 +      return itemArray;
  1.2456 +    },
  1.2457 +
  1.2458 +    // Called where we're finally ready to actually show the contextmenu. Sorts the items and shows a prompt.
  1.2459 +    _innerShow: function(target, x, y) {
  1.2460 +      Haptic.performSimpleAction(Haptic.LongPress);
  1.2461 +
  1.2462 +      // spin through the tree looking for a title for this context menu
  1.2463 +      let title = this._findTitle(target);
  1.2464 +
  1.2465 +      for (let context in this.menus) {
  1.2466 +        let menu = this.menus[context];
  1.2467 +        menu.sort((a,b) => {
  1.2468 +          if (a.order === b.order) {
  1.2469 +            return 0;
  1.2470 +          }
  1.2471 +          return (a.order > b.order) ? 1 : -1;
  1.2472 +        });
  1.2473 +      }
  1.2474 +
  1.2475 +      let useTabs = Object.keys(this.menus).length > 1;
  1.2476 +      let prompt = new Prompt({
  1.2477 +        window: target.ownerDocument.defaultView,
  1.2478 +        title: useTabs ? undefined : title
  1.2479 +      });
  1.2480 +
  1.2481 +      let items = this._reformatList(target);
  1.2482 +      if (useTabs) {
  1.2483 +        prompt.addTabs({
  1.2484 +          id: "tabs",
  1.2485 +          items: items
  1.2486 +        });
  1.2487 +      } else {
  1.2488 +        prompt.setSingleChoiceItems(items);
  1.2489 +      }
  1.2490 +
  1.2491 +      prompt.show(this._promptDone.bind(this, target, x, y, items));
  1.2492 +    },
  1.2493 +
  1.2494 +    // Called when the contextmenu prompt is closed
  1.2495 +    _promptDone: function(target, x, y, items, data) {
  1.2496 +      if (data.button == -1) {
  1.2497 +        // Prompt was cancelled, or an ActionView was used.
  1.2498 +        return;
  1.2499 +      }
  1.2500 +
  1.2501 +      let selectedItemId;
  1.2502 +      if (data.tabs) {
  1.2503 +        let menu = items[data.tabs.tab];
  1.2504 +        selectedItemId = menu.items[data.tabs.item].id;
  1.2505 +      } else {
  1.2506 +        selectedItemId = items[data.list[0]].id
  1.2507 +      }
  1.2508 +
  1.2509 +      let selectedItem = this._findMenuItem(selectedItemId);
  1.2510 +      this.menus = null;
  1.2511 +
  1.2512 +      if (!selectedItem || !selectedItem.matches || !selectedItem.callback) {
  1.2513 +        return;
  1.2514 +      }
  1.2515 +
  1.2516 +      // for menuitems added using the native UI, pass the dom element that matched that item to the callback
  1.2517 +      while (target) {
  1.2518 +        if (selectedItem.matches(target, x, y)) {
  1.2519 +          selectedItem.callback(target, x, y);
  1.2520 +          break;
  1.2521 +        }
  1.2522 +        target = target.parentNode;
  1.2523 +      }
  1.2524 +    },
  1.2525 +
  1.2526 +    // Called when the contextmenu is done propagating to content. If the event wasn't cancelled, will show a contextmenu.
  1.2527 +    handleEvent: function(aEvent) {
  1.2528 +      BrowserEventHandler._cancelTapHighlight();
  1.2529 +      aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false);
  1.2530 +      this._show(aEvent);
  1.2531 +    },
  1.2532 +
  1.2533 +    // Called when a long press is observed in the native Java frontend. Will start the process of generating/showing a contextmenu.
  1.2534 +    observe: function(aSubject, aTopic, aData) {
  1.2535 +      let data = JSON.parse(aData);
  1.2536 +      // content gets first crack at cancelling context menus
  1.2537 +      this._sendToContent(data.x, data.y);
  1.2538 +    },
  1.2539 +
  1.2540 +    // XXX - These are stolen from Util.js, we should remove them if we bring it back
  1.2541 +    makeURLAbsolute: function makeURLAbsolute(base, url) {
  1.2542 +      // Note:  makeURI() will throw if url is not a valid URI
  1.2543 +      return this.makeURI(url, null, this.makeURI(base)).spec;
  1.2544 +    },
  1.2545 +
  1.2546 +    makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
  1.2547 +      return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
  1.2548 +    },
  1.2549 +
  1.2550 +    _getLink: function(aElement) {
  1.2551 +      if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
  1.2552 +          ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
  1.2553 +          (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) ||
  1.2554 +          aElement instanceof Ci.nsIDOMHTMLLinkElement ||
  1.2555 +          aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) {
  1.2556 +        try {
  1.2557 +          let url = this._getLinkURL(aElement);
  1.2558 +          return Services.io.newURI(url, null, null);
  1.2559 +        } catch (e) {}
  1.2560 +      }
  1.2561 +      return null;
  1.2562 +    },
  1.2563 +
  1.2564 +    _disableInGuest: function _disableInGuest(selector) {
  1.2565 +      return {
  1.2566 +        matches: function _disableInGuestMatches(aElement, aX, aY) {
  1.2567 +          if (BrowserApp.isGuest)
  1.2568 +            return false;
  1.2569 +          return selector.matches(aElement, aX, aY);
  1.2570 +        }
  1.2571 +      };
  1.2572 +    },
  1.2573 +
  1.2574 +    _getLinkURL: function ch_getLinkURL(aLink) {
  1.2575 +      let href = aLink.href;
  1.2576 +      if (href)
  1.2577 +        return href;
  1.2578 +
  1.2579 +      href = aLink.getAttributeNS(kXLinkNamespace, "href");
  1.2580 +      if (!href || !href.match(/\S/)) {
  1.2581 +        // Without this we try to save as the current doc,
  1.2582 +        // for example, HTML case also throws if empty
  1.2583 +        throw "Empty href";
  1.2584 +      }
  1.2585 +
  1.2586 +      return this.makeURLAbsolute(aLink.baseURI, href);
  1.2587 +    },
  1.2588 +
  1.2589 +    _copyStringToDefaultClipboard: function(aString) {
  1.2590 +      let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
  1.2591 +      clipboard.copyString(aString);
  1.2592 +    },
  1.2593 +
  1.2594 +    _shareStringWithDefault: function(aSharedString, aTitle) {
  1.2595 +      let sharing = Cc["@mozilla.org/uriloader/external-sharing-app-service;1"].getService(Ci.nsIExternalSharingAppService);
  1.2596 +      sharing.shareWithDefault(aSharedString, "text/plain", aTitle);
  1.2597 +    },
  1.2598 +
  1.2599 +    _stripScheme: function(aString) {
  1.2600 +      let index = aString.indexOf(":");
  1.2601 +      return aString.slice(index + 1);
  1.2602 +    }
  1.2603 +  }
  1.2604 +};
  1.2605 +
  1.2606 +var LightWeightThemeWebInstaller = {
  1.2607 +  init: function sh_init() {
  1.2608 +    let temp = {};
  1.2609 +    Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp);
  1.2610 +    let theme = new temp.LightweightThemeConsumer(document);
  1.2611 +    BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
  1.2612 +    BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
  1.2613 +    BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
  1.2614 +  },
  1.2615 +
  1.2616 +  uninit: function() {
  1.2617 +    BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
  1.2618 +    BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
  1.2619 +    BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
  1.2620 +  },
  1.2621 +
  1.2622 +  handleEvent: function (event) {
  1.2623 +    switch (event.type) {
  1.2624 +      case "InstallBrowserTheme":
  1.2625 +      case "PreviewBrowserTheme":
  1.2626 +      case "ResetBrowserThemePreview":
  1.2627 +        // ignore requests from background tabs
  1.2628 +        if (event.target.ownerDocument.defaultView.top != content)
  1.2629 +          return;
  1.2630 +    }
  1.2631 +
  1.2632 +    switch (event.type) {
  1.2633 +      case "InstallBrowserTheme":
  1.2634 +        this._installRequest(event);
  1.2635 +        break;
  1.2636 +      case "PreviewBrowserTheme":
  1.2637 +        this._preview(event);
  1.2638 +        break;
  1.2639 +      case "ResetBrowserThemePreview":
  1.2640 +        this._resetPreview(event);
  1.2641 +        break;
  1.2642 +      case "pagehide":
  1.2643 +      case "TabSelect":
  1.2644 +        this._resetPreview();
  1.2645 +        break;
  1.2646 +    }
  1.2647 +  },
  1.2648 +
  1.2649 +  get _manager () {
  1.2650 +    let temp = {};
  1.2651 +    Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
  1.2652 +    delete this._manager;
  1.2653 +    return this._manager = temp.LightweightThemeManager;
  1.2654 +  },
  1.2655 +
  1.2656 +  _installRequest: function (event) {
  1.2657 +    let node = event.target;
  1.2658 +    let data = this._getThemeFromNode(node);
  1.2659 +    if (!data)
  1.2660 +      return;
  1.2661 +
  1.2662 +    if (this._isAllowed(node)) {
  1.2663 +      this._install(data);
  1.2664 +      return;
  1.2665 +    }
  1.2666 +
  1.2667 +    let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton");
  1.2668 +    let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1);
  1.2669 +    let buttons = [{
  1.2670 +      label: allowButtonText,
  1.2671 +      callback: function () {
  1.2672 +        LightWeightThemeWebInstaller._install(data);
  1.2673 +      }
  1.2674 +    }];
  1.2675 +
  1.2676 +    NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id);
  1.2677 +  },
  1.2678 +
  1.2679 +  _install: function (newLWTheme) {
  1.2680 +    this._manager.currentTheme = newLWTheme;
  1.2681 +  },
  1.2682 +
  1.2683 +  _previewWindow: null,
  1.2684 +  _preview: function (event) {
  1.2685 +    if (!this._isAllowed(event.target))
  1.2686 +      return;
  1.2687 +    let data = this._getThemeFromNode(event.target);
  1.2688 +    if (!data)
  1.2689 +      return;
  1.2690 +    this._resetPreview();
  1.2691 +
  1.2692 +    this._previewWindow = event.target.ownerDocument.defaultView;
  1.2693 +    this._previewWindow.addEventListener("pagehide", this, true);
  1.2694 +    BrowserApp.deck.addEventListener("TabSelect", this, false);
  1.2695 +    this._manager.previewTheme(data);
  1.2696 +  },
  1.2697 +
  1.2698 +  _resetPreview: function (event) {
  1.2699 +    if (!this._previewWindow ||
  1.2700 +        event && !this._isAllowed(event.target))
  1.2701 +      return;
  1.2702 +
  1.2703 +    this._previewWindow.removeEventListener("pagehide", this, true);
  1.2704 +    this._previewWindow = null;
  1.2705 +    BrowserApp.deck.removeEventListener("TabSelect", this, false);
  1.2706 +
  1.2707 +    this._manager.resetPreview();
  1.2708 +  },
  1.2709 +
  1.2710 +  _isAllowed: function (node) {
  1.2711 +    let pm = Services.perms;
  1.2712 +
  1.2713 +    let uri = node.ownerDocument.documentURIObject;
  1.2714 +    return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
  1.2715 +  },
  1.2716 +
  1.2717 +  _getThemeFromNode: function (node) {
  1.2718 +    return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI);
  1.2719 +  }
  1.2720 +};
  1.2721 +
  1.2722 +var DesktopUserAgent = {
  1.2723 +  DESKTOP_UA: null,
  1.2724 +
  1.2725 +  init: function ua_init() {
  1.2726 +    Services.obs.addObserver(this, "DesktopMode:Change", false);
  1.2727 +    UserAgentOverrides.addComplexOverride(this.onRequest.bind(this));
  1.2728 +
  1.2729 +    // See https://developer.mozilla.org/en/Gecko_user_agent_string_reference
  1.2730 +    this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
  1.2731 +                        .getService(Ci.nsIHttpProtocolHandler).userAgent
  1.2732 +                        .replace(/Android; [a-zA-Z]+/, "X11; Linux x86_64")
  1.2733 +                        .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101");
  1.2734 +  },
  1.2735 +
  1.2736 +  uninit: function ua_uninit() {
  1.2737 +    Services.obs.removeObserver(this, "DesktopMode:Change");
  1.2738 +  },
  1.2739 +
  1.2740 +  onRequest: function(channel, defaultUA) {
  1.2741 +    let channelWindow = this._getWindowForRequest(channel);
  1.2742 +    let tab = BrowserApp.getTabForWindow(channelWindow);
  1.2743 +    if (tab == null)
  1.2744 +      return null;
  1.2745 +
  1.2746 +    return this.getUserAgentForTab(tab);
  1.2747 +  },
  1.2748 +
  1.2749 +  getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) {
  1.2750 +    let tab = BrowserApp.getTabForWindow(aWindow.top);
  1.2751 +    if (tab)
  1.2752 +      return this.getUserAgentForTab(tab);
  1.2753 +
  1.2754 +    return null;
  1.2755 +  },
  1.2756 +
  1.2757 +  getUserAgentForTab: function ua_getUserAgentForTab(aTab) {
  1.2758 +    // Send desktop UA if "Request Desktop Site" is enabled.
  1.2759 +    if (aTab.desktopMode)
  1.2760 +      return this.DESKTOP_UA;
  1.2761 +
  1.2762 +    return null;
  1.2763 +  },
  1.2764 +
  1.2765 +  _getRequestLoadContext: function ua_getRequestLoadContext(aRequest) {
  1.2766 +    if (aRequest && aRequest.notificationCallbacks) {
  1.2767 +      try {
  1.2768 +        return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
  1.2769 +      } catch (ex) { }
  1.2770 +    }
  1.2771 +
  1.2772 +    if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) {
  1.2773 +      try {
  1.2774 +        return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
  1.2775 +      } catch (ex) { }
  1.2776 +    }
  1.2777 +
  1.2778 +    return null;
  1.2779 +  },
  1.2780 +
  1.2781 +  _getWindowForRequest: function ua_getWindowForRequest(aRequest) {
  1.2782 +    let loadContext = this._getRequestLoadContext(aRequest);
  1.2783 +    if (loadContext) {
  1.2784 +      try {
  1.2785 +        return loadContext.associatedWindow;
  1.2786 +      } catch (e) {
  1.2787 +        // loadContext.associatedWindow can throw when there's no window
  1.2788 +      }
  1.2789 +    }
  1.2790 +    return null;
  1.2791 +  },
  1.2792 +
  1.2793 +  observe: function ua_observe(aSubject, aTopic, aData) {
  1.2794 +    if (aTopic === "DesktopMode:Change") {
  1.2795 +      let args = JSON.parse(aData);
  1.2796 +      let tab = BrowserApp.getTabForId(args.tabId);
  1.2797 +      if (tab != null)
  1.2798 +        tab.reloadWithMode(args.desktopMode);
  1.2799 +    }
  1.2800 +  }
  1.2801 +};
  1.2802 +
  1.2803 +
  1.2804 +function nsBrowserAccess() {
  1.2805 +}
  1.2806 +
  1.2807 +nsBrowserAccess.prototype = {
  1.2808 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]),
  1.2809 +
  1.2810 +  _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aContext) {
  1.2811 +    let isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
  1.2812 +    if (isExternal && aURI && aURI.schemeIs("chrome"))
  1.2813 +      return null;
  1.2814 +
  1.2815 +    let loadflags = isExternal ?
  1.2816 +                      Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
  1.2817 +                      Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
  1.2818 +    if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
  1.2819 +      switch (aContext) {
  1.2820 +        case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL:
  1.2821 +          aWhere = Services.prefs.getIntPref("browser.link.open_external");
  1.2822 +          break;
  1.2823 +        default: // OPEN_NEW or an illegal value
  1.2824 +          aWhere = Services.prefs.getIntPref("browser.link.open_newwindow");
  1.2825 +      }
  1.2826 +    }
  1.2827 +
  1.2828 +    Services.io.offline = false;
  1.2829 +
  1.2830 +    let referrer;
  1.2831 +    if (aOpener) {
  1.2832 +      try {
  1.2833 +        let location = aOpener.location;
  1.2834 +        referrer = Services.io.newURI(location, null, null);
  1.2835 +      } catch(e) { }
  1.2836 +    }
  1.2837 +
  1.2838 +    let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
  1.2839 +    let pinned = false;
  1.2840 +
  1.2841 +    if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
  1.2842 +      pinned = true;
  1.2843 +      let spec = aURI.spec;
  1.2844 +      let tabs = BrowserApp.tabs;
  1.2845 +      for (let i = 0; i < tabs.length; i++) {
  1.2846 +        let appOrigin = ss.getTabValue(tabs[i], "appOrigin");
  1.2847 +        if (appOrigin == spec) {
  1.2848 +          let tab = tabs[i];
  1.2849 +          BrowserApp.selectTab(tab);
  1.2850 +          return tab.browser;
  1.2851 +        }
  1.2852 +      }
  1.2853 +    }
  1.2854 +
  1.2855 +    let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
  1.2856 +                  aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
  1.2857 +                  aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB);
  1.2858 +    let isPrivate = false;
  1.2859 +
  1.2860 +    if (newTab) {
  1.2861 +      let parentId = -1;
  1.2862 +      if (!isExternal && aOpener) {
  1.2863 +        let parent = BrowserApp.getTabForWindow(aOpener.top);
  1.2864 +        if (parent) {
  1.2865 +          parentId = parent.id;
  1.2866 +          isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow);
  1.2867 +        }
  1.2868 +      }
  1.2869 +
  1.2870 +      // BrowserApp.addTab calls loadURIWithFlags with the appropriate params
  1.2871 +      let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags,
  1.2872 +                                                                      referrerURI: referrer,
  1.2873 +                                                                      external: isExternal,
  1.2874 +                                                                      parentId: parentId,
  1.2875 +                                                                      selected: true,
  1.2876 +                                                                      isPrivate: isPrivate,
  1.2877 +                                                                      pinned: pinned });
  1.2878 +
  1.2879 +      return tab.browser;
  1.2880 +    }
  1.2881 +
  1.2882 +    // OPEN_CURRENTWINDOW and illegal values
  1.2883 +    let browser = BrowserApp.selectedBrowser;
  1.2884 +    if (aURI && browser)
  1.2885 +      browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null);
  1.2886 +
  1.2887 +    return browser;
  1.2888 +  },
  1.2889 +
  1.2890 +  openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) {
  1.2891 +    let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
  1.2892 +    return browser ? browser.contentWindow : null;
  1.2893 +  },
  1.2894 +
  1.2895 +  openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) {
  1.2896 +    let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
  1.2897 +    return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
  1.2898 +  },
  1.2899 +
  1.2900 +  isTabContentWindow: function(aWindow) {
  1.2901 +    return BrowserApp.getBrowserForWindow(aWindow) != null;
  1.2902 +  },
  1.2903 +
  1.2904 +  get contentWindow() {
  1.2905 +    return BrowserApp.selectedBrowser.contentWindow;
  1.2906 +  }
  1.2907 +};
  1.2908 +
  1.2909 +
  1.2910 +// track the last known screen size so that new tabs
  1.2911 +// get created with the right size rather than being 1x1
  1.2912 +let gScreenWidth = 1;
  1.2913 +let gScreenHeight = 1;
  1.2914 +let gReflowPending = null;
  1.2915 +
  1.2916 +// The margins that should be applied to the viewport for fixed position
  1.2917 +// children. This is used to avoid browser chrome permanently obscuring
  1.2918 +// fixed position content, and also to make sure window-sized pages take
  1.2919 +// into account said browser chrome.
  1.2920 +let gViewportMargins = { top: 0, right: 0, bottom: 0, left: 0};
  1.2921 +
  1.2922 +function Tab(aURL, aParams) {
  1.2923 +  this.browser = null;
  1.2924 +  this.id = 0;
  1.2925 +  this.lastTouchedAt = Date.now();
  1.2926 +  this._zoom = 1.0;
  1.2927 +  this._drawZoom = 1.0;
  1.2928 +  this._restoreZoom = false;
  1.2929 +  this._fixedMarginLeft = 0;
  1.2930 +  this._fixedMarginTop = 0;
  1.2931 +  this._fixedMarginRight = 0;
  1.2932 +  this._fixedMarginBottom = 0;
  1.2933 +  this._readerEnabled = false;
  1.2934 +  this._readerActive = false;
  1.2935 +  this.userScrollPos = { x: 0, y: 0 };
  1.2936 +  this.viewportExcludesHorizontalMargins = true;
  1.2937 +  this.viewportExcludesVerticalMargins = true;
  1.2938 +  this.viewportMeasureCallback = null;
  1.2939 +  this.lastPageSizeAfterViewportRemeasure = { width: 0, height: 0 };
  1.2940 +  this.contentDocumentIsDisplayed = true;
  1.2941 +  this.pluginDoorhangerTimeout = null;
  1.2942 +  this.shouldShowPluginDoorhanger = true;
  1.2943 +  this.clickToPlayPluginsActivated = false;
  1.2944 +  this.desktopMode = false;
  1.2945 +  this.originalURI = null;
  1.2946 +  this.savedArticle = null;
  1.2947 +  this.hasTouchListener = false;
  1.2948 +  this.browserWidth = 0;
  1.2949 +  this.browserHeight = 0;
  1.2950 +
  1.2951 +  this.create(aURL, aParams);
  1.2952 +}
  1.2953 +
  1.2954 +Tab.prototype = {
  1.2955 +  create: function(aURL, aParams) {
  1.2956 +    if (this.browser)
  1.2957 +      return;
  1.2958 +
  1.2959 +    aParams = aParams || {};
  1.2960 +
  1.2961 +    this.browser = document.createElement("browser");
  1.2962 +    this.browser.setAttribute("type", "content-targetable");
  1.2963 +    this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight);
  1.2964 +
  1.2965 +    // Make sure the previously selected panel remains selected. The selected panel of a deck is
  1.2966 +    // not stable when panels are added.
  1.2967 +    let selectedPanel = BrowserApp.deck.selectedPanel;
  1.2968 +    BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null);
  1.2969 +    BrowserApp.deck.selectedPanel = selectedPanel;
  1.2970 +
  1.2971 +    if (BrowserApp.manifestUrl) {
  1.2972 +      let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
  1.2973 +      let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl);
  1.2974 +      if (manifest) {
  1.2975 +        let app = manifest.QueryInterface(Ci.mozIApplication);
  1.2976 +        this.browser.docShell.setIsApp(app.localId);
  1.2977 +      }
  1.2978 +    }
  1.2979 +
  1.2980 +    // Must be called after appendChild so the docshell has been created.
  1.2981 +    this.setActive(false);
  1.2982 +
  1.2983 +    let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate;
  1.2984 +    if (isPrivate) {
  1.2985 +      this.browser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing = true;
  1.2986 +    }
  1.2987 +
  1.2988 +    this.browser.stop();
  1.2989 +
  1.2990 +    let frameLoader = this.browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
  1.2991 +    frameLoader.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL;
  1.2992 +
  1.2993 +    // only set tab uri if uri is valid
  1.2994 +    let uri = null;
  1.2995 +    let title = aParams.title || aURL;
  1.2996 +    try {
  1.2997 +      uri = Services.io.newURI(aURL, null, null).spec;
  1.2998 +    } catch (e) {}
  1.2999 +
  1.3000 +    // When the tab is stubbed from Java, there's a window between the stub
  1.3001 +    // creation and the tab creation in Gecko where the stub could be removed
  1.3002 +    // or the selected tab can change (which is easiest to hit during startup).
  1.3003 +    // To prevent these races, we need to differentiate between tab stubs from
  1.3004 +    // Java and new tabs from Gecko.
  1.3005 +    let stub = false;
  1.3006 +
  1.3007 +    if (!aParams.zombifying) {
  1.3008 +      if ("tabID" in aParams) {
  1.3009 +        this.id = aParams.tabID;
  1.3010 +        stub = true;
  1.3011 +      } else {
  1.3012 +        let jni = new JNI();
  1.3013 +        let cls = jni.findClass("org/mozilla/gecko/Tabs");
  1.3014 +        let method = jni.getStaticMethodID(cls, "getNextTabId", "()I");
  1.3015 +        this.id = jni.callStaticIntMethod(cls, method);
  1.3016 +        jni.close();
  1.3017 +      }
  1.3018 +
  1.3019 +      this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false;
  1.3020 +
  1.3021 +      let message = {
  1.3022 +        type: "Tab:Added",
  1.3023 +        tabID: this.id,
  1.3024 +        uri: uri,
  1.3025 +        parentId: ("parentId" in aParams) ? aParams.parentId : -1,
  1.3026 +        external: ("external" in aParams) ? aParams.external : false,
  1.3027 +        selected: ("selected" in aParams) ? aParams.selected : true,
  1.3028 +        title: title,
  1.3029 +        delayLoad: aParams.delayLoad || false,
  1.3030 +        desktopMode: this.desktopMode,
  1.3031 +        isPrivate: isPrivate,
  1.3032 +        stub: stub
  1.3033 +      };
  1.3034 +      sendMessageToJava(message);
  1.3035 +
  1.3036 +      this.overscrollController = new OverscrollController(this);
  1.3037 +    }
  1.3038 +
  1.3039 +    this.browser.contentWindow.controllers.insertControllerAt(0, this.overscrollController);
  1.3040 +
  1.3041 +    let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
  1.3042 +                Ci.nsIWebProgress.NOTIFY_LOCATION |
  1.3043 +                Ci.nsIWebProgress.NOTIFY_SECURITY;
  1.3044 +    this.browser.addProgressListener(this, flags);
  1.3045 +    this.browser.sessionHistory.addSHistoryListener(this);
  1.3046 +
  1.3047 +    this.browser.addEventListener("DOMContentLoaded", this, true);
  1.3048 +    this.browser.addEventListener("DOMFormHasPassword", this, true);
  1.3049 +    this.browser.addEventListener("DOMLinkAdded", this, true);
  1.3050 +    this.browser.addEventListener("DOMTitleChanged", this, true);
  1.3051 +    this.browser.addEventListener("DOMWindowClose", this, true);
  1.3052 +    this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
  1.3053 +    this.browser.addEventListener("DOMAutoComplete", this, true);
  1.3054 +    this.browser.addEventListener("blur", this, true);
  1.3055 +    this.browser.addEventListener("scroll", this, true);
  1.3056 +    this.browser.addEventListener("MozScrolledAreaChanged", this, true);
  1.3057 +    this.browser.addEventListener("pageshow", this, true);
  1.3058 +    this.browser.addEventListener("MozApplicationManifest", this, true);
  1.3059 +
  1.3060 +    // Note that the XBL binding is untrusted
  1.3061 +    this.browser.addEventListener("PluginBindingAttached", this, true, true);
  1.3062 +    this.browser.addEventListener("VideoBindingAttached", this, true, true);
  1.3063 +    this.browser.addEventListener("VideoBindingCast", this, true, true);
  1.3064 +
  1.3065 +    Services.obs.addObserver(this, "before-first-paint", false);
  1.3066 +    Services.obs.addObserver(this, "after-viewport-change", false);
  1.3067 +    Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false);
  1.3068 +
  1.3069 +    if (aParams.delayLoad) {
  1.3070 +      // If this is a zombie tab, attach restore data so the tab will be
  1.3071 +      // restored when selected
  1.3072 +      this.browser.__SS_data = {
  1.3073 +        entries: [{
  1.3074 +          url: aURL,
  1.3075 +          title: title
  1.3076 +        }],
  1.3077 +        index: 1
  1.3078 +      };
  1.3079 +      this.browser.__SS_restore = true;
  1.3080 +    } else {
  1.3081 +      let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
  1.3082 +      let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
  1.3083 +      let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
  1.3084 +      let charset = "charset" in aParams ? aParams.charset : null;
  1.3085 +
  1.3086 +      // The search term the user entered to load the current URL
  1.3087 +      this.userSearch = "userSearch" in aParams ? aParams.userSearch : "";
  1.3088 +
  1.3089 +      try {
  1.3090 +        this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData);
  1.3091 +      } catch(e) {
  1.3092 +        let message = {
  1.3093 +          type: "Content:LoadError",
  1.3094 +          tabID: this.id
  1.3095 +        };
  1.3096 +        sendMessageToJava(message);
  1.3097 +        dump("Handled load error: " + e);
  1.3098 +      }
  1.3099 +    }
  1.3100 +  },
  1.3101 +
  1.3102 +  /**
  1.3103 +   * Retrieves the font size in twips for a given element.
  1.3104 +   */
  1.3105 +  getInflatedFontSizeFor: function(aElement) {
  1.3106 +    // GetComputedStyle should always give us CSS pixels for a font size.
  1.3107 +    let fontSizeStr = this.window.getComputedStyle(aElement)['fontSize'];
  1.3108 +    let fontSize = fontSizeStr.slice(0, -2);
  1.3109 +    return aElement.fontSizeInflation * fontSize;
  1.3110 +  },
  1.3111 +
  1.3112 +  /**
  1.3113 +   * This returns the zoom necessary to match the font size of an element to
  1.3114 +   * the minimum font size specified by the browser.zoom.reflowOnZoom.minFontSizeTwips
  1.3115 +   * preference.
  1.3116 +   */
  1.3117 +  getZoomToMinFontSize: function(aElement) {
  1.3118 +    // We only use the font.size.inflation.minTwips preference because this is
  1.3119 +    // the only one that is controlled by the user-interface in the 'Settings'
  1.3120 +    // menu. Thus, if font.size.inflation.emPerLine is changed, this does not
  1.3121 +    // effect reflow-on-zoom.
  1.3122 +    let minFontSize = convertFromTwipsToPx(Services.prefs.getIntPref("font.size.inflation.minTwips"));
  1.3123 +    return minFontSize / this.getInflatedFontSizeFor(aElement);
  1.3124 +  },
  1.3125 +
  1.3126 +  clearReflowOnZoomPendingActions: function() {
  1.3127 +    // Reflow was completed, so now re-enable painting.
  1.3128 +    let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
  1.3129 +    let docShell = webNav.QueryInterface(Ci.nsIDocShell);
  1.3130 +    let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
  1.3131 +    docViewer.resumePainting();
  1.3132 +
  1.3133 +    BrowserApp.selectedTab._mReflozPositioned = false;
  1.3134 +  },
  1.3135 +
  1.3136 +  /**
  1.3137 +   * Reflow on zoom consists of a few different sub-operations:
  1.3138 +   *
  1.3139 +   * 1. When a double-tap event is seen, we verify that the correct preferences
  1.3140 +   *    are enabled and perform the pre-position handling calculation. We also
  1.3141 +   *    signal that reflow-on-zoom should be performed at this time, and pause
  1.3142 +   *    painting.
  1.3143 +   * 2. During the next call to setViewport(), which is in the Tab prototype,
  1.3144 +   *    we detect that a call to changeMaxLineBoxWidth should be performed. If
  1.3145 +   *    we're zooming out, then the max line box width should be reset at this
  1.3146 +   *    time. Otherwise, we call performReflowOnZoom.
  1.3147 +   *   2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to
  1.3148 +   *       doChangeMaxLineBoxWidth, based on a timeout specified in preferences.
  1.3149 +   * 3. doChangeMaxLineBoxWidth changes the line box width (which also
  1.3150 +   *    schedules a reflow event), and then calls ZoomHelper.zoomInAndSnapToRange.
  1.3151 +   * 4. ZoomHelper.zoomInAndSnapToRange performs the positioning of reflow-on-zoom
  1.3152 +   *    and then re-enables painting.
  1.3153 +   *
  1.3154 +   * Some of the events happen synchronously, while others happen asynchronously.
  1.3155 +   * The following is a rough sketch of the progression of events:
  1.3156 +   *
  1.3157 +   * double tap event seen -> onDoubleTap() -> ... asynchronous ...
  1.3158 +   *   -> setViewport() -> performReflowOnZoom() -> ... asynchronous ...
  1.3159 +   *   -> doChangeMaxLineBoxWidth() -> ZoomHelper.zoomInAndSnapToRange()
  1.3160 +   *   -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change')
  1.3161 +   *   -> resumePainting()
  1.3162 +   */
  1.3163 +  performReflowOnZoom: function(aViewport) {
  1.3164 +    let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom;
  1.3165 +
  1.3166 +    let viewportWidth = gScreenWidth / zoom;
  1.3167 +    let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
  1.3168 +
  1.3169 +    if (gReflowPending) {
  1.3170 +      clearTimeout(gReflowPending);
  1.3171 +    }
  1.3172 +
  1.3173 +    // We add in a bit of fudge just so that the end characters
  1.3174 +    // don't accidentally get clipped. 15px is an arbitrary choice.
  1.3175 +    gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
  1.3176 +                                reflozTimeout,
  1.3177 +                                viewportWidth - 15);
  1.3178 +  },
  1.3179 +
  1.3180 +  /** 
  1.3181 +   * Reloads the tab with the desktop mode setting.
  1.3182 +   */
  1.3183 +  reloadWithMode: function (aDesktopMode) {
  1.3184 +    // Set desktop mode for tab and send change to Java
  1.3185 +    if (this.desktopMode != aDesktopMode) {
  1.3186 +      this.desktopMode = aDesktopMode;
  1.3187 +      sendMessageToJava({
  1.3188 +        type: "DesktopMode:Changed",
  1.3189 +        desktopMode: aDesktopMode,
  1.3190 +        tabID: this.id
  1.3191 +      });
  1.3192 +    }
  1.3193 +
  1.3194 +    // Only reload the page for http/https schemes
  1.3195 +    let currentURI = this.browser.currentURI;
  1.3196 +    if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https"))
  1.3197 +      return;
  1.3198 +
  1.3199 +    let url = currentURI.spec;
  1.3200 +    let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
  1.3201 +                Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
  1.3202 +    if (this.originalURI && !this.originalURI.equals(currentURI)) {
  1.3203 +      // We were redirected; reload the original URL
  1.3204 +      url = this.originalURI.spec;
  1.3205 +    }
  1.3206 +
  1.3207 +    this.browser.docShell.loadURI(url, flags, null, null, null);
  1.3208 +  },
  1.3209 +
  1.3210 +  destroy: function() {
  1.3211 +    if (!this.browser)
  1.3212 +      return;
  1.3213 +
  1.3214 +    this.browser.contentWindow.controllers.removeController(this.overscrollController);
  1.3215 +
  1.3216 +    this.browser.removeProgressListener(this);
  1.3217 +    this.browser.sessionHistory.removeSHistoryListener(this);
  1.3218 +
  1.3219 +    this.browser.removeEventListener("DOMContentLoaded", this, true);
  1.3220 +    this.browser.removeEventListener("DOMFormHasPassword", this, true);
  1.3221 +    this.browser.removeEventListener("DOMLinkAdded", this, true);
  1.3222 +    this.browser.removeEventListener("DOMTitleChanged", this, true);
  1.3223 +    this.browser.removeEventListener("DOMWindowClose", this, true);
  1.3224 +    this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
  1.3225 +    this.browser.removeEventListener("DOMAutoComplete", this, true);
  1.3226 +    this.browser.removeEventListener("blur", this, true);
  1.3227 +    this.browser.removeEventListener("scroll", this, true);
  1.3228 +    this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
  1.3229 +    this.browser.removeEventListener("pageshow", this, true);
  1.3230 +    this.browser.removeEventListener("MozApplicationManifest", this, true);
  1.3231 +
  1.3232 +    this.browser.removeEventListener("PluginBindingAttached", this, true, true);
  1.3233 +    this.browser.removeEventListener("VideoBindingAttached", this, true, true);
  1.3234 +    this.browser.removeEventListener("VideoBindingCast", this, true, true);
  1.3235 +
  1.3236 +    Services.obs.removeObserver(this, "before-first-paint");
  1.3237 +    Services.obs.removeObserver(this, "after-viewport-change");
  1.3238 +    Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this);
  1.3239 +
  1.3240 +    // Make sure the previously selected panel remains selected. The selected panel of a deck is
  1.3241 +    // not stable when panels are removed.
  1.3242 +    let selectedPanel = BrowserApp.deck.selectedPanel;
  1.3243 +    BrowserApp.deck.removeChild(this.browser);
  1.3244 +    BrowserApp.deck.selectedPanel = selectedPanel;
  1.3245 +
  1.3246 +    this.browser = null;
  1.3247 +    this.savedArticle = null;
  1.3248 +  },
  1.3249 +
  1.3250 +  // This should be called to update the browser when the tab gets selected/unselected
  1.3251 +  setActive: function setActive(aActive) {
  1.3252 +    if (!this.browser || !this.browser.docShell)
  1.3253 +      return;
  1.3254 +
  1.3255 +    this.lastTouchedAt = Date.now();
  1.3256 +
  1.3257 +    if (aActive) {
  1.3258 +      this.browser.setAttribute("type", "content-primary");
  1.3259 +      this.browser.focus();
  1.3260 +      this.browser.docShellIsActive = true;
  1.3261 +      Reader.updatePageAction(this);
  1.3262 +      ExternalApps.updatePageAction(this.browser.currentURI);
  1.3263 +    } else {
  1.3264 +      this.browser.setAttribute("type", "content-targetable");
  1.3265 +      this.browser.docShellIsActive = false;
  1.3266 +    }
  1.3267 +  },
  1.3268 +
  1.3269 +  getActive: function getActive() {
  1.3270 +    return this.browser.docShellIsActive;
  1.3271 +  },
  1.3272 +
  1.3273 +  setDisplayPort: function(aDisplayPort) {
  1.3274 +    let zoom = this._zoom;
  1.3275 +    let resolution = aDisplayPort.resolution;
  1.3276 +    if (zoom <= 0 || resolution <= 0)
  1.3277 +      return;
  1.3278 +
  1.3279 +    // "zoom" is the user-visible zoom of the "this" tab
  1.3280 +    // "resolution" is the zoom at which we wish gecko to render "this" tab at
  1.3281 +    // these two may be different if we are, for example, trying to render a
  1.3282 +    // large area of the page at low resolution because the user is panning real
  1.3283 +    // fast.
  1.3284 +    // The gecko scroll position is in CSS pixels. The display port rect
  1.3285 +    // values (aDisplayPort), however, are in CSS pixels multiplied by the desired
  1.3286 +    // rendering resolution. Therefore care must be taken when doing math with
  1.3287 +    // these sets of values, to ensure that they are normalized to the same coordinate
  1.3288 +    // space first.
  1.3289 +
  1.3290 +    let element = this.browser.contentDocument.documentElement;
  1.3291 +    if (!element)
  1.3292 +      return;
  1.3293 +
  1.3294 +    // we should never be drawing background tabs at resolutions other than the user-
  1.3295 +    // visible zoom. for foreground tabs, however, if we are drawing at some other
  1.3296 +    // resolution, we need to set the resolution as specified.
  1.3297 +    let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.3298 +    if (BrowserApp.selectedTab == this) {
  1.3299 +      if (resolution != this._drawZoom) {
  1.3300 +        this._drawZoom = resolution;
  1.3301 +        cwu.setResolution(resolution / window.devicePixelRatio, resolution / window.devicePixelRatio);
  1.3302 +      }
  1.3303 +    } else if (!fuzzyEquals(resolution, zoom)) {
  1.3304 +      dump("Warning: setDisplayPort resolution did not match zoom for background tab! (" + resolution + " != " + zoom + ")");
  1.3305 +    }
  1.3306 +
  1.3307 +    // Finally, we set the display port, taking care to convert everything into the CSS-pixel
  1.3308 +    // coordinate space, because that is what the function accepts. Also we have to fudge the
  1.3309 +    // displayport somewhat to make sure it gets through all the conversions gecko will do on it
  1.3310 +    // without deforming too much. See https://bugzilla.mozilla.org/show_bug.cgi?id=737510#c10
  1.3311 +    // for details on what these operations are.
  1.3312 +    let geckoScrollX = this.browser.contentWindow.scrollX;
  1.3313 +    let geckoScrollY = this.browser.contentWindow.scrollY;
  1.3314 +    aDisplayPort = this._dirtiestHackEverToWorkAroundGeckoRounding(aDisplayPort, geckoScrollX, geckoScrollY);
  1.3315 +
  1.3316 +    let displayPort = {
  1.3317 +      x: (aDisplayPort.left / resolution) - geckoScrollX,
  1.3318 +      y: (aDisplayPort.top / resolution) - geckoScrollY,
  1.3319 +      width: (aDisplayPort.right - aDisplayPort.left) / resolution,
  1.3320 +      height: (aDisplayPort.bottom - aDisplayPort.top) / resolution
  1.3321 +    };
  1.3322 +
  1.3323 +    if (this._oldDisplayPort == null ||
  1.3324 +        !fuzzyEquals(displayPort.x, this._oldDisplayPort.x) ||
  1.3325 +        !fuzzyEquals(displayPort.y, this._oldDisplayPort.y) ||
  1.3326 +        !fuzzyEquals(displayPort.width, this._oldDisplayPort.width) ||
  1.3327 +        !fuzzyEquals(displayPort.height, this._oldDisplayPort.height)) {
  1.3328 +      if (BrowserApp.gUseLowPrecision) {
  1.3329 +        // Set the display-port to be 4x the size of the critical display-port,
  1.3330 +        // on each dimension, giving us a 0.25x lower precision buffer around the
  1.3331 +        // critical display-port. Spare area is *not* redistributed to the other
  1.3332 +        // axis, as display-list building and invalidation cost scales with the
  1.3333 +        // size of the display-port.
  1.3334 +        let pageRect = cwu.getRootBounds();
  1.3335 +        let pageXMost = pageRect.right - geckoScrollX;
  1.3336 +        let pageYMost = pageRect.bottom - geckoScrollY;
  1.3337 +
  1.3338 +        let dpW = Math.min(pageRect.right - pageRect.left, displayPort.width * 4);
  1.3339 +        let dpH = Math.min(pageRect.bottom - pageRect.top, displayPort.height * 4);
  1.3340 +
  1.3341 +        let dpX = Math.min(Math.max(displayPort.x - displayPort.width * 1.5,
  1.3342 +                                    pageRect.left - geckoScrollX), pageXMost - dpW);
  1.3343 +        let dpY = Math.min(Math.max(displayPort.y - displayPort.height * 1.5,
  1.3344 +                                    pageRect.top - geckoScrollY), pageYMost - dpH);
  1.3345 +        cwu.setDisplayPortForElement(dpX, dpY, dpW, dpH, element, 0);
  1.3346 +        cwu.setCriticalDisplayPortForElement(displayPort.x, displayPort.y,
  1.3347 +                                             displayPort.width, displayPort.height,
  1.3348 +                                             element);
  1.3349 +      } else {
  1.3350 +        cwu.setDisplayPortForElement(displayPort.x, displayPort.y,
  1.3351 +                                     displayPort.width, displayPort.height,
  1.3352 +                                     element, 0);
  1.3353 +      }
  1.3354 +    }
  1.3355 +
  1.3356 +    this._oldDisplayPort = displayPort;
  1.3357 +  },
  1.3358 +
  1.3359 +  /*
  1.3360 +   * Yes, this is ugly. But it's currently the safest way to account for the rounding errors that occur
  1.3361 +   * when we pump the displayport coordinates through gecko and they pop out in the compositor.
  1.3362 +   *
  1.3363 +   * In general, the values are converted from page-relative device pixels to viewport-relative app units,
  1.3364 +   * and then back to page-relative device pixels (now as ints). The first half of this is only slightly
  1.3365 +   * lossy, but it's enough to throw off the numbers a little. Because of this, when gecko calls
  1.3366 +   * ScaleToOutsidePixels to generate the final rect, the rect may get expanded more than it should,
  1.3367 +   * ending up a pixel larger than it started off. This is undesirable in general, but specifically
  1.3368 +   * bad for tiling, because it means we means we end up painting one line of pixels from a tile,
  1.3369 +   * causing an otherwise unnecessary upload of the whole tile.
  1.3370 +   *
  1.3371 +   * In order to counteract the rounding error, this code simulates the conversions that will happen
  1.3372 +   * to the display port, and calculates whether or not that final ScaleToOutsidePixels is actually
  1.3373 +   * expanding the rect more than it should. If so, it determines how much rounding error was introduced
  1.3374 +   * up until that point, and adjusts the original values to compensate for that rounding error.
  1.3375 +   */
  1.3376 +  _dirtiestHackEverToWorkAroundGeckoRounding: function(aDisplayPort, aGeckoScrollX, aGeckoScrollY) {
  1.3377 +    const APP_UNITS_PER_CSS_PIXEL = 60.0;
  1.3378 +    const EXTRA_FUDGE = 0.04;
  1.3379 +
  1.3380 +    let resolution = aDisplayPort.resolution;
  1.3381 +
  1.3382 +    // Some helper functions that simulate conversion processes in gecko
  1.3383 +
  1.3384 +    function cssPixelsToAppUnits(aVal) {
  1.3385 +      return Math.floor((aVal * APP_UNITS_PER_CSS_PIXEL) + 0.5);
  1.3386 +    }
  1.3387 +
  1.3388 +    function appUnitsToDevicePixels(aVal) {
  1.3389 +      return aVal / APP_UNITS_PER_CSS_PIXEL * resolution;
  1.3390 +    }
  1.3391 +
  1.3392 +    function devicePixelsToAppUnits(aVal) {
  1.3393 +      return cssPixelsToAppUnits(aVal / resolution);
  1.3394 +    }
  1.3395 +
  1.3396 +    // Stash our original (desired) displayport width and height away, we need it
  1.3397 +    // later and we might modify the displayport in between.
  1.3398 +    let originalWidth = aDisplayPort.right - aDisplayPort.left;
  1.3399 +    let originalHeight = aDisplayPort.bottom - aDisplayPort.top;
  1.3400 +
  1.3401 +    // This is the first conversion the displayport goes through, going from page-relative
  1.3402 +    // device pixels to viewport-relative app units.
  1.3403 +    let appUnitDisplayPort = {
  1.3404 +      x: cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX),
  1.3405 +      y: cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY),
  1.3406 +      w: cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution),
  1.3407 +      h: cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution)
  1.3408 +    };
  1.3409 +
  1.3410 +    // This is the translation gecko applies when converting back from viewport-relative
  1.3411 +    // device pixels to page-relative device pixels.
  1.3412 +    let geckoTransformX = -Math.floor((-aGeckoScrollX * resolution) + 0.5);
  1.3413 +    let geckoTransformY = -Math.floor((-aGeckoScrollY * resolution) + 0.5);
  1.3414 +
  1.3415 +    // The final "left" value as calculated in gecko is:
  1.3416 +    //    left = geckoTransformX + Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x))
  1.3417 +    // In a perfect world, this value would be identical to aDisplayPort.left, which is what
  1.3418 +    // we started with. However, this may not be the case if the value being floored has accumulated
  1.3419 +    // enough error to drop below what it should be.
  1.3420 +    // For example, assume geckoTransformX is 0, and aDisplayPort.left is 4, but
  1.3421 +    // appUnitsToDevicePixels(appUnitsToDevicePixels.x) comes out as 3.9 because of rounding error.
  1.3422 +    // That's bad, because the -0.1 error has caused it to floor to 3 instead of 4. (If it had errored
  1.3423 +    // the other way and come out as 4.1, there's no problem). In this example, we need to increase the
  1.3424 +    // "left" value by some amount so that the 3.9 actually comes out as >= 4, and it gets floored into
  1.3425 +    // the expected value of 4. The delta values calculated below calculate that error amount (e.g. -0.1).
  1.3426 +    let errorLeft = (geckoTransformX + appUnitsToDevicePixels(appUnitDisplayPort.x)) - aDisplayPort.left;
  1.3427 +    let errorTop = (geckoTransformY + appUnitsToDevicePixels(appUnitDisplayPort.y)) - aDisplayPort.top;
  1.3428 +
  1.3429 +    // If the error was negative, that means it will floor incorrectly, so we need to bump up the
  1.3430 +    // original aDisplayPort.left and/or aDisplayPort.top values. The amount we bump it up by is
  1.3431 +    // the error amount (increased by a small fudge factor to ensure it's sufficient), converted
  1.3432 +    // backwards through the conversion process.
  1.3433 +    if (errorLeft < 0) {
  1.3434 +      aDisplayPort.left += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorLeft));
  1.3435 +      // After we modify the left value, we need to re-simulate some values to take that into account
  1.3436 +      appUnitDisplayPort.x = cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX);
  1.3437 +      appUnitDisplayPort.w = cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution);
  1.3438 +    }
  1.3439 +    if (errorTop < 0) {
  1.3440 +      aDisplayPort.top += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorTop));
  1.3441 +      // After we modify the top value, we need to re-simulate some values to take that into account
  1.3442 +      appUnitDisplayPort.y = cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY);
  1.3443 +      appUnitDisplayPort.h = cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution);
  1.3444 +    }
  1.3445 +
  1.3446 +    // At this point, the aDisplayPort.left and aDisplayPort.top values have been corrected to account
  1.3447 +    // for the error in conversion such that they end up where we want them. Now we need to also do the
  1.3448 +    // same for the right/bottom values so that the width/height end up where we want them.
  1.3449 +
  1.3450 +    // This is the final conversion that the displayport goes through before gecko spits it back to
  1.3451 +    // us. Note that the width/height calculates are of the form "ceil(transform(right)) - floor(transform(left))"
  1.3452 +    let scaledOutDevicePixels = {
  1.3453 +      x: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)),
  1.3454 +      y: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y)),
  1.3455 +      w: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)),
  1.3456 +      h: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y))
  1.3457 +    };
  1.3458 +
  1.3459 +    // The final "width" value as calculated in gecko is scaledOutDevicePixels.w.
  1.3460 +    // In a perfect world, this would equal originalWidth. However, things are not perfect, and as before,
  1.3461 +    // we need to calculate how much rounding error has been introduced. In this case the rounding error is causing
  1.3462 +    // the Math.ceil call above to ceiling to the wrong final value. For example, 4 gets converted 4.1 and gets
  1.3463 +    // ceiling'd to 5; in this case the error is 0.1.
  1.3464 +    let errorRight = (appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w) - scaledOutDevicePixels.x) - originalWidth;
  1.3465 +    let errorBottom = (appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h) - scaledOutDevicePixels.y) - originalHeight;
  1.3466 +
  1.3467 +    // If the error was positive, that means it will ceiling incorrectly, so we need to bump down the
  1.3468 +    // original aDisplayPort.right and/or aDisplayPort.bottom. Again, we back-convert the error amount
  1.3469 +    // with a small fudge factor to figure out how much to adjust the original values.
  1.3470 +    if (errorRight > 0) aDisplayPort.right -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorRight + EXTRA_FUDGE));
  1.3471 +    if (errorBottom > 0) aDisplayPort.bottom -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorBottom + EXTRA_FUDGE));
  1.3472 +
  1.3473 +    // Et voila!
  1.3474 +    return aDisplayPort;
  1.3475 +  },
  1.3476 +
  1.3477 +  setScrollClampingSize: function(zoom) {
  1.3478 +    let viewportWidth = gScreenWidth / zoom;
  1.3479 +    let viewportHeight = gScreenHeight / zoom;
  1.3480 +    let screenWidth = gScreenWidth;
  1.3481 +    let screenHeight = gScreenHeight;
  1.3482 +
  1.3483 +    // Shrink the viewport appropriately if the margins are excluded
  1.3484 +    if (this.viewportExcludesVerticalMargins) {
  1.3485 +      screenHeight = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
  1.3486 +      viewportHeight = screenHeight / zoom;
  1.3487 +    }
  1.3488 +    if (this.viewportExcludesHorizontalMargins) {
  1.3489 +      screenWidth = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
  1.3490 +      viewportWidth = screenWidth / zoom;
  1.3491 +    }
  1.3492 +
  1.3493 +    // Make sure the aspect ratio of the screen is maintained when setting
  1.3494 +    // the clamping scroll-port size.
  1.3495 +    let factor = Math.min(viewportWidth / screenWidth,
  1.3496 +                          viewportHeight / screenHeight);
  1.3497 +    let scrollPortWidth = screenWidth * factor;
  1.3498 +    let scrollPortHeight = screenHeight * factor;
  1.3499 +
  1.3500 +    let win = this.browser.contentWindow;
  1.3501 +    win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).
  1.3502 +        setScrollPositionClampingScrollPortSize(scrollPortWidth, scrollPortHeight);
  1.3503 +  },
  1.3504 +
  1.3505 +  setViewport: function(aViewport) {
  1.3506 +    // Transform coordinates based on zoom
  1.3507 +    let x = aViewport.x / aViewport.zoom;
  1.3508 +    let y = aViewport.y / aViewport.zoom;
  1.3509 +
  1.3510 +    this.setScrollClampingSize(aViewport.zoom);
  1.3511 +
  1.3512 +    // Adjust the max line box width to be no more than the viewport width, but
  1.3513 +    // only if the reflow-on-zoom preference is enabled.
  1.3514 +    let isZooming = !fuzzyEquals(aViewport.zoom, this._zoom);
  1.3515 +
  1.3516 +    let docViewer = null;
  1.3517 +
  1.3518 +    if (isZooming &&
  1.3519 +        BrowserEventHandler.mReflozPref &&
  1.3520 +        BrowserApp.selectedTab._mReflozPoint &&
  1.3521 +        BrowserApp.selectedTab.probablyNeedRefloz) {
  1.3522 +      let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
  1.3523 +      let docShell = webNav.QueryInterface(Ci.nsIDocShell);
  1.3524 +      docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
  1.3525 +      docViewer.pausePainting();
  1.3526 +
  1.3527 +      BrowserApp.selectedTab.performReflowOnZoom(aViewport);
  1.3528 +      BrowserApp.selectedTab.probablyNeedRefloz = false;
  1.3529 +    }
  1.3530 +
  1.3531 +    let win = this.browser.contentWindow;
  1.3532 +    win.scrollTo(x, y);
  1.3533 +    this.saveSessionZoom(aViewport.zoom);
  1.3534 +
  1.3535 +    this.userScrollPos.x = win.scrollX;
  1.3536 +    this.userScrollPos.y = win.scrollY;
  1.3537 +    this.setResolution(aViewport.zoom, false);
  1.3538 +
  1.3539 +    if (aViewport.displayPort)
  1.3540 +      this.setDisplayPort(aViewport.displayPort);
  1.3541 +
  1.3542 +    // Store fixed margins for later retrieval in getViewport.
  1.3543 +    this._fixedMarginLeft = aViewport.fixedMarginLeft;
  1.3544 +    this._fixedMarginTop = aViewport.fixedMarginTop;
  1.3545 +    this._fixedMarginRight = aViewport.fixedMarginRight;
  1.3546 +    this._fixedMarginBottom = aViewport.fixedMarginBottom;
  1.3547 +
  1.3548 +    let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.3549 +    dwi.setContentDocumentFixedPositionMargins(
  1.3550 +      aViewport.fixedMarginTop / aViewport.zoom,
  1.3551 +      aViewport.fixedMarginRight / aViewport.zoom,
  1.3552 +      aViewport.fixedMarginBottom / aViewport.zoom,
  1.3553 +      aViewport.fixedMarginLeft / aViewport.zoom);
  1.3554 +
  1.3555 +    Services.obs.notifyObservers(null, "after-viewport-change", "");
  1.3556 +    if (docViewer) {
  1.3557 +        docViewer.resumePainting();
  1.3558 +    }
  1.3559 +  },
  1.3560 +
  1.3561 +  setResolution: function(aZoom, aForce) {
  1.3562 +    // Set zoom level
  1.3563 +    if (aForce || !fuzzyEquals(aZoom, this._zoom)) {
  1.3564 +      this._zoom = aZoom;
  1.3565 +      if (BrowserApp.selectedTab == this) {
  1.3566 +        let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.3567 +        this._drawZoom = aZoom;
  1.3568 +        cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio);
  1.3569 +      }
  1.3570 +    }
  1.3571 +  },
  1.3572 +
  1.3573 +  getPageSize: function(aDocument, aDefaultWidth, aDefaultHeight) {
  1.3574 +    let body = aDocument.body || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight };
  1.3575 +    let html = aDocument.documentElement || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight };
  1.3576 +    return [Math.max(body.scrollWidth, html.scrollWidth),
  1.3577 +      Math.max(body.scrollHeight, html.scrollHeight)];
  1.3578 +  },
  1.3579 +
  1.3580 +  getViewport: function() {
  1.3581 +    let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
  1.3582 +    let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
  1.3583 +    let zoom = this.restoredSessionZoom() || this._zoom;
  1.3584 +
  1.3585 +    let viewport = {
  1.3586 +      width: screenW,
  1.3587 +      height: screenH,
  1.3588 +      cssWidth: screenW / zoom,
  1.3589 +      cssHeight: screenH / zoom,
  1.3590 +      pageLeft: 0,
  1.3591 +      pageTop: 0,
  1.3592 +      pageRight: screenW,
  1.3593 +      pageBottom: screenH,
  1.3594 +      // We make up matching css page dimensions
  1.3595 +      cssPageLeft: 0,
  1.3596 +      cssPageTop: 0,
  1.3597 +      cssPageRight: screenW / zoom,
  1.3598 +      cssPageBottom: screenH / zoom,
  1.3599 +      fixedMarginLeft: this._fixedMarginLeft,
  1.3600 +      fixedMarginTop: this._fixedMarginTop,
  1.3601 +      fixedMarginRight: this._fixedMarginRight,
  1.3602 +      fixedMarginBottom: this._fixedMarginBottom,
  1.3603 +      zoom: zoom,
  1.3604 +    };
  1.3605 +
  1.3606 +    // Set the viewport offset to current scroll offset
  1.3607 +    viewport.cssX = this.browser.contentWindow.scrollX || 0;
  1.3608 +    viewport.cssY = this.browser.contentWindow.scrollY || 0;
  1.3609 +
  1.3610 +    // Transform coordinates based on zoom
  1.3611 +    viewport.x = Math.round(viewport.cssX * viewport.zoom);
  1.3612 +    viewport.y = Math.round(viewport.cssY * viewport.zoom);
  1.3613 +
  1.3614 +    let doc = this.browser.contentDocument;
  1.3615 +    if (doc != null) {
  1.3616 +      let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.3617 +      let cssPageRect = cwu.getRootBounds();
  1.3618 +
  1.3619 +      /*
  1.3620 +       * Avoid sending page sizes of less than screen size before we hit DOMContentLoaded, because
  1.3621 +       * this causes the page size to jump around wildly during page load. After the page is loaded,
  1.3622 +       * send updates regardless of page size; we'll zoom to fit the content as needed.
  1.3623 +       *
  1.3624 +       * In the check below, we floor the viewport size because there might be slight rounding errors
  1.3625 +       * introduced in the CSS page size due to the conversion to and from app units in Gecko. The
  1.3626 +       * error should be no more than one app unit so doing the floor is overkill, but safe in the
  1.3627 +       * sense that the extra page size updates that get sent as a result will be mostly harmless.
  1.3628 +       */
  1.3629 +      let pageLargerThanScreen = (cssPageRect.width >= Math.floor(viewport.cssWidth))
  1.3630 +                              && (cssPageRect.height >= Math.floor(viewport.cssHeight));
  1.3631 +      if (doc.readyState === 'complete' || pageLargerThanScreen) {
  1.3632 +        viewport.cssPageLeft = cssPageRect.left;
  1.3633 +        viewport.cssPageTop = cssPageRect.top;
  1.3634 +        viewport.cssPageRight = cssPageRect.right;
  1.3635 +        viewport.cssPageBottom = cssPageRect.bottom;
  1.3636 +        /* Transform the page width and height based on the zoom factor. */
  1.3637 +        viewport.pageLeft = (viewport.cssPageLeft * viewport.zoom);
  1.3638 +        viewport.pageTop = (viewport.cssPageTop * viewport.zoom);
  1.3639 +        viewport.pageRight = (viewport.cssPageRight * viewport.zoom);
  1.3640 +        viewport.pageBottom = (viewport.cssPageBottom * viewport.zoom);
  1.3641 +      }
  1.3642 +    }
  1.3643 +
  1.3644 +    return viewport;
  1.3645 +  },
  1.3646 +
  1.3647 +  sendViewportUpdate: function(aPageSizeUpdate) {
  1.3648 +    let viewport = this.getViewport();
  1.3649 +    let displayPort = Services.androidBridge.getDisplayPort(aPageSizeUpdate, BrowserApp.isBrowserContentDocumentDisplayed(), this.id, viewport);
  1.3650 +    if (displayPort != null)
  1.3651 +      this.setDisplayPort(displayPort);
  1.3652 +  },
  1.3653 +
  1.3654 +  updateViewportForPageSize: function() {
  1.3655 +    let hasHorizontalMargins = gViewportMargins.left != 0 || gViewportMargins.right != 0;
  1.3656 +    let hasVerticalMargins = gViewportMargins.top != 0 || gViewportMargins.bottom != 0;
  1.3657 +
  1.3658 +    if (!hasHorizontalMargins && !hasVerticalMargins) {
  1.3659 +      // If there are no margins, then we don't need to do any remeasuring
  1.3660 +      return;
  1.3661 +    }
  1.3662 +
  1.3663 +    // If the page size has changed so that it might or might not fit on the
  1.3664 +    // screen with the margins included, run updateViewportSize to resize the
  1.3665 +    // browser accordingly.
  1.3666 +    // A page will receive the smaller viewport when its page size fits
  1.3667 +    // within the screen size, so remeasure when the page size remains within
  1.3668 +    // the threshold of screen + margins, in case it's sizing itself relative
  1.3669 +    // to the viewport.
  1.3670 +    let viewport = this.getViewport();
  1.3671 +    let pageWidth = viewport.pageRight - viewport.pageLeft;
  1.3672 +    let pageHeight = viewport.pageBottom - viewport.pageTop;
  1.3673 +    let remeasureNeeded = false;
  1.3674 +
  1.3675 +    if (hasHorizontalMargins) {
  1.3676 +      let viewportShouldExcludeHorizontalMargins = (pageWidth <= gScreenWidth - 0.5);
  1.3677 +      if (viewportShouldExcludeHorizontalMargins != this.viewportExcludesHorizontalMargins) {
  1.3678 +        remeasureNeeded = true;
  1.3679 +      }
  1.3680 +    }
  1.3681 +    if (hasVerticalMargins) {
  1.3682 +      let viewportShouldExcludeVerticalMargins = (pageHeight <= gScreenHeight - 0.5);
  1.3683 +      if (viewportShouldExcludeVerticalMargins != this.viewportExcludesVerticalMargins) {
  1.3684 +        remeasureNeeded = true;
  1.3685 +      }
  1.3686 +    }
  1.3687 +
  1.3688 +    if (remeasureNeeded) {
  1.3689 +      if (!this.viewportMeasureCallback) {
  1.3690 +        this.viewportMeasureCallback = setTimeout(function() {
  1.3691 +          this.viewportMeasureCallback = null;
  1.3692 +
  1.3693 +          // Re-fetch the viewport as it may have changed between setting the timeout
  1.3694 +          // and running this callback
  1.3695 +          let viewport = this.getViewport();
  1.3696 +          let pageWidth = viewport.pageRight - viewport.pageLeft;
  1.3697 +          let pageHeight = viewport.pageBottom - viewport.pageTop;
  1.3698 +
  1.3699 +          if (Math.abs(pageWidth - this.lastPageSizeAfterViewportRemeasure.width) >= 0.5 ||
  1.3700 +              Math.abs(pageHeight - this.lastPageSizeAfterViewportRemeasure.height) >= 0.5) {
  1.3701 +            this.updateViewportSize(gScreenWidth);
  1.3702 +          }
  1.3703 +        }.bind(this), kViewportRemeasureThrottle);
  1.3704 +      }
  1.3705 +    } else if (this.viewportMeasureCallback) {
  1.3706 +      // If the page changed size twice since we last measured the viewport and
  1.3707 +      // the latest size change reveals we don't need to remeasure, cancel any
  1.3708 +      // pending remeasure.
  1.3709 +      clearTimeout(this.viewportMeasureCallback);
  1.3710 +      this.viewportMeasureCallback = null;
  1.3711 +    }
  1.3712 +  },
  1.3713 +
  1.3714 +  handleEvent: function(aEvent) {
  1.3715 +    switch (aEvent.type) {
  1.3716 +      case "DOMContentLoaded": {
  1.3717 +        let target = aEvent.originalTarget;
  1.3718 +
  1.3719 +        // ignore on frames and other documents
  1.3720 +        if (target != this.browser.contentDocument)
  1.3721 +          return;
  1.3722 +
  1.3723 +        // Sample the background color of the page and pass it along. (This is used to draw the
  1.3724 +        // checkerboard.) Right now we don't detect changes in the background color after this
  1.3725 +        // event fires; it's not clear that doing so is worth the effort.
  1.3726 +        var backgroundColor = null;
  1.3727 +        try {
  1.3728 +          let { contentDocument, contentWindow } = this.browser;
  1.3729 +          let computedStyle = contentWindow.getComputedStyle(contentDocument.body);
  1.3730 +          backgroundColor = computedStyle.backgroundColor;
  1.3731 +        } catch (e) {
  1.3732 +          // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds.
  1.3733 +        }
  1.3734 +
  1.3735 +        let docURI = target.documentURI;
  1.3736 +        let errorType = "";
  1.3737 +        if (docURI.startsWith("about:certerror"))
  1.3738 +          errorType = "certerror";
  1.3739 +        else if (docURI.startsWith("about:blocked"))
  1.3740 +          errorType = "blocked"
  1.3741 +        else if (docURI.startsWith("about:neterror"))
  1.3742 +          errorType = "neterror";
  1.3743 +
  1.3744 +        sendMessageToJava({
  1.3745 +          type: "DOMContentLoaded",
  1.3746 +          tabID: this.id,
  1.3747 +          bgColor: backgroundColor,
  1.3748 +          errorType: errorType
  1.3749 +        });
  1.3750 +
  1.3751 +        // Attach a listener to watch for "click" events bubbling up from error
  1.3752 +        // pages and other similar page. This lets us fix bugs like 401575 which
  1.3753 +        // require error page UI to do privileged things, without letting error
  1.3754 +        // pages have any privilege themselves.
  1.3755 +        if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) {
  1.3756 +          this.browser.addEventListener("click", ErrorPageEventHandler, true);
  1.3757 +          let listener = function() {
  1.3758 +            this.browser.removeEventListener("click", ErrorPageEventHandler, true);
  1.3759 +            this.browser.removeEventListener("pagehide", listener, true);
  1.3760 +          }.bind(this);
  1.3761 +
  1.3762 +          this.browser.addEventListener("pagehide", listener, true);
  1.3763 +        }
  1.3764 +
  1.3765 +        if (docURI.startsWith("about:reader")) {
  1.3766 +          // During browser restart / recovery, duplicate "DOMContentLoaded" messages are received here
  1.3767 +          // For the visible tab ... where more than one tab is being reloaded, the inital "DOMContentLoaded"
  1.3768 +          // Message can be received before the document body is available ... so we avoid instantiating an
  1.3769 +          // AboutReader object, expecting that an eventual valid message will follow.
  1.3770 +          let contentDocument = this.browser.contentDocument;
  1.3771 +          if (contentDocument.body) {
  1.3772 +            new AboutReader(contentDocument, this.browser.contentWindow);
  1.3773 +          }
  1.3774 +        }
  1.3775 +
  1.3776 +        break;
  1.3777 +      }
  1.3778 +
  1.3779 +      case "DOMFormHasPassword": {
  1.3780 +        LoginManagerContent.onFormPassword(aEvent);
  1.3781 +        break;
  1.3782 +      }
  1.3783 +
  1.3784 +      case "DOMLinkAdded": {
  1.3785 +        let target = aEvent.originalTarget;
  1.3786 +        if (!target.href || target.disabled)
  1.3787 +          return;
  1.3788 +
  1.3789 +        // Ignore on frames and other documents
  1.3790 +        if (target.ownerDocument != this.browser.contentDocument)
  1.3791 +          return;
  1.3792 +
  1.3793 +        // Sanitize the rel string
  1.3794 +        let list = [];
  1.3795 +        if (target.rel) {
  1.3796 +          list = target.rel.toLowerCase().split(/\s+/);
  1.3797 +          let hash = {};
  1.3798 +          list.forEach(function(value) { hash[value] = true; });
  1.3799 +          list = [];
  1.3800 +          for (let rel in hash)
  1.3801 +            list.push("[" + rel + "]");
  1.3802 +        }
  1.3803 +
  1.3804 +        if (list.indexOf("[icon]") != -1) {
  1.3805 +          // We want to get the largest icon size possible for our UI.
  1.3806 +          let maxSize = 0;
  1.3807 +
  1.3808 +          // We use the sizes attribute if available
  1.3809 +          // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
  1.3810 +          if (target.hasAttribute("sizes")) {
  1.3811 +            let sizes = target.getAttribute("sizes").toLowerCase();
  1.3812 +
  1.3813 +            if (sizes == "any") {
  1.3814 +              // Since Java expects an integer, use -1 to represent icons with sizes="any"
  1.3815 +              maxSize = -1; 
  1.3816 +            } else {
  1.3817 +              let tokens = sizes.split(" ");
  1.3818 +              tokens.forEach(function(token) {
  1.3819 +                // TODO: check for invalid tokens
  1.3820 +                let [w, h] = token.split("x");
  1.3821 +                maxSize = Math.max(maxSize, Math.max(w, h));
  1.3822 +              });
  1.3823 +            }
  1.3824 +          }
  1.3825 +
  1.3826 +          let json = {
  1.3827 +            type: "Link:Favicon",
  1.3828 +            tabID: this.id,
  1.3829 +            href: resolveGeckoURI(target.href),
  1.3830 +            charset: target.ownerDocument.characterSet,
  1.3831 +            title: target.title,
  1.3832 +            rel: list.join(" "),
  1.3833 +            size: maxSize
  1.3834 +          };
  1.3835 +          sendMessageToJava(json);
  1.3836 +        } else if (list.indexOf("[alternate]") != -1) {
  1.3837 +          let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
  1.3838 +          let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
  1.3839 +
  1.3840 +          if (!isFeed)
  1.3841 +            return;
  1.3842 +
  1.3843 +          try {
  1.3844 +            // urlSecurityCeck will throw if things are not OK
  1.3845 +            ContentAreaUtils.urlSecurityCheck(target.href, target.ownerDocument.nodePrincipal, Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
  1.3846 +
  1.3847 +            if (!this.browser.feeds)
  1.3848 +              this.browser.feeds = [];
  1.3849 +            this.browser.feeds.push({ href: target.href, title: target.title, type: type });
  1.3850 +
  1.3851 +            let json = {
  1.3852 +              type: "Link:Feed",
  1.3853 +              tabID: this.id
  1.3854 +            };
  1.3855 +            sendMessageToJava(json);
  1.3856 +          } catch (e) {}
  1.3857 +        } else if (list.indexOf("[search]" != -1)) {
  1.3858 +          let type = target.type && target.type.toLowerCase();
  1.3859 +
  1.3860 +          // Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
  1.3861 +          type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
  1.3862 +
  1.3863 +          // Check that type matches opensearch.
  1.3864 +          let isOpenSearch = (type == "application/opensearchdescription+xml");
  1.3865 +          if (isOpenSearch && target.title && /^(?:https?|ftp):/i.test(target.href)) {
  1.3866 +            let visibleEngines = Services.search.getVisibleEngines();
  1.3867 +            // NOTE: Engines are currently identified by name, but this can be changed
  1.3868 +            // when Engines are identified by URL (see bug 335102).
  1.3869 +            if (visibleEngines.some(function(e) {
  1.3870 +              return e.name == target.title;
  1.3871 +            })) {
  1.3872 +              // This engine is already present, do nothing.
  1.3873 +              return;
  1.3874 +            }
  1.3875 +
  1.3876 +            if (this.browser.engines) {
  1.3877 +              // This engine has already been handled, do nothing.
  1.3878 +              if (this.browser.engines.some(function(e) {
  1.3879 +                return e.url == target.href;
  1.3880 +              })) {
  1.3881 +                  return;
  1.3882 +              }
  1.3883 +            } else {
  1.3884 +              this.browser.engines = [];
  1.3885 +            }
  1.3886 +
  1.3887 +            // Get favicon.
  1.3888 +            let iconURL = target.ownerDocument.documentURIObject.prePath + "/favicon.ico";
  1.3889 +
  1.3890 +            let newEngine = {
  1.3891 +              title: target.title,
  1.3892 +              url: target.href,
  1.3893 +              iconURL: iconURL
  1.3894 +            };
  1.3895 +
  1.3896 +            this.browser.engines.push(newEngine);
  1.3897 +
  1.3898 +            // Don't send a message to display engines if we've already handled an engine.
  1.3899 +            if (this.browser.engines.length > 1)
  1.3900 +              return;
  1.3901 +
  1.3902 +            // Broadcast message that this tab contains search engines that should be visible.
  1.3903 +            let newEngineMessage = {
  1.3904 +              type: "Link:OpenSearch",
  1.3905 +              tabID: this.id,
  1.3906 +              visible: true
  1.3907 +            };
  1.3908 +
  1.3909 +            sendMessageToJava(newEngineMessage);
  1.3910 +          }
  1.3911 +        }
  1.3912 +        break;
  1.3913 +      }
  1.3914 +
  1.3915 +      case "DOMTitleChanged": {
  1.3916 +        if (!aEvent.isTrusted)
  1.3917 +          return;
  1.3918 +
  1.3919 +        // ignore on frames and other documents
  1.3920 +        if (aEvent.originalTarget != this.browser.contentDocument)
  1.3921 +          return;
  1.3922 +
  1.3923 +        sendMessageToJava({
  1.3924 +          type: "DOMTitleChanged",
  1.3925 +          tabID: this.id,
  1.3926 +          title: aEvent.target.title.substring(0, 255)
  1.3927 +        });
  1.3928 +        break;
  1.3929 +      }
  1.3930 +
  1.3931 +      case "DOMWindowClose": {
  1.3932 +        if (!aEvent.isTrusted)
  1.3933 +          return;
  1.3934 +
  1.3935 +        // Find the relevant tab, and close it from Java
  1.3936 +        if (this.browser.contentWindow == aEvent.target) {
  1.3937 +          aEvent.preventDefault();
  1.3938 +
  1.3939 +          sendMessageToJava({
  1.3940 +            type: "Tab:Close",
  1.3941 +            tabID: this.id
  1.3942 +          });
  1.3943 +        }
  1.3944 +        break;
  1.3945 +      }
  1.3946 +
  1.3947 +      case "DOMWillOpenModalDialog": {
  1.3948 +        if (!aEvent.isTrusted)
  1.3949 +          return;
  1.3950 +
  1.3951 +        // We're about to open a modal dialog, make sure the opening
  1.3952 +        // tab is brought to the front.
  1.3953 +        let tab = BrowserApp.getTabForWindow(aEvent.target.top);
  1.3954 +        BrowserApp.selectTab(tab);
  1.3955 +        break;
  1.3956 +      }
  1.3957 +
  1.3958 +      case "DOMAutoComplete":
  1.3959 +      case "blur": {
  1.3960 +        LoginManagerContent.onUsernameInput(aEvent);
  1.3961 +        break;
  1.3962 +      }
  1.3963 +
  1.3964 +      case "scroll": {
  1.3965 +        let win = this.browser.contentWindow;
  1.3966 +        if (this.userScrollPos.x != win.scrollX || this.userScrollPos.y != win.scrollY) {
  1.3967 +          this.sendViewportUpdate();
  1.3968 +        }
  1.3969 +        break;
  1.3970 +      }
  1.3971 +
  1.3972 +      case "MozScrolledAreaChanged": {
  1.3973 +        // This event is only fired for root scroll frames, and only when the
  1.3974 +        // scrolled area has actually changed, so no need to check for that.
  1.3975 +        // Just make sure it's the event for the correct root scroll frame.
  1.3976 +        if (aEvent.originalTarget != this.browser.contentDocument)
  1.3977 +          return;
  1.3978 +
  1.3979 +        this.sendViewportUpdate(true);
  1.3980 +        this.updateViewportForPageSize();
  1.3981 +        break;
  1.3982 +      }
  1.3983 +
  1.3984 +      case "PluginBindingAttached": {
  1.3985 +        PluginHelper.handlePluginBindingAttached(this, aEvent);
  1.3986 +        break;
  1.3987 +      }
  1.3988 +
  1.3989 +      case "VideoBindingAttached": {
  1.3990 +        CastingApps.handleVideoBindingAttached(this, aEvent);
  1.3991 +        break;
  1.3992 +      }
  1.3993 +
  1.3994 +      case "VideoBindingCast": {
  1.3995 +        CastingApps.handleVideoBindingCast(this, aEvent);
  1.3996 +        break;
  1.3997 +      }
  1.3998 +
  1.3999 +      case "MozApplicationManifest": {
  1.4000 +        OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView);
  1.4001 +        break;
  1.4002 +      }
  1.4003 +
  1.4004 +      case "pageshow": {
  1.4005 +        // only send pageshow for the top-level document
  1.4006 +        if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
  1.4007 +          return;
  1.4008 +
  1.4009 +        sendMessageToJava({
  1.4010 +          type: "Content:PageShow",
  1.4011 +          tabID: this.id
  1.4012 +        });
  1.4013 +
  1.4014 +        if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
  1.4015 +          if (!this._linkifier)
  1.4016 +            this._linkifier = new Linkifier();
  1.4017 +          this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
  1.4018 +        }
  1.4019 +
  1.4020 +        // Update page actions for helper apps.
  1.4021 +        let uri = this.browser.currentURI;
  1.4022 +        if (BrowserApp.selectedTab == this) {
  1.4023 +          if (ExternalApps.shouldCheckUri(uri)) {
  1.4024 +            ExternalApps.updatePageAction(uri);
  1.4025 +          } else {
  1.4026 +            ExternalApps.clearPageAction();
  1.4027 +          }
  1.4028 +        }
  1.4029 +
  1.4030 +        if (!Reader.isEnabledForParseOnLoad)
  1.4031 +          return;
  1.4032 +
  1.4033 +        // Once document is fully loaded, parse it
  1.4034 +        Reader.parseDocumentFromTab(this.id, function (article) {
  1.4035 +          // Do nothing if there's no article or the page in this tab has
  1.4036 +          // changed
  1.4037 +          let tabURL = uri.specIgnoringRef;
  1.4038 +          if (article == null || (article.url != tabURL)) {
  1.4039 +            // Don't clear the article for about:reader pages since we want to
  1.4040 +            // use the article from the previous page
  1.4041 +            if (!tabURL.startsWith("about:reader")) {
  1.4042 +              this.savedArticle = null;
  1.4043 +              this.readerEnabled = false;
  1.4044 +              this.readerActive = false;
  1.4045 +            } else {
  1.4046 +              this.readerActive = true;
  1.4047 +            }
  1.4048 +            return;
  1.4049 +          }
  1.4050 +
  1.4051 +          this.savedArticle = article;
  1.4052 +
  1.4053 +          sendMessageToJava({
  1.4054 +            type: "Content:ReaderEnabled",
  1.4055 +            tabID: this.id
  1.4056 +          });
  1.4057 +
  1.4058 +          if(this.readerActive)
  1.4059 +            this.readerActive = false;
  1.4060 +
  1.4061 +          if(!this.readerEnabled)
  1.4062 +            this.readerEnabled = true;
  1.4063 +        }.bind(this));
  1.4064 +      }
  1.4065 +    }
  1.4066 +  },
  1.4067 +
  1.4068 +  onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
  1.4069 +    let contentWin = aWebProgress.DOMWindow;
  1.4070 +    if (contentWin != contentWin.top)
  1.4071 +        return;
  1.4072 +
  1.4073 +    // Filter optimization: Only really send NETWORK state changes to Java listener
  1.4074 +    if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
  1.4075 +      if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) {
  1.4076 +        // We may receive a document stop event while a document is still loading
  1.4077 +        // (such as when doing URI fixup). Don't notify Java UI in these cases.
  1.4078 +        return;
  1.4079 +      }
  1.4080 +
  1.4081 +      // Clear page-specific opensearch engines and feeds for a new request.
  1.4082 +      if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) {
  1.4083 +        this.browser.engines = null;
  1.4084 +        this.browser.feeds = null;
  1.4085 +      }
  1.4086 +
  1.4087 +      // true if the page loaded successfully (i.e., no 404s or other errors)
  1.4088 +      let success = false;
  1.4089 +      let uri = "";
  1.4090 +      try {
  1.4091 +        // Remember original URI for UA changes on redirected pages
  1.4092 +        this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI;
  1.4093 +
  1.4094 +        if (this.originalURI != null)
  1.4095 +          uri = this.originalURI.spec;
  1.4096 +      } catch (e) { }
  1.4097 +      try {
  1.4098 +        success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded;
  1.4099 +      } catch (e) {
  1.4100 +        // If the request does not handle the nsIHttpChannel interface, use nsIRequest's success
  1.4101 +        // status. Used for local files. See bug 948849.
  1.4102 +        success = aRequest.status == 0;
  1.4103 +      }
  1.4104 +
  1.4105 +      // Check to see if we restoring the content from a previous presentation (session)
  1.4106 +      // since there should be no real network activity
  1.4107 +      let restoring = (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) > 0;
  1.4108 +
  1.4109 +      let message = {
  1.4110 +        type: "Content:StateChange",
  1.4111 +        tabID: this.id,
  1.4112 +        uri: uri,
  1.4113 +        state: aStateFlags,
  1.4114 +        restoring: restoring,
  1.4115 +        success: success
  1.4116 +      };
  1.4117 +      sendMessageToJava(message);
  1.4118 +    }
  1.4119 +  },
  1.4120 +
  1.4121 +  onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) {
  1.4122 +    let contentWin = aWebProgress.DOMWindow;
  1.4123 +
  1.4124 +    // Browser webapps may load content inside iframes that can not reach across the app/frame boundary
  1.4125 +    // i.e. even though the page is loaded in an iframe window.top != webapp
  1.4126 +    // Make cure this window is a top level tab before moving on.
  1.4127 +    if (BrowserApp.getBrowserForWindow(contentWin) == null)
  1.4128 +      return;
  1.4129 +
  1.4130 +    this._hostChanged = true;
  1.4131 +
  1.4132 +    let fixedURI = aLocationURI;
  1.4133 +    try {
  1.4134 +      fixedURI = URIFixup.createExposableURI(aLocationURI);
  1.4135 +    } catch (ex) { }
  1.4136 +
  1.4137 +    let contentType = contentWin.document.contentType;
  1.4138 +
  1.4139 +    // If fixedURI matches browser.lastURI, we assume this isn't a real location
  1.4140 +    // change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883.
  1.4141 +    // Note that we have to ensure fixedURI is not the same as aLocationURI so we
  1.4142 +    // don't false-positive page reloads as spurious additions.
  1.4143 +    let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 ||
  1.4144 +                       ((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI));
  1.4145 +    this.browser.lastURI = fixedURI;
  1.4146 +
  1.4147 +    // Reset state of click-to-play plugin notifications.
  1.4148 +    clearTimeout(this.pluginDoorhangerTimeout);
  1.4149 +    this.pluginDoorhangerTimeout = null;
  1.4150 +    this.shouldShowPluginDoorhanger = true;
  1.4151 +    this.clickToPlayPluginsActivated = false;
  1.4152 +    // Borrowed from desktop Firefox: http://mxr.mozilla.org/mozilla-central/source/browser/base/content/urlbarBindings.xml#174
  1.4153 +    let documentURI = contentWin.document.documentURIObject.spec
  1.4154 +    let matchedURL = documentURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
  1.4155 +    let baseDomain = "";
  1.4156 +    if (matchedURL) {
  1.4157 +      var domain = "";
  1.4158 +      [, , domain] = matchedURL;
  1.4159 +
  1.4160 +      try {
  1.4161 +        baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
  1.4162 +        if (!domain.endsWith(baseDomain)) {
  1.4163 +          // getBaseDomainFromHost converts its resultant to ACE.
  1.4164 +          let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
  1.4165 +          baseDomain = IDNService.convertACEtoUTF8(baseDomain);
  1.4166 +        }
  1.4167 +      } catch (e) {}
  1.4168 +    }
  1.4169 +
  1.4170 +    // Update the page actions URI for helper apps.
  1.4171 +    if (BrowserApp.selectedTab == this) {
  1.4172 +      ExternalApps.updatePageActionUri(fixedURI);
  1.4173 +    }
  1.4174 +
  1.4175 +    let message = {
  1.4176 +      type: "Content:LocationChange",
  1.4177 +      tabID: this.id,
  1.4178 +      uri: fixedURI.spec,
  1.4179 +      userSearch: this.userSearch || "",
  1.4180 +      baseDomain: baseDomain,
  1.4181 +      contentType: (contentType ? contentType : ""),
  1.4182 +      sameDocument: sameDocument
  1.4183 +    };
  1.4184 +
  1.4185 +    sendMessageToJava(message);
  1.4186 +
  1.4187 +    // The search term is only valid for this location change event, so reset it here.
  1.4188 +    this.userSearch = "";
  1.4189 +
  1.4190 +    if (!sameDocument) {
  1.4191 +      // XXX This code assumes that this is the earliest hook we have at which
  1.4192 +      // browser.contentDocument is changed to the new document we're loading
  1.4193 +      this.contentDocumentIsDisplayed = false;
  1.4194 +      this.hasTouchListener = false;
  1.4195 +    } else {
  1.4196 +      this.sendViewportUpdate();
  1.4197 +    }
  1.4198 +  },
  1.4199 +
  1.4200 +  // Properties used to cache security state used to update the UI
  1.4201 +  _state: null,
  1.4202 +  _hostChanged: false, // onLocationChange will flip this bit
  1.4203 +
  1.4204 +  onSecurityChange: function(aWebProgress, aRequest, aState) {
  1.4205 +    // Don't need to do anything if the data we use to update the UI hasn't changed
  1.4206 +    if (this._state == aState && !this._hostChanged)
  1.4207 +      return;
  1.4208 +
  1.4209 +    this._state = aState;
  1.4210 +    this._hostChanged = false;
  1.4211 +
  1.4212 +    let identity = IdentityHandler.checkIdentity(aState, this.browser);
  1.4213 +
  1.4214 +    let message = {
  1.4215 +      type: "Content:SecurityChange",
  1.4216 +      tabID: this.id,
  1.4217 +      identity: identity
  1.4218 +    };
  1.4219 +
  1.4220 +    sendMessageToJava(message);
  1.4221 +  },
  1.4222 +
  1.4223 +  onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
  1.4224 +  },
  1.4225 +
  1.4226 +  onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
  1.4227 +  },
  1.4228 +
  1.4229 +  _sendHistoryEvent: function(aMessage, aParams) {
  1.4230 +    let message = {
  1.4231 +      type: "SessionHistory:" + aMessage,
  1.4232 +      tabID: this.id,
  1.4233 +    };
  1.4234 +
  1.4235 +    // Restore zoom only when moving in session history, not for new page loads.
  1.4236 +    this._restoreZoom = aMessage != "New";
  1.4237 +
  1.4238 +    if (aParams) {
  1.4239 +      if ("url" in aParams)
  1.4240 +        message.url = aParams.url;
  1.4241 +      if ("index" in aParams)
  1.4242 +        message.index = aParams.index;
  1.4243 +      if ("numEntries" in aParams)
  1.4244 +        message.numEntries = aParams.numEntries;
  1.4245 +    }
  1.4246 +
  1.4247 +    sendMessageToJava(message);
  1.4248 +  },
  1.4249 +
  1.4250 +  _getGeckoZoom: function() {
  1.4251 +    let res = {x: {}, y: {}};
  1.4252 +    let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.4253 +    cwu.getResolution(res.x, res.y);
  1.4254 +    let zoom = res.x.value * window.devicePixelRatio;
  1.4255 +    return zoom;
  1.4256 +  },
  1.4257 +
  1.4258 +  saveSessionZoom: function(aZoom) {
  1.4259 +    let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.4260 +    cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio);
  1.4261 +  },
  1.4262 +
  1.4263 +  restoredSessionZoom: function() {
  1.4264 +    let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.4265 +
  1.4266 +    if (this._restoreZoom && cwu.isResolutionSet) {
  1.4267 +      return this._getGeckoZoom();
  1.4268 +    }
  1.4269 +    return null;
  1.4270 +  },
  1.4271 +
  1.4272 +  OnHistoryNewEntry: function(aUri) {
  1.4273 +    this._sendHistoryEvent("New", { url: aUri.spec });
  1.4274 +  },
  1.4275 +
  1.4276 +  OnHistoryGoBack: function(aUri) {
  1.4277 +    this._sendHistoryEvent("Back");
  1.4278 +    return true;
  1.4279 +  },
  1.4280 +
  1.4281 +  OnHistoryGoForward: function(aUri) {
  1.4282 +    this._sendHistoryEvent("Forward");
  1.4283 +    return true;
  1.4284 +  },
  1.4285 +
  1.4286 +  OnHistoryReload: function(aUri, aFlags) {
  1.4287 +    // we don't do anything with this, so don't propagate it
  1.4288 +    // for now anyway
  1.4289 +    return true;
  1.4290 +  },
  1.4291 +
  1.4292 +  OnHistoryGotoIndex: function(aIndex, aUri) {
  1.4293 +    this._sendHistoryEvent("Goto", { index: aIndex });
  1.4294 +    return true;
  1.4295 +  },
  1.4296 +
  1.4297 +  OnHistoryPurge: function(aNumEntries) {
  1.4298 +    this._sendHistoryEvent("Purge", { numEntries: aNumEntries });
  1.4299 +    return true;
  1.4300 +  },
  1.4301 +
  1.4302 +  OnHistoryReplaceEntry: function(aIndex) {
  1.4303 +    // we don't do anything with this, so don't propogate it
  1.4304 +    // for now anyway.
  1.4305 +  },
  1.4306 +
  1.4307 +  get metadata() {
  1.4308 +    return ViewportHandler.getMetadataForDocument(this.browser.contentDocument);
  1.4309 +  },
  1.4310 +
  1.4311 +  /** Update viewport when the metadata changes. */
  1.4312 +  updateViewportMetadata: function updateViewportMetadata(aMetadata, aInitialLoad) {
  1.4313 +    if (Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) {
  1.4314 +      aMetadata.allowZoom = true;
  1.4315 +      aMetadata.allowDoubleTapZoom = true;
  1.4316 +      aMetadata.minZoom = aMetadata.maxZoom = NaN;
  1.4317 +    }
  1.4318 +
  1.4319 +    let scaleRatio = window.devicePixelRatio;
  1.4320 +
  1.4321 +    if (aMetadata.defaultZoom > 0)
  1.4322 +      aMetadata.defaultZoom *= scaleRatio;
  1.4323 +    if (aMetadata.minZoom > 0)
  1.4324 +      aMetadata.minZoom *= scaleRatio;
  1.4325 +    if (aMetadata.maxZoom > 0)
  1.4326 +      aMetadata.maxZoom *= scaleRatio;
  1.4327 +
  1.4328 +    aMetadata.isRTL = this.browser.contentDocument.documentElement.dir == "rtl";
  1.4329 +
  1.4330 +    ViewportHandler.setMetadataForDocument(this.browser.contentDocument, aMetadata);
  1.4331 +    this.sendViewportMetadata();
  1.4332 +
  1.4333 +    this.updateViewportSize(gScreenWidth, aInitialLoad);
  1.4334 +  },
  1.4335 +
  1.4336 +  /** Update viewport when the metadata or the window size changes. */
  1.4337 +  updateViewportSize: function updateViewportSize(aOldScreenWidth, aInitialLoad) {
  1.4338 +    // When this function gets called on window resize, we must execute
  1.4339 +    // this.sendViewportUpdate() so that refreshDisplayPort is called.
  1.4340 +    // Ensure that when making changes to this function that code path
  1.4341 +    // is not accidentally removed (the call to sendViewportUpdate() is
  1.4342 +    // at the very end).
  1.4343 +
  1.4344 +    if (this.viewportMeasureCallback) {
  1.4345 +      clearTimeout(this.viewportMeasureCallback);
  1.4346 +      this.viewportMeasureCallback = null;
  1.4347 +    }
  1.4348 +
  1.4349 +    let browser = this.browser;
  1.4350 +    if (!browser)
  1.4351 +      return;
  1.4352 +
  1.4353 +    let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
  1.4354 +    let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
  1.4355 +    let viewportW, viewportH;
  1.4356 +
  1.4357 +    let metadata = this.metadata;
  1.4358 +    if (metadata.autoSize) {
  1.4359 +      viewportW = screenW / window.devicePixelRatio;
  1.4360 +      viewportH = screenH / window.devicePixelRatio;
  1.4361 +    } else {
  1.4362 +      viewportW = metadata.width;
  1.4363 +      viewportH = metadata.height;
  1.4364 +
  1.4365 +      // If (scale * width) < device-width, increase the width (bug 561413).
  1.4366 +      let maxInitialZoom = metadata.defaultZoom || metadata.maxZoom;
  1.4367 +      if (maxInitialZoom && viewportW) {
  1.4368 +        viewportW = Math.max(viewportW, screenW / maxInitialZoom);
  1.4369 +      }
  1.4370 +
  1.4371 +      let validW = viewportW > 0;
  1.4372 +      let validH = viewportH > 0;
  1.4373 +
  1.4374 +      if (!validW)
  1.4375 +        viewportW = validH ? (viewportH * (screenW / screenH)) : BrowserApp.defaultBrowserWidth;
  1.4376 +      if (!validH)
  1.4377 +        viewportH = viewportW * (screenH / screenW);
  1.4378 +    }
  1.4379 +
  1.4380 +    // Make sure the viewport height is not shorter than the window when
  1.4381 +    // the page is zoomed out to show its full width. Note that before
  1.4382 +    // we set the viewport width, the "full width" of the page isn't properly
  1.4383 +    // defined, so that's why we have to call setBrowserSize twice - once
  1.4384 +    // to set the width, and the second time to figure out the height based
  1.4385 +    // on the layout at that width.
  1.4386 +    let oldBrowserWidth = this.browserWidth;
  1.4387 +    this.setBrowserSize(viewportW, viewportH);
  1.4388 +
  1.4389 +    // This change to the zoom accounts for all types of changes I can conceive:
  1.4390 +    // 1. screen size changes, CSS viewport does not (pages with no meta viewport
  1.4391 +    //    or a fixed size viewport)
  1.4392 +    // 2. screen size changes, CSS viewport also does (pages with a device-width
  1.4393 +    //    viewport)
  1.4394 +    // 3. screen size remains constant, but CSS viewport changes (meta viewport
  1.4395 +    //    tag is added or removed)
  1.4396 +    // 4. neither screen size nor CSS viewport changes
  1.4397 +    //
  1.4398 +    // In all of these cases, we maintain how much actual content is visible
  1.4399 +    // within the screen width. Note that "actual content" may be different
  1.4400 +    // with respect to CSS pixels because of the CSS viewport size changing.
  1.4401 +    let zoom = this.restoredSessionZoom() || metadata.defaultZoom;
  1.4402 +    if (!zoom || !aInitialLoad) {
  1.4403 +      let zoomScale = (screenW * oldBrowserWidth) / (aOldScreenWidth * viewportW);
  1.4404 +      zoom = this.clampZoom(this._zoom * zoomScale);
  1.4405 +    }
  1.4406 +    this.setResolution(zoom, false);
  1.4407 +    this.setScrollClampingSize(zoom);
  1.4408 +
  1.4409 +    // if this page has not been painted yet, then this must be getting run
  1.4410 +    // because a meta-viewport element was added (via the DOMMetaAdded handler).
  1.4411 +    // in this case, we should not do anything that forces a reflow (see bug 759678)
  1.4412 +    // such as requesting the page size or sending a viewport update. this code
  1.4413 +    // will get run again in the before-first-paint handler and that point we
  1.4414 +    // will run though all of it. the reason we even bother executing up to this
  1.4415 +    // point on the DOMMetaAdded handler is so that scripts that use window.innerWidth
  1.4416 +    // before they are painted have a correct value (bug 771575).
  1.4417 +    if (!this.contentDocumentIsDisplayed) {
  1.4418 +      return;
  1.4419 +    }
  1.4420 +
  1.4421 +    this.viewportExcludesHorizontalMargins = true;
  1.4422 +    this.viewportExcludesVerticalMargins = true;
  1.4423 +    let minScale = 1.0;
  1.4424 +    if (this.browser.contentDocument) {
  1.4425 +      // this may get run during a Viewport:Change message while the document
  1.4426 +      // has not yet loaded, so need to guard against a null document.
  1.4427 +      let [pageWidth, pageHeight] = this.getPageSize(this.browser.contentDocument, viewportW, viewportH);
  1.4428 +
  1.4429 +      // In the situation the page size equals or exceeds the screen size,
  1.4430 +      // lengthen the viewport on the corresponding axis to include the margins.
  1.4431 +      // The '- 0.5' is to account for rounding errors.
  1.4432 +      if (pageWidth * this._zoom > gScreenWidth - 0.5) {
  1.4433 +        screenW = gScreenWidth;
  1.4434 +        this.viewportExcludesHorizontalMargins = false;
  1.4435 +      }
  1.4436 +      if (pageHeight * this._zoom > gScreenHeight - 0.5) {
  1.4437 +        screenH = gScreenHeight;
  1.4438 +        this.viewportExcludesVerticalMargins = false;
  1.4439 +      }
  1.4440 +
  1.4441 +      minScale = screenW / pageWidth;
  1.4442 +    }
  1.4443 +    minScale = this.clampZoom(minScale);
  1.4444 +    viewportH = Math.max(viewportH, screenH / minScale);
  1.4445 +
  1.4446 +    // In general we want to keep calls to setBrowserSize and setScrollClampingSize
  1.4447 +    // together because setBrowserSize could mark the viewport size as dirty, creating
  1.4448 +    // a pending resize event for content. If that resize gets dispatched (which happens
  1.4449 +    // on the next reflow) without setScrollClampingSize having being called, then
  1.4450 +    // content might be exposed to incorrect innerWidth/innerHeight values.
  1.4451 +    this.setBrowserSize(viewportW, viewportH);
  1.4452 +    this.setScrollClampingSize(zoom);
  1.4453 +
  1.4454 +    // Avoid having the scroll position jump around after device rotation.
  1.4455 +    let win = this.browser.contentWindow;
  1.4456 +    this.userScrollPos.x = win.scrollX;
  1.4457 +    this.userScrollPos.y = win.scrollY;
  1.4458 +
  1.4459 +    this.sendViewportUpdate();
  1.4460 +
  1.4461 +    if (metadata.allowZoom && !Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) {
  1.4462 +      // If the CSS viewport is narrower than the screen (i.e. width <= device-width)
  1.4463 +      // then we disable double-tap-to-zoom behaviour.
  1.4464 +      var oldAllowDoubleTapZoom = metadata.allowDoubleTapZoom;
  1.4465 +      var newAllowDoubleTapZoom = (!metadata.isSpecified) || (viewportW > screenW / window.devicePixelRatio);
  1.4466 +      if (oldAllowDoubleTapZoom !== newAllowDoubleTapZoom) {
  1.4467 +        metadata.allowDoubleTapZoom = newAllowDoubleTapZoom;
  1.4468 +        this.sendViewportMetadata();
  1.4469 +      }
  1.4470 +    }
  1.4471 +
  1.4472 +    // Store the page size that was used to calculate the viewport so that we
  1.4473 +    // can verify it's changed when we consider remeasuring in updateViewportForPageSize
  1.4474 +    let viewport = this.getViewport();
  1.4475 +    this.lastPageSizeAfterViewportRemeasure = {
  1.4476 +      width: viewport.pageRight - viewport.pageLeft,
  1.4477 +      height: viewport.pageBottom - viewport.pageTop
  1.4478 +    };
  1.4479 +  },
  1.4480 +
  1.4481 +  sendViewportMetadata: function sendViewportMetadata() {
  1.4482 +    let metadata = this.metadata;
  1.4483 +    sendMessageToJava({
  1.4484 +      type: "Tab:ViewportMetadata",
  1.4485 +      allowZoom: metadata.allowZoom,
  1.4486 +      allowDoubleTapZoom: metadata.allowDoubleTapZoom,
  1.4487 +      defaultZoom: metadata.defaultZoom || window.devicePixelRatio,
  1.4488 +      minZoom: metadata.minZoom || 0,
  1.4489 +      maxZoom: metadata.maxZoom || 0,
  1.4490 +      isRTL: metadata.isRTL,
  1.4491 +      tabID: this.id
  1.4492 +    });
  1.4493 +  },
  1.4494 +
  1.4495 +  setBrowserSize: function(aWidth, aHeight) {
  1.4496 +    if (fuzzyEquals(this.browserWidth, aWidth) && fuzzyEquals(this.browserHeight, aHeight)) {
  1.4497 +      return;
  1.4498 +    }
  1.4499 +
  1.4500 +    this.browserWidth = aWidth;
  1.4501 +    this.browserHeight = aHeight;
  1.4502 +
  1.4503 +    if (!this.browser.contentWindow)
  1.4504 +      return;
  1.4505 +    let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.4506 +    cwu.setCSSViewport(aWidth, aHeight);
  1.4507 +  },
  1.4508 +
  1.4509 +  /** Takes a scale and restricts it based on this tab's zoom limits. */
  1.4510 +  clampZoom: function clampZoom(aZoom) {
  1.4511 +    let zoom = ViewportHandler.clamp(aZoom, kViewportMinScale, kViewportMaxScale);
  1.4512 +
  1.4513 +    let md = this.metadata;
  1.4514 +    if (!md.allowZoom)
  1.4515 +      return md.defaultZoom || zoom;
  1.4516 +
  1.4517 +    if (md && md.minZoom)
  1.4518 +      zoom = Math.max(zoom, md.minZoom);
  1.4519 +    if (md && md.maxZoom)
  1.4520 +      zoom = Math.min(zoom, md.maxZoom);
  1.4521 +    return zoom;
  1.4522 +  },
  1.4523 +
  1.4524 +  observe: function(aSubject, aTopic, aData) {
  1.4525 +    switch (aTopic) {
  1.4526 +      case "before-first-paint":
  1.4527 +        // Is it on the top level?
  1.4528 +        let contentDocument = aSubject;
  1.4529 +        if (contentDocument == this.browser.contentDocument) {
  1.4530 +          if (BrowserApp.selectedTab == this) {
  1.4531 +            BrowserApp.contentDocumentChanged();
  1.4532 +          }
  1.4533 +          this.contentDocumentIsDisplayed = true;
  1.4534 +
  1.4535 +          // reset CSS viewport and zoom to default on new page, and then calculate
  1.4536 +          // them properly using the actual metadata from the page. note that the
  1.4537 +          // updateMetadata call takes into account the existing CSS viewport size
  1.4538 +          // and zoom when calculating the new ones, so we need to reset these
  1.4539 +          // things here before calling updateMetadata.
  1.4540 +          this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight);
  1.4541 +          let zoom = this.restoredSessionZoom() || gScreenWidth / this.browserWidth;
  1.4542 +          this.setResolution(zoom, true);
  1.4543 +          ViewportHandler.updateMetadata(this, true);
  1.4544 +
  1.4545 +          // Note that if we draw without a display-port, things can go wrong. By the
  1.4546 +          // time we execute this, it's almost certain a display-port has been set via
  1.4547 +          // the MozScrolledAreaChanged event. If that didn't happen, the updateMetadata
  1.4548 +          // call above does so at the end of the updateViewportSize function. As long
  1.4549 +          // as that is happening, we don't need to do it again here.
  1.4550 +
  1.4551 +          if (!this.restoredSessionZoom() && contentDocument.mozSyntheticDocument) {
  1.4552 +            // for images, scale to fit width. this needs to happen *after* the call
  1.4553 +            // to updateMetadata above, because that call sets the CSS viewport which
  1.4554 +            // will affect the page size (i.e. contentDocument.body.scroll*) that we
  1.4555 +            // use in this calculation. also we call sendViewportUpdate after changing
  1.4556 +            // the resolution so that the display port gets recalculated appropriately.
  1.4557 +            let fitZoom = Math.min(gScreenWidth / contentDocument.body.scrollWidth,
  1.4558 +                                   gScreenHeight / contentDocument.body.scrollHeight);
  1.4559 +            this.setResolution(fitZoom, false);
  1.4560 +            this.sendViewportUpdate();
  1.4561 +          }
  1.4562 +        }
  1.4563 +
  1.4564 +        // If the reflow-text-on-page-load pref is enabled, and reflow-on-zoom
  1.4565 +        // is enabled, and our defaultZoom level is set, then we need to get
  1.4566 +        // the default zoom and reflow the text according to the defaultZoom
  1.4567 +        // level.
  1.4568 +        let rzEnabled = BrowserEventHandler.mReflozPref;
  1.4569 +        let rzPl = Services.prefs.getBoolPref("browser.zoom.reflowZoom.reflowTextOnPageLoad");
  1.4570 +
  1.4571 +        if (rzEnabled && rzPl) {
  1.4572 +          // Retrieve the viewport width and adjust the max line box width
  1.4573 +          // accordingly.
  1.4574 +          let vp = BrowserApp.selectedTab.getViewport();
  1.4575 +          BrowserApp.selectedTab.performReflowOnZoom(vp);
  1.4576 +        }
  1.4577 +        break;
  1.4578 +      case "after-viewport-change":
  1.4579 +        if (BrowserApp.selectedTab._mReflozPositioned) {
  1.4580 +          BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
  1.4581 +        }
  1.4582 +        break;
  1.4583 +      case "nsPref:changed":
  1.4584 +        if (aData == "browser.ui.zoom.force-user-scalable")
  1.4585 +          ViewportHandler.updateMetadata(this, false);
  1.4586 +        break;
  1.4587 +    }
  1.4588 +  },
  1.4589 +
  1.4590 +  set readerEnabled(isReaderEnabled) {
  1.4591 +    this._readerEnabled = isReaderEnabled;
  1.4592 +    if (this.getActive())
  1.4593 +      Reader.updatePageAction(this);
  1.4594 +  },
  1.4595 +
  1.4596 +  get readerEnabled() {
  1.4597 +    return this._readerEnabled;
  1.4598 +  },
  1.4599 +
  1.4600 +  set readerActive(isReaderActive) {
  1.4601 +    this._readerActive = isReaderActive;
  1.4602 +    if (this.getActive())
  1.4603 +      Reader.updatePageAction(this);
  1.4604 +  },
  1.4605 +
  1.4606 +  get readerActive() {
  1.4607 +    return this._readerActive;
  1.4608 +  },
  1.4609 +
  1.4610 +  // nsIBrowserTab
  1.4611 +  get window() {
  1.4612 +    if (!this.browser)
  1.4613 +      return null;
  1.4614 +    return this.browser.contentWindow;
  1.4615 +  },
  1.4616 +
  1.4617 +  get scale() {
  1.4618 +    return this._zoom;
  1.4619 +  },
  1.4620 +
  1.4621 +  QueryInterface: XPCOMUtils.generateQI([
  1.4622 +    Ci.nsIWebProgressListener,
  1.4623 +    Ci.nsISHistoryListener,
  1.4624 +    Ci.nsIObserver,
  1.4625 +    Ci.nsISupportsWeakReference,
  1.4626 +    Ci.nsIBrowserTab
  1.4627 +  ])
  1.4628 +};
  1.4629 +
  1.4630 +var BrowserEventHandler = {
  1.4631 +  init: function init() {
  1.4632 +    Services.obs.addObserver(this, "Gesture:SingleTap", false);
  1.4633 +    Services.obs.addObserver(this, "Gesture:CancelTouch", false);
  1.4634 +    Services.obs.addObserver(this, "Gesture:DoubleTap", false);
  1.4635 +    Services.obs.addObserver(this, "Gesture:Scroll", false);
  1.4636 +    Services.obs.addObserver(this, "dom-touch-listener-added", false);
  1.4637 +
  1.4638 +    BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false);
  1.4639 +    BrowserApp.deck.addEventListener("touchstart", this, true);
  1.4640 +    BrowserApp.deck.addEventListener("click", InputWidgetHelper, true);
  1.4641 +    BrowserApp.deck.addEventListener("click", SelectHelper, true);
  1.4642 +
  1.4643 +    SpatialNavigation.init(BrowserApp.deck, null);
  1.4644 +
  1.4645 +    document.addEventListener("MozMagnifyGesture", this, true);
  1.4646 +
  1.4647 +    Services.prefs.addObserver("browser.zoom.reflowOnZoom", this, false);
  1.4648 +    this.updateReflozPref();
  1.4649 +  },
  1.4650 +
  1.4651 +  resetMaxLineBoxWidth: function() {
  1.4652 +    BrowserApp.selectedTab.probablyNeedRefloz = false;
  1.4653 +
  1.4654 +    if (gReflowPending) {
  1.4655 +      clearTimeout(gReflowPending);
  1.4656 +    }
  1.4657 +
  1.4658 +    let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
  1.4659 +    gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
  1.4660 +                                reflozTimeout, 0);
  1.4661 +  },
  1.4662 +
  1.4663 +  updateReflozPref: function() {
  1.4664 +     this.mReflozPref = Services.prefs.getBoolPref("browser.zoom.reflowOnZoom");
  1.4665 +  },
  1.4666 +
  1.4667 +  handleEvent: function(aEvent) {
  1.4668 +    switch (aEvent.type) {
  1.4669 +      case 'touchstart':
  1.4670 +        this._handleTouchStart(aEvent);
  1.4671 +        break;
  1.4672 +      case 'MozMagnifyGesture':
  1.4673 +        this.observe(this, aEvent.type,
  1.4674 +                     JSON.stringify({x: aEvent.screenX, y: aEvent.screenY,
  1.4675 +                                     zoomDelta: aEvent.delta}));
  1.4676 +        break;
  1.4677 +    }
  1.4678 +  },
  1.4679 +
  1.4680 +  _handleTouchStart: function(aEvent) {
  1.4681 +    if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.touches.length > 1 || aEvent.defaultPrevented)
  1.4682 +      return;
  1.4683 +
  1.4684 +    let closest = aEvent.target;
  1.4685 +
  1.4686 +    if (closest) {
  1.4687 +      // If we've pressed a scrollable element, let Java know that we may
  1.4688 +      // want to override the scroll behaviour (for document sub-frames)
  1.4689 +      this._scrollableElement = this._findScrollableElement(closest, true);
  1.4690 +      this._firstScrollEvent = true;
  1.4691 +
  1.4692 +      if (this._scrollableElement != null) {
  1.4693 +        // Discard if it's the top-level scrollable, we let Java handle this
  1.4694 +        // The top-level scrollable is the body in quirks mode and the html element
  1.4695 +        // in standards mode
  1.4696 +        let doc = BrowserApp.selectedBrowser.contentDocument;
  1.4697 +        let rootScrollable = (doc.compatMode === "BackCompat" ? doc.body : doc.documentElement);
  1.4698 +        if (this._scrollableElement != rootScrollable) {
  1.4699 +          sendMessageToJava({ type: "Panning:Override" });
  1.4700 +        }
  1.4701 +      }
  1.4702 +    }
  1.4703 +
  1.4704 +    if (!ElementTouchHelper.isElementClickable(closest, null, false))
  1.4705 +      closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX,
  1.4706 +                                                    aEvent.changedTouches[0].screenY);
  1.4707 +    if (!closest)
  1.4708 +      closest = aEvent.target;
  1.4709 +
  1.4710 +    if (closest) {
  1.4711 +      let uri = this._getLinkURI(closest);
  1.4712 +      if (uri) {
  1.4713 +        try {
  1.4714 +          Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
  1.4715 +        } catch (e) {}
  1.4716 +      }
  1.4717 +      this._doTapHighlight(closest);
  1.4718 +    }
  1.4719 +  },
  1.4720 +
  1.4721 +  _getLinkURI: function(aElement) {
  1.4722 +    if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
  1.4723 +        ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
  1.4724 +        (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) {
  1.4725 +      try {
  1.4726 +        return Services.io.newURI(aElement.href, null, null);
  1.4727 +      } catch (e) {}
  1.4728 +    }
  1.4729 +    return null;
  1.4730 +  },
  1.4731 +
  1.4732 +  observe: function(aSubject, aTopic, aData) {
  1.4733 +    if (aTopic == "dom-touch-listener-added") {
  1.4734 +      let tab = BrowserApp.getTabForWindow(aSubject.top);
  1.4735 +      if (!tab || tab.hasTouchListener)
  1.4736 +        return;
  1.4737 +
  1.4738 +      tab.hasTouchListener = true;
  1.4739 +      sendMessageToJava({
  1.4740 +        type: "Tab:HasTouchListener",
  1.4741 +        tabID: tab.id
  1.4742 +      });
  1.4743 +      return;
  1.4744 +    } else if (aTopic == "nsPref:changed") {
  1.4745 +      if (aData == "browser.zoom.reflowOnZoom") {
  1.4746 +        this.updateReflozPref();
  1.4747 +      }
  1.4748 +      return;
  1.4749 +    }
  1.4750 +
  1.4751 +    // the remaining events are all dependent on the browser content document being the
  1.4752 +    // same as the browser displayed document. if they are not the same, we should ignore
  1.4753 +    // the event.
  1.4754 +    if (BrowserApp.isBrowserContentDocumentDisplayed()) {
  1.4755 +      this.handleUserEvent(aTopic, aData);
  1.4756 +    }
  1.4757 +  },
  1.4758 +
  1.4759 +  handleUserEvent: function(aTopic, aData) {
  1.4760 +    switch (aTopic) {
  1.4761 +
  1.4762 +      case "Gesture:Scroll": {
  1.4763 +        // If we've lost our scrollable element, return. Don't cancel the
  1.4764 +        // override, as we probably don't want Java to handle panning until the
  1.4765 +        // user releases their finger.
  1.4766 +        if (this._scrollableElement == null)
  1.4767 +          return;
  1.4768 +
  1.4769 +        // If this is the first scroll event and we can't scroll in the direction
  1.4770 +        // the user wanted, and neither can any non-root sub-frame, cancel the
  1.4771 +        // override so that Java can handle panning the main document.
  1.4772 +        let data = JSON.parse(aData);
  1.4773 +
  1.4774 +        // round the scroll amounts because they come in as floats and might be
  1.4775 +        // subject to minor rounding errors because of zoom values. I've seen values
  1.4776 +        // like 0.99 come in here and get truncated to 0; this avoids that problem.
  1.4777 +        let zoom = BrowserApp.selectedTab._zoom;
  1.4778 +        let x = Math.round(data.x / zoom);
  1.4779 +        let y = Math.round(data.y / zoom);
  1.4780 +
  1.4781 +        if (this._firstScrollEvent) {
  1.4782 +          while (this._scrollableElement != null &&
  1.4783 +                 !this._elementCanScroll(this._scrollableElement, x, y))
  1.4784 +            this._scrollableElement = this._findScrollableElement(this._scrollableElement, false);
  1.4785 +
  1.4786 +          let doc = BrowserApp.selectedBrowser.contentDocument;
  1.4787 +          if (this._scrollableElement == null ||
  1.4788 +              this._scrollableElement == doc.documentElement) {
  1.4789 +            sendMessageToJava({ type: "Panning:CancelOverride" });
  1.4790 +            return;
  1.4791 +          }
  1.4792 +
  1.4793 +          this._firstScrollEvent = false;
  1.4794 +        }
  1.4795 +
  1.4796 +        // Scroll the scrollable element
  1.4797 +        if (this._elementCanScroll(this._scrollableElement, x, y)) {
  1.4798 +          this._scrollElementBy(this._scrollableElement, x, y);
  1.4799 +          sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: true });
  1.4800 +          SelectionHandler.subdocumentScrolled(this._scrollableElement);
  1.4801 +        } else {
  1.4802 +          sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: false });
  1.4803 +        }
  1.4804 +
  1.4805 +        break;
  1.4806 +      }
  1.4807 +
  1.4808 +      case "Gesture:CancelTouch":
  1.4809 +        this._cancelTapHighlight();
  1.4810 +        break;
  1.4811 +
  1.4812 +      case "Gesture:SingleTap": {
  1.4813 +        let element = this._highlightElement;
  1.4814 +        if (element) {
  1.4815 +          try {
  1.4816 +            let data = JSON.parse(aData);
  1.4817 +            let [x, y] = [data.x, data.y];
  1.4818 +            if (ElementTouchHelper.isElementClickable(element)) {
  1.4819 +              [x, y] = this._moveClickPoint(element, x, y);
  1.4820 +            }
  1.4821 +
  1.4822 +            // Was the element already focused before it was clicked?
  1.4823 +            let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser));
  1.4824 +
  1.4825 +            this._sendMouseEvent("mousemove", element, x, y);
  1.4826 +            this._sendMouseEvent("mousedown", element, x, y);
  1.4827 +            this._sendMouseEvent("mouseup",   element, x, y);
  1.4828 +
  1.4829 +            // If the element was previously focused, show the caret attached to it.
  1.4830 +            if (isFocused)
  1.4831 +              SelectionHandler.attachCaret(element);
  1.4832 +
  1.4833 +            // scrollToFocusedInput does its own checks to find out if an element should be zoomed into
  1.4834 +            BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser);
  1.4835 +          } catch(e) {
  1.4836 +            Cu.reportError(e);
  1.4837 +          }
  1.4838 +        }
  1.4839 +        this._cancelTapHighlight();
  1.4840 +        break;
  1.4841 +      }
  1.4842 +
  1.4843 +      case"Gesture:DoubleTap":
  1.4844 +        this._cancelTapHighlight();
  1.4845 +        this.onDoubleTap(aData);
  1.4846 +        break;
  1.4847 +
  1.4848 +      case "MozMagnifyGesture":
  1.4849 +        this.onPinchFinish(aData);
  1.4850 +        break;
  1.4851 +
  1.4852 +      default:
  1.4853 +        dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
  1.4854 +        break;
  1.4855 +    }
  1.4856 +  },
  1.4857 +
  1.4858 +  onDoubleTap: function(aData) {
  1.4859 +    let data = JSON.parse(aData);
  1.4860 +    let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y);
  1.4861 +
  1.4862 +    // We only want to do this if reflow-on-zoom is enabled, we don't already
  1.4863 +    // have a reflow-on-zoom event pending, and the element upon which the user
  1.4864 +    // double-tapped isn't of a type we want to avoid reflow-on-zoom.
  1.4865 +    if (BrowserEventHandler.mReflozPref &&
  1.4866 +       !BrowserApp.selectedTab._mReflozPoint &&
  1.4867 +       !this._shouldSuppressReflowOnZoom(element)) {
  1.4868 +
  1.4869 +      // See comment above performReflowOnZoom() for a detailed description of
  1.4870 +      // the events happening in the reflow-on-zoom operation.
  1.4871 +      let data = JSON.parse(aData);
  1.4872 +      let zoomPointX = data.x;
  1.4873 +      let zoomPointY = data.y;
  1.4874 +
  1.4875 +      BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY,
  1.4876 +        range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) };
  1.4877 +
  1.4878 +      // Before we perform a reflow on zoom, let's disable painting.
  1.4879 +      let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
  1.4880 +      let docShell = webNav.QueryInterface(Ci.nsIDocShell);
  1.4881 +      let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
  1.4882 +      docViewer.pausePainting();
  1.4883 +
  1.4884 +      BrowserApp.selectedTab.probablyNeedRefloz = true;
  1.4885 +    }
  1.4886 +
  1.4887 +    if (!element) {
  1.4888 +      ZoomHelper.zoomOut();
  1.4889 +      return;
  1.4890 +    }
  1.4891 +
  1.4892 +    while (element && !this._shouldZoomToElement(element))
  1.4893 +      element = element.parentNode;
  1.4894 +
  1.4895 +    if (!element) {
  1.4896 +      ZoomHelper.zoomOut();
  1.4897 +    } else {
  1.4898 +      ZoomHelper.zoomToElement(element, data.y);
  1.4899 +    }
  1.4900 +  },
  1.4901 +
  1.4902 +  /**
  1.4903 +   * Determine if reflow-on-zoom functionality should be suppressed, given a
  1.4904 +   * particular element. Double-tapping on the following elements suppresses
  1.4905 +   * reflow-on-zoom:
  1.4906 +   *
  1.4907 +   * <video>, <object>, <embed>, <applet>, <canvas>, <img>, <media>, <pre>
  1.4908 +   */
  1.4909 +  _shouldSuppressReflowOnZoom: function(aElement) {
  1.4910 +    if (aElement instanceof Ci.nsIDOMHTMLVideoElement ||
  1.4911 +        aElement instanceof Ci.nsIDOMHTMLObjectElement ||
  1.4912 +        aElement instanceof Ci.nsIDOMHTMLEmbedElement ||
  1.4913 +        aElement instanceof Ci.nsIDOMHTMLAppletElement ||
  1.4914 +        aElement instanceof Ci.nsIDOMHTMLCanvasElement ||
  1.4915 +        aElement instanceof Ci.nsIDOMHTMLImageElement ||
  1.4916 +        aElement instanceof Ci.nsIDOMHTMLMediaElement ||
  1.4917 +        aElement instanceof Ci.nsIDOMHTMLPreElement) {
  1.4918 +      return true;
  1.4919 +    }
  1.4920 +
  1.4921 +    return false;
  1.4922 +  },
  1.4923 +
  1.4924 +  onPinchFinish: function(aData) {
  1.4925 +    let data = {};
  1.4926 +    try {
  1.4927 +      data = JSON.parse(aData);
  1.4928 +    } catch(ex) {
  1.4929 +      console.log(ex);
  1.4930 +      return;
  1.4931 +    }
  1.4932 +
  1.4933 +    if (BrowserEventHandler.mReflozPref &&
  1.4934 +        data.zoomDelta < 0.0) {
  1.4935 +      BrowserEventHandler.resetMaxLineBoxWidth();
  1.4936 +    }
  1.4937 +  },
  1.4938 +
  1.4939 +  _shouldZoomToElement: function(aElement) {
  1.4940 +    let win = aElement.ownerDocument.defaultView;
  1.4941 +    if (win.getComputedStyle(aElement, null).display == "inline")
  1.4942 +      return false;
  1.4943 +    if (aElement instanceof Ci.nsIDOMHTMLLIElement)
  1.4944 +      return false;
  1.4945 +    if (aElement instanceof Ci.nsIDOMHTMLQuoteElement)
  1.4946 +      return false;
  1.4947 +    return true;
  1.4948 +  },
  1.4949 +
  1.4950 +  _firstScrollEvent: false,
  1.4951 +
  1.4952 +  _scrollableElement: null,
  1.4953 +
  1.4954 +  _highlightElement: null,
  1.4955 +
  1.4956 +  _doTapHighlight: function _doTapHighlight(aElement) {
  1.4957 +    DOMUtils.setContentState(aElement, kStateActive);
  1.4958 +    this._highlightElement = aElement;
  1.4959 +  },
  1.4960 +
  1.4961 +  _cancelTapHighlight: function _cancelTapHighlight() {
  1.4962 +    if (!this._highlightElement)
  1.4963 +      return;
  1.4964 +
  1.4965 +    // If the active element is in a sub-frame, we need to make that frame's document
  1.4966 +    // active to remove the element's active state.
  1.4967 +    if (this._highlightElement.ownerDocument != BrowserApp.selectedBrowser.contentWindow.document)
  1.4968 +      DOMUtils.setContentState(this._highlightElement.ownerDocument.documentElement, kStateActive);
  1.4969 +
  1.4970 +    DOMUtils.setContentState(BrowserApp.selectedBrowser.contentWindow.document.documentElement, kStateActive);
  1.4971 +    this._highlightElement = null;
  1.4972 +  },
  1.4973 +
  1.4974 +  _updateLastPosition: function(x, y, dx, dy) {
  1.4975 +    this.lastX = x;
  1.4976 +    this.lastY = y;
  1.4977 +    this.lastTime = Date.now();
  1.4978 +
  1.4979 +    this.motionBuffer.push({ dx: dx, dy: dy, time: this.lastTime });
  1.4980 +  },
  1.4981 +
  1.4982 +  _moveClickPoint: function(aElement, aX, aY) {
  1.4983 +    // the element can be out of the aX/aY point because of the touch radius
  1.4984 +    // if outside, we gracefully move the touch point to the edge of the element
  1.4985 +    if (!(aElement instanceof HTMLHtmlElement)) {
  1.4986 +      let isTouchClick = true;
  1.4987 +      let rects = ElementTouchHelper.getContentClientRects(aElement);
  1.4988 +      for (let i = 0; i < rects.length; i++) {
  1.4989 +        let rect = rects[i];
  1.4990 +        let inBounds =
  1.4991 +          (aX > rect.left && aX < (rect.left + rect.width)) &&
  1.4992 +          (aY > rect.top && aY < (rect.top + rect.height));
  1.4993 +        if (inBounds) {
  1.4994 +          isTouchClick = false;
  1.4995 +          break;
  1.4996 +        }
  1.4997 +      }
  1.4998 +
  1.4999 +      if (isTouchClick) {
  1.5000 +        let rect = rects[0];
  1.5001 +        // if either width or height is zero, we don't want to move the click to the edge of the element. See bug 757208
  1.5002 +        if (rect.width != 0 && rect.height != 0) {
  1.5003 +          aX = Math.min(Math.ceil(rect.left + rect.width) - 1, Math.max(Math.ceil(rect.left), aX));
  1.5004 +          aY = Math.min(Math.ceil(rect.top + rect.height) - 1, Math.max(Math.ceil(rect.top),  aY));
  1.5005 +        }
  1.5006 +      }
  1.5007 +    }
  1.5008 +    return [aX, aY];
  1.5009 +  },
  1.5010 +
  1.5011 +  _sendMouseEvent: function _sendMouseEvent(aName, aElement, aX, aY) {
  1.5012 +    let window = aElement.ownerDocument.defaultView;
  1.5013 +    try {
  1.5014 +      let cwu = window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.5015 +      cwu.sendMouseEventToWindow(aName, aX, aY, 0, 1, 0, true);
  1.5016 +    } catch(e) {
  1.5017 +      Cu.reportError(e);
  1.5018 +    }
  1.5019 +  },
  1.5020 +
  1.5021 +  _hasScrollableOverflow: function(elem) {
  1.5022 +    var win = elem.ownerDocument.defaultView;
  1.5023 +    if (!win)
  1.5024 +      return false;
  1.5025 +    var computedStyle = win.getComputedStyle(elem);
  1.5026 +    if (!computedStyle)
  1.5027 +      return false;
  1.5028 +    // We check for overflow:hidden only because all the other cases are scrollable
  1.5029 +    // under various conditions. See https://bugzilla.mozilla.org/show_bug.cgi?id=911574#c24
  1.5030 +    // for some more details.
  1.5031 +    return !(computedStyle.overflowX == 'hidden' && computedStyle.overflowY == 'hidden');
  1.5032 +  },
  1.5033 +
  1.5034 +  _findScrollableElement: function(elem, checkElem) {
  1.5035 +    // Walk the DOM tree until we find a scrollable element
  1.5036 +    let scrollable = false;
  1.5037 +    while (elem) {
  1.5038 +      /* Element is scrollable if its scroll-size exceeds its client size, and:
  1.5039 +       * - It has overflow other than 'hidden', or
  1.5040 +       * - It's a textarea node, or
  1.5041 +       * - It's a text input, or
  1.5042 +       * - It's a select element showing multiple rows
  1.5043 +       */
  1.5044 +      if (checkElem) {
  1.5045 +        if ((elem.scrollTopMax > 0 || elem.scrollLeftMax > 0) &&
  1.5046 +            (this._hasScrollableOverflow(elem) ||
  1.5047 +             elem.mozMatchesSelector("textarea")) ||
  1.5048 +            (elem instanceof HTMLInputElement && elem.mozIsTextField(false)) ||
  1.5049 +            (elem instanceof HTMLSelectElement && (elem.size > 1 || elem.multiple))) {
  1.5050 +          scrollable = true;
  1.5051 +          break;
  1.5052 +        }
  1.5053 +      } else {
  1.5054 +        checkElem = true;
  1.5055 +      }
  1.5056 +
  1.5057 +      // Propagate up iFrames
  1.5058 +      if (!elem.parentNode && elem.documentElement && elem.documentElement.ownerDocument)
  1.5059 +        elem = elem.documentElement.ownerDocument.defaultView.frameElement;
  1.5060 +      else
  1.5061 +        elem = elem.parentNode;
  1.5062 +    }
  1.5063 +
  1.5064 +    if (!scrollable)
  1.5065 +      return null;
  1.5066 +
  1.5067 +    return elem;
  1.5068 +  },
  1.5069 +
  1.5070 +  _scrollElementBy: function(elem, x, y) {
  1.5071 +    elem.scrollTop = elem.scrollTop + y;
  1.5072 +    elem.scrollLeft = elem.scrollLeft + x;
  1.5073 +  },
  1.5074 +
  1.5075 +  _elementCanScroll: function(elem, x, y) {
  1.5076 +    let scrollX = (x < 0 && elem.scrollLeft > 0)
  1.5077 +               || (x > 0 && elem.scrollLeft < elem.scrollLeftMax);
  1.5078 +
  1.5079 +    let scrollY = (y < 0 && elem.scrollTop > 0)
  1.5080 +               || (y > 0 && elem.scrollTop < elem.scrollTopMax);
  1.5081 +
  1.5082 +    return scrollX || scrollY;
  1.5083 +  }
  1.5084 +};
  1.5085 +
  1.5086 +const kReferenceDpi = 240; // standard "pixel" size used in some preferences
  1.5087 +
  1.5088 +const ElementTouchHelper = {
  1.5089 +  /* Return the element at the given coordinates, starting from the given window and
  1.5090 +     drilling down through frames. If no window is provided, the top-level window of
  1.5091 +     the currently selected tab is used. The coordinates provided should be CSS pixels
  1.5092 +     relative to the window's scroll position. */
  1.5093 +  anyElementFromPoint: function(aX, aY, aWindow) {
  1.5094 +    let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow);
  1.5095 +    let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.5096 +    let elem = cwu.elementFromPoint(aX, aY, true, true);
  1.5097 +
  1.5098 +    while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
  1.5099 +      let rect = elem.getBoundingClientRect();
  1.5100 +      aX -= rect.left;
  1.5101 +      aY -= rect.top;
  1.5102 +      cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.5103 +      elem = cwu.elementFromPoint(aX, aY, true, true);
  1.5104 +    }
  1.5105 +
  1.5106 +    return elem;
  1.5107 +  },
  1.5108 +
  1.5109 +  /* Return the most appropriate clickable element (if any), starting from the given window
  1.5110 +     and drilling down through iframes as necessary. If no window is provided, the top-level
  1.5111 +     window of the currently selected tab is used. The coordinates provided should be CSS
  1.5112 +     pixels relative to the window's scroll position. The element returned may not actually
  1.5113 +     contain the coordinates passed in because of touch radius and clickability heuristics. */
  1.5114 +  elementFromPoint: function(aX, aY, aWindow) {
  1.5115 +    // browser's elementFromPoint expect browser-relative client coordinates.
  1.5116 +    // subtract browser's scroll values to adjust
  1.5117 +    let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow);
  1.5118 +    let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.5119 +    let elem = this.getClosest(cwu, aX, aY);
  1.5120 +
  1.5121 +    // step through layers of IFRAMEs and FRAMES to find innermost element
  1.5122 +    while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
  1.5123 +      // adjust client coordinates' origin to be top left of iframe viewport
  1.5124 +      let rect = elem.getBoundingClientRect();
  1.5125 +      aX -= rect.left;
  1.5126 +      aY -= rect.top;
  1.5127 +      cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.5128 +      elem = this.getClosest(cwu, aX, aY);
  1.5129 +    }
  1.5130 +
  1.5131 +    return elem;
  1.5132 +  },
  1.5133 +
  1.5134 +  /* Returns the touch radius in content px. */
  1.5135 +  getTouchRadius: function getTouchRadius() {
  1.5136 +    let dpiRatio = ViewportHandler.displayDPI / kReferenceDpi;
  1.5137 +    let zoom = BrowserApp.selectedTab._zoom;
  1.5138 +    return {
  1.5139 +      top: this.radius.top * dpiRatio / zoom,
  1.5140 +      right: this.radius.right * dpiRatio / zoom,
  1.5141 +      bottom: this.radius.bottom * dpiRatio / zoom,
  1.5142 +      left: this.radius.left * dpiRatio / zoom
  1.5143 +    };
  1.5144 +  },
  1.5145 +
  1.5146 +  /* Returns the touch radius in reference pixels. */
  1.5147 +  get radius() {
  1.5148 +    let prefs = Services.prefs;
  1.5149 +    delete this.radius;
  1.5150 +    return this.radius = { "top": prefs.getIntPref("browser.ui.touch.top"),
  1.5151 +                           "right": prefs.getIntPref("browser.ui.touch.right"),
  1.5152 +                           "bottom": prefs.getIntPref("browser.ui.touch.bottom"),
  1.5153 +                           "left": prefs.getIntPref("browser.ui.touch.left")
  1.5154 +                         };
  1.5155 +  },
  1.5156 +
  1.5157 +  get weight() {
  1.5158 +    delete this.weight;
  1.5159 +    return this.weight = { "visited": Services.prefs.getIntPref("browser.ui.touch.weight.visited") };
  1.5160 +  },
  1.5161 +
  1.5162 +  /* Retrieve the closest element to a point by looking at borders position */
  1.5163 +  getClosest: function getClosest(aWindowUtils, aX, aY) {
  1.5164 +    let target = aWindowUtils.elementFromPoint(aX, aY,
  1.5165 +                                               true,   /* ignore root scroll frame*/
  1.5166 +                                               false); /* don't flush layout */
  1.5167 +
  1.5168 +    // if this element is clickable we return quickly. also, if it isn't,
  1.5169 +    // use a cache to speed up future calls to isElementClickable in the
  1.5170 +    // loop below.
  1.5171 +    let unclickableCache = new Array();
  1.5172 +    if (this.isElementClickable(target, unclickableCache, false))
  1.5173 +      return target;
  1.5174 +
  1.5175 +    target = null;
  1.5176 +    let radius = this.getTouchRadius();
  1.5177 +    let nodes = aWindowUtils.nodesFromRect(aX, aY, radius.top, radius.right, radius.bottom, radius.left, true, false);
  1.5178 +
  1.5179 +    let threshold = Number.POSITIVE_INFINITY;
  1.5180 +    for (let i = 0; i < nodes.length; i++) {
  1.5181 +      let current = nodes[i];
  1.5182 +      if (!current.mozMatchesSelector || !this.isElementClickable(current, unclickableCache, true))
  1.5183 +        continue;
  1.5184 +
  1.5185 +      let rect = current.getBoundingClientRect();
  1.5186 +      let distance = this._computeDistanceFromRect(aX, aY, rect);
  1.5187 +
  1.5188 +      // increase a little bit the weight for already visited items
  1.5189 +      if (current && current.mozMatchesSelector("*:visited"))
  1.5190 +        distance *= (this.weight.visited / 100);
  1.5191 +
  1.5192 +      if (distance < threshold) {
  1.5193 +        target = current;
  1.5194 +        threshold = distance;
  1.5195 +      }
  1.5196 +    }
  1.5197 +
  1.5198 +    return target;
  1.5199 +  },
  1.5200 +
  1.5201 +  isElementClickable: function isElementClickable(aElement, aUnclickableCache, aAllowBodyListeners) {
  1.5202 +    const selector = "a,:link,:visited,[role=button],button,input,select,textarea";
  1.5203 +
  1.5204 +    let stopNode = null;
  1.5205 +    if (!aAllowBodyListeners && aElement && aElement.ownerDocument)
  1.5206 +      stopNode = aElement.ownerDocument.body;
  1.5207 +
  1.5208 +    for (let elem = aElement; elem && elem != stopNode; elem = elem.parentNode) {
  1.5209 +      if (aUnclickableCache && aUnclickableCache.indexOf(elem) != -1)
  1.5210 +        continue;
  1.5211 +      if (this._hasMouseListener(elem))
  1.5212 +        return true;
  1.5213 +      if (elem.mozMatchesSelector && elem.mozMatchesSelector(selector))
  1.5214 +        return true;
  1.5215 +      if (elem instanceof HTMLLabelElement && elem.control != null)
  1.5216 +        return true;
  1.5217 +      if (aUnclickableCache)
  1.5218 +        aUnclickableCache.push(elem);
  1.5219 +    }
  1.5220 +    return false;
  1.5221 +  },
  1.5222 +
  1.5223 +  _computeDistanceFromRect: function _computeDistanceFromRect(aX, aY, aRect) {
  1.5224 +    let x = 0, y = 0;
  1.5225 +    let xmost = aRect.left + aRect.width;
  1.5226 +    let ymost = aRect.top + aRect.height;
  1.5227 +
  1.5228 +    // compute horizontal distance from left/right border depending if X is
  1.5229 +    // before/inside/after the element's rectangle
  1.5230 +    if (aRect.left < aX && aX < xmost)
  1.5231 +      x = Math.min(xmost - aX, aX - aRect.left);
  1.5232 +    else if (aX < aRect.left)
  1.5233 +      x = aRect.left - aX;
  1.5234 +    else if (aX > xmost)
  1.5235 +      x = aX - xmost;
  1.5236 +
  1.5237 +    // compute vertical distance from top/bottom border depending if Y is
  1.5238 +    // above/inside/below the element's rectangle
  1.5239 +    if (aRect.top < aY && aY < ymost)
  1.5240 +      y = Math.min(ymost - aY, aY - aRect.top);
  1.5241 +    else if (aY < aRect.top)
  1.5242 +      y = aRect.top - aY;
  1.5243 +    if (aY > ymost)
  1.5244 +      y = aY - ymost;
  1.5245 +
  1.5246 +    return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
  1.5247 +  },
  1.5248 +
  1.5249 +  _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),
  1.5250 +  _clickableEvents: ["mousedown", "mouseup", "click"],
  1.5251 +  _hasMouseListener: function _hasMouseListener(aElement) {
  1.5252 +    let els = this._els;
  1.5253 +    let listeners = els.getListenerInfoFor(aElement, {});
  1.5254 +    for (let i = 0; i < listeners.length; i++) {
  1.5255 +      if (this._clickableEvents.indexOf(listeners[i].type) != -1)
  1.5256 +        return true;
  1.5257 +    }
  1.5258 +    return false;
  1.5259 +  },
  1.5260 +
  1.5261 +  getContentClientRects: function(aElement) {
  1.5262 +    let offset = { x: 0, y: 0 };
  1.5263 +
  1.5264 +    let nativeRects = aElement.getClientRects();
  1.5265 +    // step out of iframes and frames, offsetting scroll values
  1.5266 +    for (let frame = aElement.ownerDocument.defaultView; frame.frameElement; frame = frame.parent) {
  1.5267 +      // adjust client coordinates' origin to be top left of iframe viewport
  1.5268 +      let rect = frame.frameElement.getBoundingClientRect();
  1.5269 +      let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
  1.5270 +      let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
  1.5271 +      offset.x += rect.left + parseInt(left);
  1.5272 +      offset.y += rect.top + parseInt(top);
  1.5273 +    }
  1.5274 +
  1.5275 +    let result = [];
  1.5276 +    for (let i = nativeRects.length - 1; i >= 0; i--) {
  1.5277 +      let r = nativeRects[i];
  1.5278 +      result.push({ left: r.left + offset.x,
  1.5279 +                    top: r.top + offset.y,
  1.5280 +                    width: r.width,
  1.5281 +                    height: r.height
  1.5282 +                  });
  1.5283 +    }
  1.5284 +    return result;
  1.5285 +  },
  1.5286 +
  1.5287 +  getBoundingContentRect: function(aElement) {
  1.5288 +    if (!aElement)
  1.5289 +      return {x: 0, y: 0, w: 0, h: 0};
  1.5290 +
  1.5291 +    let document = aElement.ownerDocument;
  1.5292 +    while (document.defaultView.frameElement)
  1.5293 +      document = document.defaultView.frameElement.ownerDocument;
  1.5294 +
  1.5295 +    let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.5296 +    let scrollX = {}, scrollY = {};
  1.5297 +    cwu.getScrollXY(false, scrollX, scrollY);
  1.5298 +
  1.5299 +    let r = aElement.getBoundingClientRect();
  1.5300 +
  1.5301 +    // step out of iframes and frames, offsetting scroll values
  1.5302 +    for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) {
  1.5303 +      // adjust client coordinates' origin to be top left of iframe viewport
  1.5304 +      let rect = frame.frameElement.getBoundingClientRect();
  1.5305 +      let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
  1.5306 +      let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
  1.5307 +      scrollX.value += rect.left + parseInt(left);
  1.5308 +      scrollY.value += rect.top + parseInt(top);
  1.5309 +    }
  1.5310 +
  1.5311 +    return {x: r.left + scrollX.value,
  1.5312 +            y: r.top + scrollY.value,
  1.5313 +            w: r.width,
  1.5314 +            h: r.height };
  1.5315 +  }
  1.5316 +};
  1.5317 +
  1.5318 +var ErrorPageEventHandler = {
  1.5319 +  handleEvent: function(aEvent) {
  1.5320 +    switch (aEvent.type) {
  1.5321 +      case "click": {
  1.5322 +        // Don't trust synthetic events
  1.5323 +        if (!aEvent.isTrusted)
  1.5324 +          return;
  1.5325 +
  1.5326 +        let target = aEvent.originalTarget;
  1.5327 +        let errorDoc = target.ownerDocument;
  1.5328 +
  1.5329 +        // If the event came from an ssl error page, it is probably either the "Add
  1.5330 +        // Exception…" or "Get me out of here!" button
  1.5331 +        if (errorDoc.documentURI.startsWith("about:certerror?e=nssBadCert")) {
  1.5332 +          let perm = errorDoc.getElementById("permanentExceptionButton");
  1.5333 +          let temp = errorDoc.getElementById("temporaryExceptionButton");
  1.5334 +          if (target == temp || target == perm) {
  1.5335 +            // Handle setting an cert exception and reloading the page
  1.5336 +            try {
  1.5337 +              // Add a new SSL exception for this URL
  1.5338 +              let uri = Services.io.newURI(errorDoc.location.href, null, null);
  1.5339 +              let sslExceptions = new SSLExceptions();
  1.5340 +
  1.5341 +              if (target == perm)
  1.5342 +                sslExceptions.addPermanentException(uri, errorDoc.defaultView);
  1.5343 +              else
  1.5344 +                sslExceptions.addTemporaryException(uri, errorDoc.defaultView);
  1.5345 +            } catch (e) {
  1.5346 +              dump("Failed to set cert exception: " + e + "\n");
  1.5347 +            }
  1.5348 +            errorDoc.location.reload();
  1.5349 +          } else if (target == errorDoc.getElementById("getMeOutOfHereButton")) {
  1.5350 +            errorDoc.location = "about:home";
  1.5351 +          }
  1.5352 +        } else if (errorDoc.documentURI.startsWith("about:blocked")) {
  1.5353 +          // The event came from a button on a malware/phishing block page
  1.5354 +          // First check whether it's malware or phishing, so that we can
  1.5355 +          // use the right strings/links
  1.5356 +          let isMalware = errorDoc.documentURI.contains("e=malwareBlocked");
  1.5357 +          let bucketName = isMalware ? "WARNING_MALWARE_PAGE_" : "WARNING_PHISHING_PAGE_";
  1.5358 +          let nsISecTel = Ci.nsISecurityUITelemetry;
  1.5359 +          let isIframe = (errorDoc.defaultView.parent === errorDoc.defaultView);
  1.5360 +          bucketName += isIframe ? "TOP_" : "FRAME_";
  1.5361 +
  1.5362 +          let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
  1.5363 +
  1.5364 +          if (target == errorDoc.getElementById("getMeOutButton")) {
  1.5365 +            Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]);
  1.5366 +            errorDoc.location = "about:home";
  1.5367 +          } else if (target == errorDoc.getElementById("reportButton")) {
  1.5368 +            // We log even if malware/phishing info URL couldn't be found:
  1.5369 +            // the measurement is for how many users clicked the WHY BLOCKED button
  1.5370 +            Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "WHY_BLOCKED"]);
  1.5371 +
  1.5372 +            // This is the "Why is this site blocked" button.  For malware,
  1.5373 +            // we can fetch a site-specific report, for phishing, we redirect
  1.5374 +            // to the generic page describing phishing protection.
  1.5375 +            if (isMalware) {
  1.5376 +              // Get the stop badware "why is this blocked" report url, append the current url, and go there.
  1.5377 +              try {
  1.5378 +                let reportURL = formatter.formatURLPref("browser.safebrowsing.malware.reportURL");
  1.5379 +                reportURL += errorDoc.location.href;
  1.5380 +                BrowserApp.selectedBrowser.loadURI(reportURL);
  1.5381 +              } catch (e) {
  1.5382 +                Cu.reportError("Couldn't get malware report URL: " + e);
  1.5383 +              }
  1.5384 +            } else {
  1.5385 +              // It's a phishing site, just link to the generic information page
  1.5386 +              let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
  1.5387 +              BrowserApp.selectedBrowser.loadURI(url + "phishing-malware");
  1.5388 +            }
  1.5389 +          } else if (target == errorDoc.getElementById("ignoreWarningButton")) {
  1.5390 +            Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "IGNORE_WARNING"]);
  1.5391 +
  1.5392 +            // Allow users to override and continue through to the site,
  1.5393 +            let webNav = BrowserApp.selectedBrowser.docShell.QueryInterface(Ci.nsIWebNavigation);
  1.5394 +            let location = BrowserApp.selectedBrowser.contentWindow.location;
  1.5395 +            webNav.loadURI(location, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, null, null, null);
  1.5396 +
  1.5397 +            // ....but add a notify bar as a reminder, so that they don't lose
  1.5398 +            // track after, e.g., tab switching.
  1.5399 +            NativeWindow.doorhanger.show(Strings.browser.GetStringFromName("safeBrowsingDoorhanger"), "safebrowsing-warning", [], BrowserApp.selectedTab.id);
  1.5400 +          }
  1.5401 +        }
  1.5402 +        break;
  1.5403 +      }
  1.5404 +    }
  1.5405 +  }
  1.5406 +};
  1.5407 +
  1.5408 +var FormAssistant = {
  1.5409 +  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
  1.5410 +
  1.5411 +  // Used to keep track of the element that corresponds to the current
  1.5412 +  // autocomplete suggestions
  1.5413 +  _currentInputElement: null,
  1.5414 +
  1.5415 +  _isBlocklisted: false,
  1.5416 +
  1.5417 +  // Keep track of whether or not an invalid form has been submitted
  1.5418 +  _invalidSubmit: false,
  1.5419 +
  1.5420 +  init: function() {
  1.5421 +    Services.obs.addObserver(this, "FormAssist:AutoComplete", false);
  1.5422 +    Services.obs.addObserver(this, "FormAssist:Blocklisted", false);
  1.5423 +    Services.obs.addObserver(this, "FormAssist:Hidden", false);
  1.5424 +    Services.obs.addObserver(this, "invalidformsubmit", false);
  1.5425 +    Services.obs.addObserver(this, "PanZoom:StateChange", false);
  1.5426 +
  1.5427 +    // We need to use a capturing listener for focus events
  1.5428 +    BrowserApp.deck.addEventListener("focus", this, true);
  1.5429 +    BrowserApp.deck.addEventListener("click", this, true);
  1.5430 +    BrowserApp.deck.addEventListener("input", this, false);
  1.5431 +    BrowserApp.deck.addEventListener("pageshow", this, false);
  1.5432 +  },
  1.5433 +
  1.5434 +  uninit: function() {
  1.5435 +    Services.obs.removeObserver(this, "FormAssist:AutoComplete");
  1.5436 +    Services.obs.removeObserver(this, "FormAssist:Blocklisted");
  1.5437 +    Services.obs.removeObserver(this, "FormAssist:Hidden");
  1.5438 +    Services.obs.removeObserver(this, "invalidformsubmit");
  1.5439 +    Services.obs.removeObserver(this, "PanZoom:StateChange");
  1.5440 +
  1.5441 +    BrowserApp.deck.removeEventListener("focus", this);
  1.5442 +    BrowserApp.deck.removeEventListener("click", this);
  1.5443 +    BrowserApp.deck.removeEventListener("input", this);
  1.5444 +    BrowserApp.deck.removeEventListener("pageshow", this);
  1.5445 +  },
  1.5446 +
  1.5447 +  observe: function(aSubject, aTopic, aData) {
  1.5448 +    switch (aTopic) {
  1.5449 +      case "PanZoom:StateChange":
  1.5450 +        // If the user is just touching the screen and we haven't entered a pan or zoom state yet do nothing
  1.5451 +        if (aData == "TOUCHING" || aData == "WAITING_LISTENERS")
  1.5452 +          break;
  1.5453 +        if (aData == "NOTHING") {
  1.5454 +          // only look for input elements, not contentEditable or multiline text areas
  1.5455 +          let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true);
  1.5456 +          if (!focused)
  1.5457 +            break;
  1.5458 +
  1.5459 +          if (this._showValidationMessage(focused))
  1.5460 +            break;
  1.5461 +          this._showAutoCompleteSuggestions(focused, function () {});
  1.5462 +        } else {
  1.5463 +          // temporarily hide the form assist popup while we're panning or zooming the page
  1.5464 +          this._hideFormAssistPopup();
  1.5465 +        }
  1.5466 +        break;
  1.5467 +      case "FormAssist:AutoComplete":
  1.5468 +        if (!this._currentInputElement)
  1.5469 +          break;
  1.5470 +
  1.5471 +        let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);
  1.5472 +
  1.5473 +        // If we have an active composition string, commit it before sending
  1.5474 +        // the autocomplete event with the text that will replace it.
  1.5475 +        try {
  1.5476 +          let imeEditor = editableElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
  1.5477 +          if (imeEditor.composing)
  1.5478 +            imeEditor.forceCompositionEnd();
  1.5479 +        } catch (e) {}
  1.5480 +
  1.5481 +        editableElement.setUserInput(aData);
  1.5482 +
  1.5483 +        let event = this._currentInputElement.ownerDocument.createEvent("Events");
  1.5484 +        event.initEvent("DOMAutoComplete", true, true);
  1.5485 +        this._currentInputElement.dispatchEvent(event);
  1.5486 +        break;
  1.5487 +
  1.5488 +      case "FormAssist:Blocklisted":
  1.5489 +        this._isBlocklisted = (aData == "true");
  1.5490 +        break;
  1.5491 +
  1.5492 +      case "FormAssist:Hidden":
  1.5493 +        this._currentInputElement = null;
  1.5494 +        break;
  1.5495 +    }
  1.5496 +  },
  1.5497 +
  1.5498 +  notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
  1.5499 +    if (!aInvalidElements.length)
  1.5500 +      return;
  1.5501 +
  1.5502 +    // Ignore this notificaiton if the current tab doesn't contain the invalid form
  1.5503 +    if (BrowserApp.selectedBrowser.contentDocument !=
  1.5504 +        aFormElement.ownerDocument.defaultView.top.document)
  1.5505 +      return;
  1.5506 +
  1.5507 +    this._invalidSubmit = true;
  1.5508 +
  1.5509 +    // Our focus listener will show the element's validation message
  1.5510 +    let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
  1.5511 +    currentElement.focus();
  1.5512 +  },
  1.5513 +
  1.5514 +  handleEvent: function(aEvent) {
  1.5515 +    switch (aEvent.type) {
  1.5516 +      case "focus":
  1.5517 +        let currentElement = aEvent.target;
  1.5518 +
  1.5519 +        // Only show a validation message on focus.
  1.5520 +        this._showValidationMessage(currentElement);
  1.5521 +        break;
  1.5522 +
  1.5523 +      case "click":
  1.5524 +        currentElement = aEvent.target;
  1.5525 +
  1.5526 +        // Prioritize a form validation message over autocomplete suggestions
  1.5527 +        // when the element is first focused (a form validation message will
  1.5528 +        // only be available if an invalid form was submitted)
  1.5529 +        if (this._showValidationMessage(currentElement))
  1.5530 +          break;
  1.5531 +
  1.5532 +        let checkResultsClick = hasResults => {
  1.5533 +          if (!hasResults) {
  1.5534 +            this._hideFormAssistPopup();
  1.5535 +          }
  1.5536 +        };
  1.5537 +
  1.5538 +        this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
  1.5539 +        break;
  1.5540 +
  1.5541 +      case "input":
  1.5542 +        currentElement = aEvent.target;
  1.5543 +
  1.5544 +        // Since we can only show one popup at a time, prioritze autocomplete
  1.5545 +        // suggestions over a form validation message
  1.5546 +        let checkResultsInput = hasResults => {
  1.5547 +          if (hasResults)
  1.5548 +            return;
  1.5549 +
  1.5550 +          if (this._showValidationMessage(currentElement))
  1.5551 +            return;
  1.5552 +
  1.5553 +          // If we're not showing autocomplete suggestions, hide the form assist popup
  1.5554 +          this._hideFormAssistPopup();
  1.5555 +        };
  1.5556 +
  1.5557 +        this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
  1.5558 +        break;
  1.5559 +
  1.5560 +      // Reset invalid submit state on each pageshow
  1.5561 +      case "pageshow":
  1.5562 +        if (!this._invalidSubmit)
  1.5563 +          return;
  1.5564 +
  1.5565 +        let selectedBrowser = BrowserApp.selectedBrowser;
  1.5566 +        if (selectedBrowser) {
  1.5567 +          let selectedDocument = selectedBrowser.contentDocument;
  1.5568 +          let target = aEvent.originalTarget;
  1.5569 +          if (target == selectedDocument || target.ownerDocument == selectedDocument)
  1.5570 +            this._invalidSubmit = false;
  1.5571 +        }
  1.5572 +    }
  1.5573 +  },
  1.5574 +
  1.5575 +  // We only want to show autocomplete suggestions for certain elements
  1.5576 +  _isAutoComplete: function _isAutoComplete(aElement) {
  1.5577 +    if (!(aElement instanceof HTMLInputElement) || aElement.readOnly ||
  1.5578 +        (aElement.getAttribute("type") == "password") ||
  1.5579 +        (aElement.hasAttribute("autocomplete") &&
  1.5580 +         aElement.getAttribute("autocomplete").toLowerCase() == "off"))
  1.5581 +      return false;
  1.5582 +
  1.5583 +    return true;
  1.5584 +  },
  1.5585 +
  1.5586 +  // Retrieves autocomplete suggestions for an element from the form autocomplete service.
  1.5587 +  // aCallback(array_of_suggestions) is called when results are available.
  1.5588 +  _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) {
  1.5589 +    // Cache the form autocomplete service for future use
  1.5590 +    if (!this._formAutoCompleteService)
  1.5591 +      this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"].
  1.5592 +                                      getService(Ci.nsIFormAutoComplete);
  1.5593 +
  1.5594 +    let resultsAvailable = function (results) {
  1.5595 +      let suggestions = [];
  1.5596 +      for (let i = 0; i < results.matchCount; i++) {
  1.5597 +        let value = results.getValueAt(i);
  1.5598 +
  1.5599 +        // Do not show the value if it is the current one in the input field
  1.5600 +        if (value == aSearchString)
  1.5601 +          continue;
  1.5602 +
  1.5603 +        // Supply a label and value, since they can differ for datalist suggestions
  1.5604 +        suggestions.push({ label: value, value: value });
  1.5605 +      }
  1.5606 +      aCallback(suggestions);
  1.5607 +    };
  1.5608 +
  1.5609 +    this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
  1.5610 +                                                          aSearchString, aElement, null,
  1.5611 +                                                          resultsAvailable);
  1.5612 +  },
  1.5613 +
  1.5614 +  /**
  1.5615 +   * (Copied from mobile/xul/chrome/content/forms.js)
  1.5616 +   * This function is similar to getListSuggestions from
  1.5617 +   * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
  1.5618 +   * used by the autocomplete.xml binding which is not in used in fennec
  1.5619 +   */
  1.5620 +  _getListSuggestions: function _getListSuggestions(aElement) {
  1.5621 +    if (!(aElement instanceof HTMLInputElement) || !aElement.list)
  1.5622 +      return [];
  1.5623 +
  1.5624 +    let suggestions = [];
  1.5625 +    let filter = !aElement.hasAttribute("mozNoFilter");
  1.5626 +    let lowerFieldValue = aElement.value.toLowerCase();
  1.5627 +
  1.5628 +    let options = aElement.list.options;
  1.5629 +    let length = options.length;
  1.5630 +    for (let i = 0; i < length; i++) {
  1.5631 +      let item = options.item(i);
  1.5632 +
  1.5633 +      let label = item.value;
  1.5634 +      if (item.label)
  1.5635 +        label = item.label;
  1.5636 +      else if (item.text)
  1.5637 +        label = item.text;
  1.5638 +
  1.5639 +      if (filter && !(label.toLowerCase().contains(lowerFieldValue)) )
  1.5640 +        continue;
  1.5641 +      suggestions.push({ label: label, value: item.value });
  1.5642 +    }
  1.5643 +
  1.5644 +    return suggestions;
  1.5645 +  },
  1.5646 +
  1.5647 +  // Retrieves autocomplete suggestions for an element from the form autocomplete service
  1.5648 +  // and sends the suggestions to the Java UI, along with element position data. As
  1.5649 +  // autocomplete queries are asynchronous, calls aCallback when done with a true
  1.5650 +  // argument if results were found and false if no results were found.
  1.5651 +  _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
  1.5652 +    if (!this._isAutoComplete(aElement)) {
  1.5653 +      aCallback(false);
  1.5654 +      return;
  1.5655 +    }
  1.5656 +
  1.5657 +    // Don't display the form auto-complete popup after the user starts typing
  1.5658 +    // to avoid confusing somes IME. See bug 758820 and bug 632744.
  1.5659 +    if (this._isBlocklisted && aElement.value.length > 0) {
  1.5660 +      aCallback(false);
  1.5661 +      return;
  1.5662 +    }
  1.5663 +
  1.5664 +    let resultsAvailable = autoCompleteSuggestions => {
  1.5665 +      // On desktop, we show datalist suggestions below autocomplete suggestions,
  1.5666 +      // without duplicates removed.
  1.5667 +      let listSuggestions = this._getListSuggestions(aElement);
  1.5668 +      let suggestions = autoCompleteSuggestions.concat(listSuggestions);
  1.5669 +
  1.5670 +      // Return false if there are no suggestions to show
  1.5671 +      if (!suggestions.length) {
  1.5672 +        aCallback(false);
  1.5673 +        return;
  1.5674 +      }
  1.5675 +
  1.5676 +      sendMessageToJava({
  1.5677 +        type:  "FormAssist:AutoComplete",
  1.5678 +        suggestions: suggestions,
  1.5679 +        rect: ElementTouchHelper.getBoundingContentRect(aElement)
  1.5680 +      });
  1.5681 +
  1.5682 +      // Keep track of input element so we can fill it in if the user
  1.5683 +      // selects an autocomplete suggestion
  1.5684 +      this._currentInputElement = aElement;
  1.5685 +      aCallback(true);
  1.5686 +    };
  1.5687 +
  1.5688 +    this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
  1.5689 +  },
  1.5690 +
  1.5691 +  // Only show a validation message if the user submitted an invalid form,
  1.5692 +  // there's a non-empty message string, and the element is the correct type
  1.5693 +  _isValidateable: function _isValidateable(aElement) {
  1.5694 +    if (!this._invalidSubmit ||
  1.5695 +        !aElement.validationMessage ||
  1.5696 +        !(aElement instanceof HTMLInputElement ||
  1.5697 +          aElement instanceof HTMLTextAreaElement ||
  1.5698 +          aElement instanceof HTMLSelectElement ||
  1.5699 +          aElement instanceof HTMLButtonElement))
  1.5700 +      return false;
  1.5701 +
  1.5702 +    return true;
  1.5703 +  },
  1.5704 +
  1.5705 +  // Sends a validation message and position data for an element to the Java UI.
  1.5706 +  // Returns true if there's a validation message to show, false otherwise.
  1.5707 +  _showValidationMessage: function _sendValidationMessage(aElement) {
  1.5708 +    if (!this._isValidateable(aElement))
  1.5709 +      return false;
  1.5710 +
  1.5711 +    sendMessageToJava({
  1.5712 +      type: "FormAssist:ValidationMessage",
  1.5713 +      validationMessage: aElement.validationMessage,
  1.5714 +      rect: ElementTouchHelper.getBoundingContentRect(aElement)
  1.5715 +    });
  1.5716 +
  1.5717 +    return true;
  1.5718 +  },
  1.5719 +
  1.5720 +  _hideFormAssistPopup: function _hideFormAssistPopup() {
  1.5721 +    sendMessageToJava({ type: "FormAssist:Hide" });
  1.5722 +  }
  1.5723 +};
  1.5724 +
  1.5725 +/**
  1.5726 + * An object to watch for Gecko status changes -- add-on installs, pref changes
  1.5727 + * -- and reflect them back to Java.
  1.5728 + */
  1.5729 +let HealthReportStatusListener = {
  1.5730 +  PREF_ACCEPT_LANG: "intl.accept_languages",
  1.5731 +  PREF_BLOCKLIST_ENABLED: "extensions.blocklist.enabled",
  1.5732 +
  1.5733 +  PREF_TELEMETRY_ENABLED:
  1.5734 +#ifdef MOZ_TELEMETRY_REPORTING
  1.5735 +    "toolkit.telemetry.enabled",
  1.5736 +#else
  1.5737 +    null,
  1.5738 +#endif
  1.5739 +
  1.5740 +  init: function () {
  1.5741 +    try {
  1.5742 +      AddonManager.addAddonListener(this);
  1.5743 +    } catch (ex) {
  1.5744 +      console.log("Failed to initialize add-on status listener. FHR cannot report add-on state. " + ex);
  1.5745 +    }
  1.5746 +
  1.5747 +    console.log("Adding HealthReport:RequestSnapshot observer.");
  1.5748 +    Services.obs.addObserver(this, "HealthReport:RequestSnapshot", false);
  1.5749 +    Services.prefs.addObserver(this.PREF_ACCEPT_LANG, this, false);
  1.5750 +    Services.prefs.addObserver(this.PREF_BLOCKLIST_ENABLED, this, false);
  1.5751 +    if (this.PREF_TELEMETRY_ENABLED) {
  1.5752 +      Services.prefs.addObserver(this.PREF_TELEMETRY_ENABLED, this, false);
  1.5753 +    }
  1.5754 +  },
  1.5755 +
  1.5756 +  uninit: function () {
  1.5757 +    Services.obs.removeObserver(this, "HealthReport:RequestSnapshot");
  1.5758 +    Services.prefs.removeObserver(this.PREF_ACCEPT_LANG, this);
  1.5759 +    Services.prefs.removeObserver(this.PREF_BLOCKLIST_ENABLED, this);
  1.5760 +    if (this.PREF_TELEMETRY_ENABLED) {
  1.5761 +      Services.prefs.removeObserver(this.PREF_TELEMETRY_ENABLED, this);
  1.5762 +    }
  1.5763 +
  1.5764 +    AddonManager.removeAddonListener(this);
  1.5765 +  },
  1.5766 +
  1.5767 +  observe: function (aSubject, aTopic, aData) {
  1.5768 +    switch (aTopic) {
  1.5769 +      case "HealthReport:RequestSnapshot":
  1.5770 +        HealthReportStatusListener.sendSnapshotToJava();
  1.5771 +        break;
  1.5772 +      case "nsPref:changed":
  1.5773 +        let response = {
  1.5774 +          type: "Pref:Change",
  1.5775 +          pref: aData,
  1.5776 +          isUserSet: Services.prefs.prefHasUserValue(aData),
  1.5777 +        };
  1.5778 +
  1.5779 +        switch (aData) {
  1.5780 +          case this.PREF_ACCEPT_LANG:
  1.5781 +            response.value = Services.prefs.getCharPref(aData);
  1.5782 +            break;
  1.5783 +          case this.PREF_TELEMETRY_ENABLED:
  1.5784 +          case this.PREF_BLOCKLIST_ENABLED:
  1.5785 +            response.value = Services.prefs.getBoolPref(aData);
  1.5786 +            break;
  1.5787 +          default:
  1.5788 +            console.log("Unexpected pref in HealthReportStatusListener: " + aData);
  1.5789 +            return;
  1.5790 +        }
  1.5791 +
  1.5792 +        sendMessageToJava(response);
  1.5793 +        break;
  1.5794 +    }
  1.5795 +  },
  1.5796 +
  1.5797 +  MILLISECONDS_PER_DAY: 24 * 60 * 60 * 1000,
  1.5798 +
  1.5799 +  COPY_FIELDS: [
  1.5800 +    "blocklistState",
  1.5801 +    "userDisabled",
  1.5802 +    "appDisabled",
  1.5803 +    "version",
  1.5804 +    "type",
  1.5805 +    "scope",
  1.5806 +    "foreignInstall",
  1.5807 +    "hasBinaryComponents",
  1.5808 +  ],
  1.5809 +
  1.5810 +  // Add-on types for which full details are recorded in FHR.
  1.5811 +  // All other types are ignored.
  1.5812 +  FULL_DETAIL_TYPES: [
  1.5813 +    "plugin",
  1.5814 +    "extension",
  1.5815 +    "service",
  1.5816 +  ],
  1.5817 +
  1.5818 +  /**
  1.5819 +   * Return true if the add-on is not of a type for which we report full details.
  1.5820 +   * These add-ons will still make it over to Java, but will be filtered out.
  1.5821 +   */
  1.5822 +  _shouldIgnore: function (aAddon) {
  1.5823 +    return this.FULL_DETAIL_TYPES.indexOf(aAddon.type) == -1;
  1.5824 +  },
  1.5825 +
  1.5826 +  _dateToDays: function (aDate) {
  1.5827 +    return Math.floor(aDate.getTime() / this.MILLISECONDS_PER_DAY);
  1.5828 +  },
  1.5829 +
  1.5830 +  jsonForAddon: function (aAddon) {
  1.5831 +    let o = {};
  1.5832 +    if (aAddon.installDate) {
  1.5833 +      o.installDay = this._dateToDays(aAddon.installDate);
  1.5834 +    }
  1.5835 +    if (aAddon.updateDate) {
  1.5836 +      o.updateDay = this._dateToDays(aAddon.updateDate);
  1.5837 +    }
  1.5838 +
  1.5839 +    for (let field of this.COPY_FIELDS) {
  1.5840 +      o[field] = aAddon[field];
  1.5841 +    }
  1.5842 +
  1.5843 +    return o;
  1.5844 +  },
  1.5845 +
  1.5846 +  notifyJava: function (aAddon, aNeedsRestart, aAction="Addons:Change") {
  1.5847 +    let json = this.jsonForAddon(aAddon);
  1.5848 +    if (this._shouldIgnore(aAddon)) {
  1.5849 +      json.ignore = true;
  1.5850 +    }
  1.5851 +    sendMessageToJava({ type: aAction, id: aAddon.id, json: json });
  1.5852 +  },
  1.5853 +
  1.5854 +  // Add-on listeners.
  1.5855 +  onEnabling: function (aAddon, aNeedsRestart) {
  1.5856 +    this.notifyJava(aAddon, aNeedsRestart);
  1.5857 +  },
  1.5858 +  onDisabling: function (aAddon, aNeedsRestart) {
  1.5859 +    this.notifyJava(aAddon, aNeedsRestart);
  1.5860 +  },
  1.5861 +  onInstalling: function (aAddon, aNeedsRestart) {
  1.5862 +    this.notifyJava(aAddon, aNeedsRestart);
  1.5863 +  },
  1.5864 +  onUninstalling: function (aAddon, aNeedsRestart) {
  1.5865 +    this.notifyJava(aAddon, aNeedsRestart, "Addons:Uninstalling");
  1.5866 +  },
  1.5867 +  onPropertyChanged: function (aAddon, aProperties) {
  1.5868 +    this.notifyJava(aAddon);
  1.5869 +  },
  1.5870 +  onOperationCancelled: function (aAddon) {
  1.5871 +    this.notifyJava(aAddon);
  1.5872 +  },
  1.5873 +
  1.5874 +  sendSnapshotToJava: function () {
  1.5875 +    AddonManager.getAllAddons(function (aAddons) {
  1.5876 +        let jsonA = {};
  1.5877 +        if (aAddons) {
  1.5878 +          for (let i = 0; i < aAddons.length; ++i) {
  1.5879 +            let addon = aAddons[i];
  1.5880 +            try {
  1.5881 +              let addonJSON = HealthReportStatusListener.jsonForAddon(addon);
  1.5882 +              if (HealthReportStatusListener._shouldIgnore(addon)) {
  1.5883 +                addonJSON.ignore = true;
  1.5884 +              }
  1.5885 +              jsonA[addon.id] = addonJSON;
  1.5886 +            } catch (e) {
  1.5887 +              // Just skip this add-on.
  1.5888 +            }
  1.5889 +          }
  1.5890 +        }
  1.5891 +
  1.5892 +        // Now add prefs.
  1.5893 +        let jsonP = {};
  1.5894 +        for (let pref of [this.PREF_BLOCKLIST_ENABLED, this.PREF_TELEMETRY_ENABLED]) {
  1.5895 +          if (!pref) {
  1.5896 +            // This will be the case for PREF_TELEMETRY_ENABLED in developer builds.
  1.5897 +            continue;
  1.5898 +          }
  1.5899 +          jsonP[pref] = {
  1.5900 +            pref: pref,
  1.5901 +            value: Services.prefs.getBoolPref(pref),
  1.5902 +            isUserSet: Services.prefs.prefHasUserValue(pref),
  1.5903 +          };
  1.5904 +        }
  1.5905 +        for (let pref of [this.PREF_ACCEPT_LANG]) {
  1.5906 +          jsonP[pref] = {
  1.5907 +            pref: pref,
  1.5908 +            value: Services.prefs.getCharPref(pref),
  1.5909 +            isUserSet: Services.prefs.prefHasUserValue(pref),
  1.5910 +          };
  1.5911 +        }
  1.5912 +
  1.5913 +        console.log("Sending snapshot message.");
  1.5914 +        sendMessageToJava({
  1.5915 +          type: "HealthReport:Snapshot",
  1.5916 +          json: {
  1.5917 +            addons: jsonA,
  1.5918 +            prefs: jsonP,
  1.5919 +          },
  1.5920 +        });
  1.5921 +      }.bind(this));
  1.5922 +  },
  1.5923 +};
  1.5924 +
  1.5925 +var XPInstallObserver = {
  1.5926 +  init: function xpi_init() {
  1.5927 +    Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false);
  1.5928 +    Services.obs.addObserver(XPInstallObserver, "addon-install-started", false);
  1.5929 +
  1.5930 +    AddonManager.addInstallListener(XPInstallObserver);
  1.5931 +  },
  1.5932 +
  1.5933 +  uninit: function xpi_uninit() {
  1.5934 +    Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
  1.5935 +    Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
  1.5936 +
  1.5937 +    AddonManager.removeInstallListener(XPInstallObserver);
  1.5938 +  },
  1.5939 +
  1.5940 +  observe: function xpi_observer(aSubject, aTopic, aData) {
  1.5941 +    switch (aTopic) {
  1.5942 +      case "addon-install-started":
  1.5943 +        NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsDownloading"), "short");
  1.5944 +        break;
  1.5945 +      case "addon-install-blocked":
  1.5946 +        let installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo);
  1.5947 +        let win = installInfo.originatingWindow;
  1.5948 +        let tab = BrowserApp.getTabForWindow(win.top);
  1.5949 +        if (!tab)
  1.5950 +          return;
  1.5951 +
  1.5952 +        let host = null;
  1.5953 +        if (installInfo.originatingURI) {
  1.5954 +          host = installInfo.originatingURI.host;
  1.5955 +        }
  1.5956 +
  1.5957 +        let brandShortName = Strings.brand.GetStringFromName("brandShortName");
  1.5958 +        let notificationName, buttons, message;
  1.5959 +        let strings = Strings.browser;
  1.5960 +        let enabled = true;
  1.5961 +        try {
  1.5962 +          enabled = Services.prefs.getBoolPref("xpinstall.enabled");
  1.5963 +        }
  1.5964 +        catch (e) {}
  1.5965 +
  1.5966 +        if (!enabled) {
  1.5967 +          notificationName = "xpinstall-disabled";
  1.5968 +          if (Services.prefs.prefIsLocked("xpinstall.enabled")) {
  1.5969 +            message = strings.GetStringFromName("xpinstallDisabledMessageLocked");
  1.5970 +            buttons = [];
  1.5971 +          } else {
  1.5972 +            message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2);
  1.5973 +            buttons = [{
  1.5974 +              label: strings.GetStringFromName("xpinstallDisabledButton"),
  1.5975 +              callback: function editPrefs() {
  1.5976 +                Services.prefs.setBoolPref("xpinstall.enabled", true);
  1.5977 +                return false;
  1.5978 +              }
  1.5979 +            }];
  1.5980 +          }
  1.5981 +        } else {
  1.5982 +          notificationName = "xpinstall";
  1.5983 +          if (host) {
  1.5984 +            // We have a host which asked for the install.
  1.5985 +            message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2);
  1.5986 +          } else {
  1.5987 +            // Without a host we address the add-on as the initiator of the install.
  1.5988 +            let addon = null;
  1.5989 +            if (installInfo.installs.length > 0) {
  1.5990 +              addon = installInfo.installs[0].name;
  1.5991 +            }
  1.5992 +            if (addon) {
  1.5993 +              // We have an addon name, show the regular message.
  1.5994 +              message = strings.formatStringFromName("xpinstallPromptWarningLocal", [brandShortName, addon], 2);
  1.5995 +            } else {
  1.5996 +              // We don't have an addon name, show an alternative message.
  1.5997 +              message = strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1);
  1.5998 +            }
  1.5999 +          }
  1.6000 +
  1.6001 +          buttons = [{
  1.6002 +            label: strings.GetStringFromName("xpinstallPromptAllowButton"),
  1.6003 +            callback: function() {
  1.6004 +              // Kick off the install
  1.6005 +              installInfo.install();
  1.6006 +              return false;
  1.6007 +            }
  1.6008 +          }];
  1.6009 +        }
  1.6010 +        NativeWindow.doorhanger.show(message, aTopic, buttons, tab.id);
  1.6011 +        break;
  1.6012 +    }
  1.6013 +  },
  1.6014 +
  1.6015 +  onInstallEnded: function(aInstall, aAddon) {
  1.6016 +    let needsRestart = false;
  1.6017 +    if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
  1.6018 +      needsRestart = true;
  1.6019 +    else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
  1.6020 +      needsRestart = true;
  1.6021 +
  1.6022 +    if (needsRestart) {
  1.6023 +      this.showRestartPrompt();
  1.6024 +    } else {
  1.6025 +      // Display completion message for new installs or updates not done Automatically
  1.6026 +      if (!aInstall.existingAddon || !AddonManager.shouldAutoUpdate(aInstall.existingAddon)) {
  1.6027 +        let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart");
  1.6028 +        NativeWindow.toast.show(message, "short");
  1.6029 +      }
  1.6030 +    }
  1.6031 +  },
  1.6032 +
  1.6033 +  onInstallFailed: function(aInstall) {
  1.6034 +    NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsFail"), "short");
  1.6035 +  },
  1.6036 +
  1.6037 +  onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {},
  1.6038 +
  1.6039 +  onDownloadFailed: function(aInstall) {
  1.6040 +    this.onInstallFailed(aInstall);
  1.6041 +  },
  1.6042 +
  1.6043 +  onDownloadCancelled: function(aInstall) {
  1.6044 +    let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host;
  1.6045 +    if (!host)
  1.6046 +      host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host;
  1.6047 +
  1.6048 +    let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError";
  1.6049 +    if (aInstall.error != 0)
  1.6050 +      error += aInstall.error;
  1.6051 +    else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
  1.6052 +      error += "Blocklisted";
  1.6053 +    else if (aInstall.addon && (!aInstall.addon.isCompatible || !aInstall.addon.isPlatformCompatible))
  1.6054 +      error += "Incompatible";
  1.6055 +    else
  1.6056 +      return; // No need to show anything in this case.
  1.6057 +
  1.6058 +    let msg = Strings.browser.GetStringFromName(error);
  1.6059 +    // TODO: formatStringFromName
  1.6060 +    msg = msg.replace("#1", aInstall.name);
  1.6061 +    if (host)
  1.6062 +      msg = msg.replace("#2", host);
  1.6063 +    msg = msg.replace("#3", Strings.brand.GetStringFromName("brandShortName"));
  1.6064 +    msg = msg.replace("#4", Services.appinfo.version);
  1.6065 +
  1.6066 +    NativeWindow.toast.show(msg, "short");
  1.6067 +  },
  1.6068 +
  1.6069 +  showRestartPrompt: function() {
  1.6070 +    let buttons = [{
  1.6071 +      label: Strings.browser.GetStringFromName("notificationRestart.button"),
  1.6072 +      callback: function() {
  1.6073 +        // Notify all windows that an application quit has been requested
  1.6074 +        let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
  1.6075 +        Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
  1.6076 +
  1.6077 +        // If nothing aborted, quit the app
  1.6078 +        if (cancelQuit.data == false) {
  1.6079 +          let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
  1.6080 +          appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
  1.6081 +        }
  1.6082 +      }
  1.6083 +    }];
  1.6084 +
  1.6085 +    let message = Strings.browser.GetStringFromName("notificationRestart.normal");
  1.6086 +    NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 });
  1.6087 +  },
  1.6088 +
  1.6089 +  hideRestartPrompt: function() {
  1.6090 +    NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id);
  1.6091 +  }
  1.6092 +};
  1.6093 +
  1.6094 +// Blindly copied from Safari documentation for now.
  1.6095 +const kViewportMinScale  = 0;
  1.6096 +const kViewportMaxScale  = 10;
  1.6097 +const kViewportMinWidth  = 200;
  1.6098 +const kViewportMaxWidth  = 10000;
  1.6099 +const kViewportMinHeight = 223;
  1.6100 +const kViewportMaxHeight = 10000;
  1.6101 +
  1.6102 +var ViewportHandler = {
  1.6103 +  // The cached viewport metadata for each document. We tie viewport metadata to each document
  1.6104 +  // instead of to each tab so that we don't have to update it when the document changes. Using an
  1.6105 +  // ES6 weak map lets us avoid leaks.
  1.6106 +  _metadata: new WeakMap(),
  1.6107 +
  1.6108 +  init: function init() {
  1.6109 +    addEventListener("DOMMetaAdded", this, false);
  1.6110 +    Services.obs.addObserver(this, "Window:Resize", false);
  1.6111 +  },
  1.6112 +
  1.6113 +  uninit: function uninit() {
  1.6114 +    removeEventListener("DOMMetaAdded", this, false);
  1.6115 +    Services.obs.removeObserver(this, "Window:Resize");
  1.6116 +  },
  1.6117 +
  1.6118 +  handleEvent: function handleEvent(aEvent) {
  1.6119 +    switch (aEvent.type) {
  1.6120 +      case "DOMMetaAdded":
  1.6121 +        let target = aEvent.originalTarget;
  1.6122 +        if (target.name != "viewport")
  1.6123 +          break;
  1.6124 +        let document = target.ownerDocument;
  1.6125 +        let browser = BrowserApp.getBrowserForDocument(document);
  1.6126 +        let tab = BrowserApp.getTabForBrowser(browser);
  1.6127 +        if (tab)
  1.6128 +          this.updateMetadata(tab, false);
  1.6129 +        break;
  1.6130 +    }
  1.6131 +  },
  1.6132 +
  1.6133 +  observe: function(aSubject, aTopic, aData) {
  1.6134 +    switch (aTopic) {
  1.6135 +      case "Window:Resize":
  1.6136 +        if (window.outerWidth == gScreenWidth && window.outerHeight == gScreenHeight)
  1.6137 +          break;
  1.6138 +        if (window.outerWidth == 0 || window.outerHeight == 0)
  1.6139 +          break;
  1.6140 +
  1.6141 +        let oldScreenWidth = gScreenWidth;
  1.6142 +        gScreenWidth = window.outerWidth * window.devicePixelRatio;
  1.6143 +        gScreenHeight = window.outerHeight * window.devicePixelRatio;
  1.6144 +        let tabs = BrowserApp.tabs;
  1.6145 +        for (let i = 0; i < tabs.length; i++)
  1.6146 +          tabs[i].updateViewportSize(oldScreenWidth);
  1.6147 +        break;
  1.6148 +    }
  1.6149 +  },
  1.6150 +
  1.6151 +  updateMetadata: function updateMetadata(tab, aInitialLoad) {
  1.6152 +    let contentWindow = tab.browser.contentWindow;
  1.6153 +    if (contentWindow.document.documentElement) {
  1.6154 +      let metadata = this.getViewportMetadata(contentWindow);
  1.6155 +      tab.updateViewportMetadata(metadata, aInitialLoad);
  1.6156 +    }
  1.6157 +  },
  1.6158 +
  1.6159 +  /**
  1.6160 +   * Returns the ViewportMetadata object.
  1.6161 +   */
  1.6162 +  getViewportMetadata: function getViewportMetadata(aWindow) {
  1.6163 +    let windowUtils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.6164 +
  1.6165 +    // viewport details found here
  1.6166 +    // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
  1.6167 +    // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html
  1.6168 +
  1.6169 +    // Note: These values will be NaN if parseFloat or parseInt doesn't find a number.
  1.6170 +    // Remember that NaN is contagious: Math.max(1, NaN) == Math.min(1, NaN) == NaN.
  1.6171 +    let hasMetaViewport = true;
  1.6172 +    let scale = parseFloat(windowUtils.getDocumentMetadata("viewport-initial-scale"));
  1.6173 +    let minScale = parseFloat(windowUtils.getDocumentMetadata("viewport-minimum-scale"));
  1.6174 +    let maxScale = parseFloat(windowUtils.getDocumentMetadata("viewport-maximum-scale"));
  1.6175 +
  1.6176 +    let widthStr = windowUtils.getDocumentMetadata("viewport-width");
  1.6177 +    let heightStr = windowUtils.getDocumentMetadata("viewport-height");
  1.6178 +    let width = this.clamp(parseInt(widthStr), kViewportMinWidth, kViewportMaxWidth) || 0;
  1.6179 +    let height = this.clamp(parseInt(heightStr), kViewportMinHeight, kViewportMaxHeight) || 0;
  1.6180 +
  1.6181 +    // Allow zoom unless explicity disabled or minScale and maxScale are equal.
  1.6182 +    // WebKit allows 0, "no", or "false" for viewport-user-scalable.
  1.6183 +    // Note: NaN != NaN. Therefore if minScale and maxScale are undefined the clause has no effect.
  1.6184 +    let allowZoomStr = windowUtils.getDocumentMetadata("viewport-user-scalable");
  1.6185 +    let allowZoom = !/^(0|no|false)$/.test(allowZoomStr) && (minScale != maxScale);
  1.6186 +
  1.6187 +    // Double-tap should always be disabled if allowZoom is disabled. So we initialize
  1.6188 +    // allowDoubleTapZoom to the same value as allowZoom and have additional conditions to
  1.6189 +    // disable it in updateViewportSize.
  1.6190 +    let allowDoubleTapZoom = allowZoom;
  1.6191 +
  1.6192 +    let autoSize = true;
  1.6193 +
  1.6194 +    if (isNaN(scale) && isNaN(minScale) && isNaN(maxScale) && allowZoomStr == "" && widthStr == "" && heightStr == "") {
  1.6195 +      // Only check for HandheldFriendly if we don't have a viewport meta tag
  1.6196 +      let handheldFriendly = windowUtils.getDocumentMetadata("HandheldFriendly");
  1.6197 +      if (handheldFriendly == "true") {
  1.6198 +        return new ViewportMetadata({
  1.6199 +          defaultZoom: 1,
  1.6200 +          autoSize: true,
  1.6201 +          allowZoom: true,
  1.6202 +          allowDoubleTapZoom: false
  1.6203 +        });
  1.6204 +      }
  1.6205 +
  1.6206 +      let doctype = aWindow.document.doctype;
  1.6207 +      if (doctype && /(WAP|WML|Mobile)/.test(doctype.publicId)) {
  1.6208 +        return new ViewportMetadata({
  1.6209 +          defaultZoom: 1,
  1.6210 +          autoSize: true,
  1.6211 +          allowZoom: true,
  1.6212 +          allowDoubleTapZoom: false
  1.6213 +        });
  1.6214 +      }
  1.6215 +
  1.6216 +      hasMetaViewport = false;
  1.6217 +      let defaultZoom = Services.prefs.getIntPref("browser.viewport.defaultZoom");
  1.6218 +      if (defaultZoom >= 0) {
  1.6219 +        scale = defaultZoom / 1000;
  1.6220 +        autoSize = false;
  1.6221 +      }
  1.6222 +    }
  1.6223 +
  1.6224 +    scale = this.clamp(scale, kViewportMinScale, kViewportMaxScale);
  1.6225 +    minScale = this.clamp(minScale, kViewportMinScale, kViewportMaxScale);
  1.6226 +    maxScale = this.clamp(maxScale, minScale, kViewportMaxScale);
  1.6227 +
  1.6228 +    if (autoSize) {
  1.6229 +      // If initial scale is 1.0 and width is not set, assume width=device-width
  1.6230 +      autoSize = (widthStr == "device-width" ||
  1.6231 +                  (!widthStr && (heightStr == "device-height" || scale == 1.0)));
  1.6232 +    }
  1.6233 +
  1.6234 +    let isRTL = aWindow.document.documentElement.dir == "rtl";
  1.6235 +
  1.6236 +    return new ViewportMetadata({
  1.6237 +      defaultZoom: scale,
  1.6238 +      minZoom: minScale,
  1.6239 +      maxZoom: maxScale,
  1.6240 +      width: width,
  1.6241 +      height: height,
  1.6242 +      autoSize: autoSize,
  1.6243 +      allowZoom: allowZoom,
  1.6244 +      allowDoubleTapZoom: allowDoubleTapZoom,
  1.6245 +      isSpecified: hasMetaViewport,
  1.6246 +      isRTL: isRTL
  1.6247 +    });
  1.6248 +  },
  1.6249 +
  1.6250 +  clamp: function(num, min, max) {
  1.6251 +    return Math.max(min, Math.min(max, num));
  1.6252 +  },
  1.6253 +
  1.6254 +  get displayDPI() {
  1.6255 +    let utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
  1.6256 +    delete this.displayDPI;
  1.6257 +    return this.displayDPI = utils.displayDPI;
  1.6258 +  },
  1.6259 +
  1.6260 +  /**
  1.6261 +   * Returns the viewport metadata for the given document, or the default metrics if no viewport
  1.6262 +   * metadata is available for that document.
  1.6263 +   */
  1.6264 +  getMetadataForDocument: function getMetadataForDocument(aDocument) {
  1.6265 +    let metadata = this._metadata.get(aDocument, new ViewportMetadata());
  1.6266 +    return metadata;
  1.6267 +  },
  1.6268 +
  1.6269 +  /** Updates the saved viewport metadata for the given content document. */
  1.6270 +  setMetadataForDocument: function setMetadataForDocument(aDocument, aMetadata) {
  1.6271 +    if (!aMetadata)
  1.6272 +      this._metadata.delete(aDocument);
  1.6273 +    else
  1.6274 +      this._metadata.set(aDocument, aMetadata);
  1.6275 +  }
  1.6276 +
  1.6277 +};
  1.6278 +
  1.6279 +/**
  1.6280 + * An object which represents the page's preferred viewport properties:
  1.6281 + *   width (int): The CSS viewport width in px.
  1.6282 + *   height (int): The CSS viewport height in px.
  1.6283 + *   defaultZoom (float): The initial scale when the page is loaded.
  1.6284 + *   minZoom (float): The minimum zoom level.
  1.6285 + *   maxZoom (float): The maximum zoom level.
  1.6286 + *   autoSize (boolean): Resize the CSS viewport when the window resizes.
  1.6287 + *   allowZoom (boolean): Let the user zoom in or out.
  1.6288 + *   allowDoubleTapZoom (boolean): Allow double-tap to zoom in.
  1.6289 + *   isSpecified (boolean): Whether the page viewport is specified or not.
  1.6290 + */
  1.6291 +function ViewportMetadata(aMetadata = {}) {
  1.6292 +  this.width = ("width" in aMetadata) ? aMetadata.width : 0;
  1.6293 +  this.height = ("height" in aMetadata) ? aMetadata.height : 0;
  1.6294 +  this.defaultZoom = ("defaultZoom" in aMetadata) ? aMetadata.defaultZoom : 0;
  1.6295 +  this.minZoom = ("minZoom" in aMetadata) ? aMetadata.minZoom : 0;
  1.6296 +  this.maxZoom = ("maxZoom" in aMetadata) ? aMetadata.maxZoom : 0;
  1.6297 +  this.autoSize = ("autoSize" in aMetadata) ? aMetadata.autoSize : false;
  1.6298 +  this.allowZoom = ("allowZoom" in aMetadata) ? aMetadata.allowZoom : true;
  1.6299 +  this.allowDoubleTapZoom = ("allowDoubleTapZoom" in aMetadata) ? aMetadata.allowDoubleTapZoom : true;
  1.6300 +  this.isSpecified = ("isSpecified" in aMetadata) ? aMetadata.isSpecified : false;
  1.6301 +  this.isRTL = ("isRTL" in aMetadata) ? aMetadata.isRTL : false;
  1.6302 +  Object.seal(this);
  1.6303 +}
  1.6304 +
  1.6305 +ViewportMetadata.prototype = {
  1.6306 +  width: null,
  1.6307 +  height: null,
  1.6308 +  defaultZoom: null,
  1.6309 +  minZoom: null,
  1.6310 +  maxZoom: null,
  1.6311 +  autoSize: null,
  1.6312 +  allowZoom: null,
  1.6313 +  allowDoubleTapZoom: null,
  1.6314 +  isSpecified: null,
  1.6315 +  isRTL: null,
  1.6316 +
  1.6317 +  toString: function() {
  1.6318 +    return "width=" + this.width
  1.6319 +         + "; height=" + this.height
  1.6320 +         + "; defaultZoom=" + this.defaultZoom
  1.6321 +         + "; minZoom=" + this.minZoom
  1.6322 +         + "; maxZoom=" + this.maxZoom
  1.6323 +         + "; autoSize=" + this.autoSize
  1.6324 +         + "; allowZoom=" + this.allowZoom
  1.6325 +         + "; allowDoubleTapZoom=" + this.allowDoubleTapZoom
  1.6326 +         + "; isSpecified=" + this.isSpecified
  1.6327 +         + "; isRTL=" + this.isRTL;
  1.6328 +  }
  1.6329 +};
  1.6330 +
  1.6331 +
  1.6332 +/**
  1.6333 + * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml
  1.6334 + */
  1.6335 +var PopupBlockerObserver = {
  1.6336 +  onUpdatePageReport: function onUpdatePageReport(aEvent) {
  1.6337 +    let browser = BrowserApp.selectedBrowser;
  1.6338 +    if (aEvent.originalTarget != browser)
  1.6339 +      return;
  1.6340 +
  1.6341 +    if (!browser.pageReport)
  1.6342 +      return;
  1.6343 +
  1.6344 +    let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup");
  1.6345 +    if (result == Ci.nsIPermissionManager.DENY_ACTION)
  1.6346 +      return;
  1.6347 +
  1.6348 +    // Only show the notification again if we've not already shown it. Since
  1.6349 +    // notifications are per-browser, we don't need to worry about re-adding
  1.6350 +    // it.
  1.6351 +    if (!browser.pageReport.reported) {
  1.6352 +      if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) {
  1.6353 +        let brandShortName = Strings.brand.GetStringFromName("brandShortName");
  1.6354 +        let popupCount = browser.pageReport.length;
  1.6355 +
  1.6356 +        let strings = Strings.browser;
  1.6357 +        let message = PluralForm.get(popupCount, strings.GetStringFromName("popup.message"))
  1.6358 +                                .replace("#1", brandShortName)
  1.6359 +                                .replace("#2", popupCount);
  1.6360 +
  1.6361 +        let buttons = [
  1.6362 +          {
  1.6363 +            label: strings.GetStringFromName("popup.show"),
  1.6364 +            callback: function(aChecked) {
  1.6365 +              // Set permission before opening popup windows
  1.6366 +              if (aChecked)
  1.6367 +                PopupBlockerObserver.allowPopupsForSite(true);
  1.6368 +
  1.6369 +              PopupBlockerObserver.showPopupsForSite();
  1.6370 +            }
  1.6371 +          },
  1.6372 +          {
  1.6373 +            label: strings.GetStringFromName("popup.dontShow"),
  1.6374 +            callback: function(aChecked) {
  1.6375 +              if (aChecked)
  1.6376 +                PopupBlockerObserver.allowPopupsForSite(false);
  1.6377 +            }
  1.6378 +          }
  1.6379 +        ];
  1.6380 +
  1.6381 +        let options = { checkbox: Strings.browser.GetStringFromName("popup.dontAskAgain") };
  1.6382 +        NativeWindow.doorhanger.show(message, "popup-blocked", buttons, null, options);
  1.6383 +      }
  1.6384 +      // Record the fact that we've reported this blocked popup, so we don't
  1.6385 +      // show it again.
  1.6386 +      browser.pageReport.reported = true;
  1.6387 +    }
  1.6388 +  },
  1.6389 +
  1.6390 +  allowPopupsForSite: function allowPopupsForSite(aAllow) {
  1.6391 +    let currentURI = BrowserApp.selectedBrowser.currentURI;
  1.6392 +    Services.perms.add(currentURI, "popup", aAllow
  1.6393 +                       ?  Ci.nsIPermissionManager.ALLOW_ACTION
  1.6394 +                       :  Ci.nsIPermissionManager.DENY_ACTION);
  1.6395 +    dump("Allowing popups for: " + currentURI);
  1.6396 +  },
  1.6397 +
  1.6398 +  showPopupsForSite: function showPopupsForSite() {
  1.6399 +    let uri = BrowserApp.selectedBrowser.currentURI;
  1.6400 +    let pageReport = BrowserApp.selectedBrowser.pageReport;
  1.6401 +    if (pageReport) {
  1.6402 +      for (let i = 0; i < pageReport.length; ++i) {
  1.6403 +        let popupURIspec = pageReport[i].popupWindowURI.spec;
  1.6404 +
  1.6405 +        // Sometimes the popup URI that we get back from the pageReport
  1.6406 +        // isn't useful (for instance, netscape.com's popup URI ends up
  1.6407 +        // being "http://www.netscape.com", which isn't really the URI of
  1.6408 +        // the popup they're trying to show).  This isn't going to be
  1.6409 +        // useful to the user, so we won't create a menu item for it.
  1.6410 +        if (popupURIspec == "" || popupURIspec == "about:blank" || popupURIspec == uri.spec)
  1.6411 +          continue;
  1.6412 +
  1.6413 +        let popupFeatures = pageReport[i].popupWindowFeatures;
  1.6414 +        let popupName = pageReport[i].popupWindowName;
  1.6415 +
  1.6416 +        let parent = BrowserApp.selectedTab;
  1.6417 +        let isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow);
  1.6418 +        BrowserApp.addTab(popupURIspec, { parentId: parent.id, isPrivate: isPrivate });
  1.6419 +      }
  1.6420 +    }
  1.6421 +  }
  1.6422 +};
  1.6423 +
  1.6424 +
  1.6425 +var IndexedDB = {
  1.6426 +  _permissionsPrompt: "indexedDB-permissions-prompt",
  1.6427 +  _permissionsResponse: "indexedDB-permissions-response",
  1.6428 +
  1.6429 +  _quotaPrompt: "indexedDB-quota-prompt",
  1.6430 +  _quotaResponse: "indexedDB-quota-response",
  1.6431 +  _quotaCancel: "indexedDB-quota-cancel",
  1.6432 +
  1.6433 +  init: function IndexedDB_init() {
  1.6434 +    Services.obs.addObserver(this, this._permissionsPrompt, false);
  1.6435 +    Services.obs.addObserver(this, this._quotaPrompt, false);
  1.6436 +    Services.obs.addObserver(this, this._quotaCancel, false);
  1.6437 +  },
  1.6438 +
  1.6439 +  uninit: function IndexedDB_uninit() {
  1.6440 +    Services.obs.removeObserver(this, this._permissionsPrompt);
  1.6441 +    Services.obs.removeObserver(this, this._quotaPrompt);
  1.6442 +    Services.obs.removeObserver(this, this._quotaCancel);
  1.6443 +  },
  1.6444 +
  1.6445 +  observe: function IndexedDB_observe(subject, topic, data) {
  1.6446 +    if (topic != this._permissionsPrompt &&
  1.6447 +        topic != this._quotaPrompt &&
  1.6448 +        topic != this._quotaCancel) {
  1.6449 +      throw new Error("Unexpected topic!");
  1.6450 +    }
  1.6451 +
  1.6452 +    let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);
  1.6453 +
  1.6454 +    let contentWindow = requestor.getInterface(Ci.nsIDOMWindow);
  1.6455 +    let contentDocument = contentWindow.document;
  1.6456 +    let tab = BrowserApp.getTabForWindow(contentWindow);
  1.6457 +    if (!tab)
  1.6458 +      return;
  1.6459 +
  1.6460 +    let host = contentDocument.documentURIObject.asciiHost;
  1.6461 +
  1.6462 +    let strings = Strings.browser;
  1.6463 +
  1.6464 +    let message, responseTopic;
  1.6465 +    if (topic == this._permissionsPrompt) {
  1.6466 +      message = strings.formatStringFromName("offlineApps.ask", [host], 1);
  1.6467 +      responseTopic = this._permissionsResponse;
  1.6468 +    } else if (topic == this._quotaPrompt) {
  1.6469 +      message = strings.formatStringFromName("indexedDBQuota.wantsTo", [ host, data ], 2);
  1.6470 +      responseTopic = this._quotaResponse;
  1.6471 +    } else if (topic == this._quotaCancel) {
  1.6472 +      responseTopic = this._quotaResponse;
  1.6473 +    }
  1.6474 +
  1.6475 +    const firstTimeoutDuration = 300000; // 5 minutes
  1.6476 +
  1.6477 +    let timeoutId;
  1.6478 +
  1.6479 +    let notificationID = responseTopic + host;
  1.6480 +    let observer = requestor.getInterface(Ci.nsIObserver);
  1.6481 +
  1.6482 +    // This will be set to the result of PopupNotifications.show() below, or to
  1.6483 +    // the result of PopupNotifications.getNotification() if this is a
  1.6484 +    // quotaCancel notification.
  1.6485 +    let notification;
  1.6486 +
  1.6487 +    function timeoutNotification() {
  1.6488 +      // Remove the notification.
  1.6489 +      NativeWindow.doorhanger.hide(notificationID, tab.id);
  1.6490 +
  1.6491 +      // Clear all of our timeout stuff. We may be called directly, not just
  1.6492 +      // when the timeout actually elapses.
  1.6493 +      clearTimeout(timeoutId);
  1.6494 +
  1.6495 +      // And tell the page that the popup timed out.
  1.6496 +      observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
  1.6497 +    }
  1.6498 +
  1.6499 +    if (topic == this._quotaCancel) {
  1.6500 +      NativeWindow.doorhanger.hide(notificationID, tab.id);
  1.6501 +      timeoutNotification();
  1.6502 +      observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
  1.6503 +      return;
  1.6504 +    }
  1.6505 +
  1.6506 +    let buttons = [{
  1.6507 +      label: strings.GetStringFromName("offlineApps.allow"),
  1.6508 +      callback: function() {
  1.6509 +        clearTimeout(timeoutId);
  1.6510 +        observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION);
  1.6511 +      }
  1.6512 +    },
  1.6513 +    {
  1.6514 +      label: strings.GetStringFromName("offlineApps.dontAllow2"),
  1.6515 +      callback: function(aChecked) {
  1.6516 +        clearTimeout(timeoutId);
  1.6517 +        let action = aChecked ? Ci.nsIPermissionManager.DENY_ACTION : Ci.nsIPermissionManager.UNKNOWN_ACTION;
  1.6518 +        observer.observe(null, responseTopic, action);
  1.6519 +      }
  1.6520 +    }];
  1.6521 +
  1.6522 +    let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
  1.6523 +    NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);
  1.6524 +
  1.6525 +    // Set the timeoutId after the popup has been created, and use the long
  1.6526 +    // timeout value. If the user doesn't notice the popup after this amount of
  1.6527 +    // time then it is most likely not visible and we want to alert the page.
  1.6528 +    timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
  1.6529 +  }
  1.6530 +};
  1.6531 +
  1.6532 +var CharacterEncoding = {
  1.6533 +  _charsets: [],
  1.6534 +
  1.6535 +  init: function init() {
  1.6536 +    Services.obs.addObserver(this, "CharEncoding:Get", false);
  1.6537 +    Services.obs.addObserver(this, "CharEncoding:Set", false);
  1.6538 +    this.sendState();
  1.6539 +  },
  1.6540 +
  1.6541 +  uninit: function uninit() {
  1.6542 +    Services.obs.removeObserver(this, "CharEncoding:Get");
  1.6543 +    Services.obs.removeObserver(this, "CharEncoding:Set");
  1.6544 +  },
  1.6545 +
  1.6546 +  observe: function observe(aSubject, aTopic, aData) {
  1.6547 +    switch (aTopic) {
  1.6548 +      case "CharEncoding:Get":
  1.6549 +        this.getEncoding();
  1.6550 +        break;
  1.6551 +      case "CharEncoding:Set":
  1.6552 +        this.setEncoding(aData);
  1.6553 +        break;
  1.6554 +    }
  1.6555 +  },
  1.6556 +
  1.6557 +  sendState: function sendState() {
  1.6558 +    let showCharEncoding = "false";
  1.6559 +    try {
  1.6560 +      showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data;
  1.6561 +    } catch (e) { /* Optional */ }
  1.6562 +
  1.6563 +    sendMessageToJava({
  1.6564 +      type: "CharEncoding:State",
  1.6565 +      visible: showCharEncoding
  1.6566 +    });
  1.6567 +  },
  1.6568 +
  1.6569 +  getEncoding: function getEncoding() {
  1.6570 +    function infoToCharset(info) {
  1.6571 +      return { code: info.value, title: info.label };
  1.6572 +    }
  1.6573 +
  1.6574 +    if (!this._charsets.length) {
  1.6575 +      let data = CharsetMenu.getData();
  1.6576 +
  1.6577 +      // In the desktop UI, the pinned charsets are shown above the rest.
  1.6578 +      let pinnedCharsets = data.pinnedCharsets.map(infoToCharset);
  1.6579 +      let otherCharsets = data.otherCharsets.map(infoToCharset)
  1.6580 +
  1.6581 +      this._charsets = pinnedCharsets.concat(otherCharsets);
  1.6582 +    }
  1.6583 +
  1.6584 +    // Look for the index of the selected charset. Default to -1 if the
  1.6585 +    // doc charset isn't found in the list of available charsets.
  1.6586 +    let docCharset = BrowserApp.selectedBrowser.contentDocument.characterSet;
  1.6587 +    let selected = -1;
  1.6588 +    let charsetCount = this._charsets.length;
  1.6589 +
  1.6590 +    for (let i = 0; i < charsetCount; i++) {
  1.6591 +      if (this._charsets[i].code === docCharset) {
  1.6592 +        selected = i;
  1.6593 +        break;
  1.6594 +      }
  1.6595 +    }
  1.6596 +
  1.6597 +    sendMessageToJava({
  1.6598 +      type: "CharEncoding:Data",
  1.6599 +      charsets: this._charsets,
  1.6600 +      selected: selected
  1.6601 +    });
  1.6602 +  },
  1.6603 +
  1.6604 +  setEncoding: function setEncoding(aEncoding) {
  1.6605 +    let browser = BrowserApp.selectedBrowser;
  1.6606 +    browser.docShell.gatherCharsetMenuTelemetry();
  1.6607 +    browser.docShell.charset = aEncoding;
  1.6608 +    browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
  1.6609 +  }
  1.6610 +};
  1.6611 +
  1.6612 +var IdentityHandler = {
  1.6613 +  // No trusted identity information. No site identity icon is shown.
  1.6614 +  IDENTITY_MODE_UNKNOWN: "unknown",
  1.6615 +
  1.6616 +  // Minimal SSL CA-signed domain verification. Blue lock icon is shown.
  1.6617 +  IDENTITY_MODE_DOMAIN_VERIFIED: "verified",
  1.6618 +
  1.6619 +  // High-quality identity information. Green lock icon is shown.
  1.6620 +  IDENTITY_MODE_IDENTIFIED: "identified",
  1.6621 +
  1.6622 +  // The following mixed content modes are only used if "security.mixed_content.block_active_content"
  1.6623 +  // is enabled. Even though the mixed content state and identitity state are orthogonal,
  1.6624 +  // our Java frontend coalesces them into one indicator.
  1.6625 +
  1.6626 +  // Blocked active mixed content. Shield icon is shown, with a popup option to load content.
  1.6627 +  IDENTITY_MODE_MIXED_CONTENT_BLOCKED: "mixed_content_blocked",
  1.6628 +
  1.6629 +  // Loaded active mixed content. Yellow triangle icon is shown.
  1.6630 +  IDENTITY_MODE_MIXED_CONTENT_LOADED: "mixed_content_loaded",
  1.6631 +
  1.6632 +  // Cache the most recent SSLStatus and Location seen in getIdentityStrings
  1.6633 +  _lastStatus : null,
  1.6634 +  _lastLocation : null,
  1.6635 +
  1.6636 +  /**
  1.6637 +   * Helper to parse out the important parts of _lastStatus (of the SSL cert in
  1.6638 +   * particular) for use in constructing identity UI strings
  1.6639 +  */
  1.6640 +  getIdentityData : function() {
  1.6641 +    let result = {};
  1.6642 +    let status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus);
  1.6643 +    let cert = status.serverCert;
  1.6644 +
  1.6645 +    // Human readable name of Subject
  1.6646 +    result.subjectOrg = cert.organization;
  1.6647 +
  1.6648 +    // SubjectName fields, broken up for individual access
  1.6649 +    if (cert.subjectName) {
  1.6650 +      result.subjectNameFields = {};
  1.6651 +      cert.subjectName.split(",").forEach(function(v) {
  1.6652 +        let field = v.split("=");
  1.6653 +        this[field[0]] = field[1];
  1.6654 +      }, result.subjectNameFields);
  1.6655 +
  1.6656 +      // Call out city, state, and country specifically
  1.6657 +      result.city = result.subjectNameFields.L;
  1.6658 +      result.state = result.subjectNameFields.ST;
  1.6659 +      result.country = result.subjectNameFields.C;
  1.6660 +    }
  1.6661 +
  1.6662 +    // Human readable name of Certificate Authority
  1.6663 +    result.caOrg =  cert.issuerOrganization || cert.issuerCommonName;
  1.6664 +    result.cert = cert;
  1.6665 +
  1.6666 +    return result;
  1.6667 +  },
  1.6668 +
  1.6669 +  /**
  1.6670 +   * Determines the identity mode corresponding to the icon we show in the urlbar.
  1.6671 +   */
  1.6672 +  getIdentityMode: function getIdentityMode(aState) {
  1.6673 +    if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT)
  1.6674 +      return this.IDENTITY_MODE_MIXED_CONTENT_BLOCKED;
  1.6675 +
  1.6676 +    // Only show an indicator for loaded mixed content if the pref to block it is enabled
  1.6677 +    if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) &&
  1.6678 +         Services.prefs.getBoolPref("security.mixed_content.block_active_content"))
  1.6679 +      return this.IDENTITY_MODE_MIXED_CONTENT_LOADED;
  1.6680 +
  1.6681 +    if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
  1.6682 +      return this.IDENTITY_MODE_IDENTIFIED;
  1.6683 +
  1.6684 +    if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE)
  1.6685 +      return this.IDENTITY_MODE_DOMAIN_VERIFIED;
  1.6686 +
  1.6687 +    return this.IDENTITY_MODE_UNKNOWN;
  1.6688 +  },
  1.6689 +
  1.6690 +  /**
  1.6691 +   * Determine the identity of the page being displayed by examining its SSL cert
  1.6692 +   * (if available). Return the data needed to update the UI.
  1.6693 +   */
  1.6694 +  checkIdentity: function checkIdentity(aState, aBrowser) {
  1.6695 +    this._lastStatus = aBrowser.securityUI
  1.6696 +                               .QueryInterface(Components.interfaces.nsISSLStatusProvider)
  1.6697 +                               .SSLStatus;
  1.6698 +
  1.6699 +    // Don't pass in the actual location object, since it can cause us to 
  1.6700 +    // hold on to the window object too long.  Just pass in the fields we
  1.6701 +    // care about. (bug 424829)
  1.6702 +    let locationObj = {};
  1.6703 +    try {
  1.6704 +      let location = aBrowser.contentWindow.location;
  1.6705 +      locationObj.host = location.host;
  1.6706 +      locationObj.hostname = location.hostname;
  1.6707 +      locationObj.port = location.port;
  1.6708 +    } catch (ex) {
  1.6709 +      // Can sometimes throw if the URL being visited has no host/hostname,
  1.6710 +      // e.g. about:blank. The _state for these pages means we won't need these
  1.6711 +      // properties anyways, though.
  1.6712 +    }
  1.6713 +    this._lastLocation = locationObj;
  1.6714 +
  1.6715 +    let mode = this.getIdentityMode(aState);
  1.6716 +    let result = { mode: mode };
  1.6717 +
  1.6718 +    // Don't show identity data for pages with an unknown identity or if any
  1.6719 +    // mixed content is loaded (mixed display content is loaded by default).
  1.6720 +    if (mode == this.IDENTITY_MODE_UNKNOWN ||
  1.6721 +        aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN)
  1.6722 +      return result;
  1.6723 +
  1.6724 +    // Ideally we'd just make this a Java string
  1.6725 +    result.encrypted = Strings.browser.GetStringFromName("identity.encrypted2");
  1.6726 +    result.host = this.getEffectiveHost();
  1.6727 +
  1.6728 +    let iData = this.getIdentityData();
  1.6729 +    result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
  1.6730 +
  1.6731 +    // If the cert is identified, then we can populate the results with credentials
  1.6732 +    if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
  1.6733 +      result.owner = iData.subjectOrg;
  1.6734 +
  1.6735 +      // Build an appropriate supplemental block out of whatever location data we have
  1.6736 +      let supplemental = "";
  1.6737 +      if (iData.city)
  1.6738 +        supplemental += iData.city + "\n";
  1.6739 +      if (iData.state && iData.country)
  1.6740 +        supplemental += Strings.browser.formatStringFromName("identity.identified.state_and_country", [iData.state, iData.country], 2);
  1.6741 +      else if (iData.state) // State only
  1.6742 +        supplemental += iData.state;
  1.6743 +      else if (iData.country) // Country only
  1.6744 +        supplemental += iData.country;
  1.6745 +      result.supplemental = supplemental;
  1.6746 +
  1.6747 +      return result;
  1.6748 +    }
  1.6749 +    
  1.6750 +    // Otherwise, we don't know the cert owner
  1.6751 +    result.owner = Strings.browser.GetStringFromName("identity.ownerUnknown3");
  1.6752 +
  1.6753 +    // Cache the override service the first time we need to check it
  1.6754 +    if (!this._overrideService)
  1.6755 +      this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
  1.6756 +
  1.6757 +    // Check whether this site is a security exception. XPConnect does the right
  1.6758 +    // thing here in terms of converting _lastLocation.port from string to int, but
  1.6759 +    // the overrideService doesn't like undefined ports, so make sure we have
  1.6760 +    // something in the default case (bug 432241).
  1.6761 +    // .hostname can return an empty string in some exceptional cases -
  1.6762 +    // hasMatchingOverride does not handle that, so avoid calling it.
  1.6763 +    // Updating the tooltip value in those cases isn't critical.
  1.6764 +    // FIXME: Fixing bug 646690 would probably makes this check unnecessary
  1.6765 +    if (this._lastLocation.hostname &&
  1.6766 +        this._overrideService.hasMatchingOverride(this._lastLocation.hostname,
  1.6767 +                                                  (this._lastLocation.port || 443),
  1.6768 +                                                  iData.cert, {}, {}))
  1.6769 +      result.verifier = Strings.browser.GetStringFromName("identity.identified.verified_by_you");
  1.6770 +
  1.6771 +    return result;
  1.6772 +  },
  1.6773 +
  1.6774 +  /**
  1.6775 +   * Return the eTLD+1 version of the current hostname
  1.6776 +   */
  1.6777 +  getEffectiveHost: function getEffectiveHost() {
  1.6778 +    if (!this._IDNService)
  1.6779 +      this._IDNService = Cc["@mozilla.org/network/idn-service;1"]
  1.6780 +                         .getService(Ci.nsIIDNService);
  1.6781 +    try {
  1.6782 +      let baseDomain = Services.eTLD.getBaseDomainFromHost(this._lastLocation.hostname);
  1.6783 +      return this._IDNService.convertToDisplayIDN(baseDomain, {});
  1.6784 +    } catch (e) {
  1.6785 +      // If something goes wrong (e.g. hostname is an IP address) just fail back
  1.6786 +      // to the full domain.
  1.6787 +      return this._lastLocation.hostname;
  1.6788 +    }
  1.6789 +  }
  1.6790 +};
  1.6791 +
  1.6792 +function OverscrollController(aTab) {
  1.6793 +  this.tab = aTab;
  1.6794 +}
  1.6795 +
  1.6796 +OverscrollController.prototype = {
  1.6797 +  supportsCommand : function supportsCommand(aCommand) {
  1.6798 +    if (aCommand != "cmd_linePrevious" && aCommand != "cmd_scrollPageUp")
  1.6799 +      return false;
  1.6800 +
  1.6801 +    return (this.tab.getViewport().y == 0);
  1.6802 +  },
  1.6803 +
  1.6804 +  isCommandEnabled : function isCommandEnabled(aCommand) {
  1.6805 +    return this.supportsCommand(aCommand);
  1.6806 +  },
  1.6807 +
  1.6808 +  doCommand : function doCommand(aCommand){
  1.6809 +    sendMessageToJava({ type: "ToggleChrome:Focus" });
  1.6810 +  },
  1.6811 +
  1.6812 +  onEvent : function onEvent(aEvent) { }
  1.6813 +};
  1.6814 +
  1.6815 +var SearchEngines = {
  1.6816 +  _contextMenuId: null,
  1.6817 +  PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
  1.6818 +  PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
  1.6819 +
  1.6820 +  init: function init() {
  1.6821 +    Services.obs.addObserver(this, "SearchEngines:Add", false);
  1.6822 +    Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
  1.6823 +    Services.obs.addObserver(this, "SearchEngines:Remove", false);
  1.6824 +    Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false);
  1.6825 +    Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
  1.6826 +
  1.6827 +    let filter = {
  1.6828 +      matches: function (aElement) {
  1.6829 +        // Copied from body of isTargetAKeywordField function in nsContextMenu.js
  1.6830 +        if(!(aElement instanceof HTMLInputElement))
  1.6831 +          return false;
  1.6832 +        let form = aElement.form;
  1.6833 +        if (!form || aElement.type == "password")
  1.6834 +          return false;
  1.6835 +
  1.6836 +        let method = form.method.toUpperCase();
  1.6837 +
  1.6838 +        // These are the following types of forms we can create keywords for:
  1.6839 +        //
  1.6840 +        // method    encoding type        can create keyword
  1.6841 +        // GET       *                                   YES
  1.6842 +        //           *                                   YES
  1.6843 +        // POST      *                                   YES
  1.6844 +        // POST      application/x-www-form-urlencoded   YES
  1.6845 +        // POST      text/plain                          NO ( a little tricky to do)
  1.6846 +        // POST      multipart/form-data                 NO
  1.6847 +        // POST      everything else                     YES
  1.6848 +        return (method == "GET" || method == "") ||
  1.6849 +               (form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
  1.6850 +      }
  1.6851 +    };
  1.6852 +    SelectionHandler.addAction({
  1.6853 +      id: "search_add_action",
  1.6854 +      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine"),
  1.6855 +      icon: "drawable://ab_add_search_engine",
  1.6856 +      selector: filter,
  1.6857 +      action: function(aElement) {
  1.6858 +        UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
  1.6859 +        SearchEngines.addEngine(aElement);
  1.6860 +      }
  1.6861 +    });
  1.6862 +  },
  1.6863 +
  1.6864 +  uninit: function uninit() {
  1.6865 +    Services.obs.removeObserver(this, "SearchEngines:Add");
  1.6866 +    Services.obs.removeObserver(this, "SearchEngines:GetVisible");
  1.6867 +    Services.obs.removeObserver(this, "SearchEngines:Remove");
  1.6868 +    Services.obs.removeObserver(this, "SearchEngines:RestoreDefaults");
  1.6869 +    Services.obs.removeObserver(this, "SearchEngines:SetDefault");
  1.6870 +    if (this._contextMenuId != null)
  1.6871 +      NativeWindow.contextmenus.remove(this._contextMenuId);
  1.6872 +  },
  1.6873 +
  1.6874 +  // Fetch list of search engines. all ? All engines : Visible engines only.
  1.6875 +  _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) {
  1.6876 +    if (!Components.isSuccessCode(rv)) {
  1.6877 +      Cu.reportError("Could not initialize search service, bailing out.");
  1.6878 +      return;
  1.6879 +    }
  1.6880 +
  1.6881 +    let engineData = Services.search.getVisibleEngines({});
  1.6882 +    let searchEngines = engineData.map(function (engine) {
  1.6883 +      return {
  1.6884 +        name: engine.name,
  1.6885 +        identifier: engine.identifier,
  1.6886 +        iconURI: (engine.iconURI ? engine.iconURI.spec : null),
  1.6887 +        hidden: engine.hidden
  1.6888 +      };
  1.6889 +    });
  1.6890 +
  1.6891 +    let suggestTemplate = null;
  1.6892 +    let suggestEngine = null;
  1.6893 +
  1.6894 +    // Check to see if the default engine supports search suggestions. We only need to check
  1.6895 +    // the default engine because we only show suggestions for the default engine in the UI.
  1.6896 +    let engine = Services.search.defaultEngine;
  1.6897 +    if (engine.supportsResponseType("application/x-suggestions+json")) {
  1.6898 +      suggestEngine = engine.name;
  1.6899 +      suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec;
  1.6900 +    }
  1.6901 +
  1.6902 +    // By convention, the currently configured default engine is at position zero in searchEngines.
  1.6903 +    sendMessageToJava({
  1.6904 +      type: "SearchEngines:Data",
  1.6905 +      searchEngines: searchEngines,
  1.6906 +      suggest: {
  1.6907 +        engine: suggestEngine,
  1.6908 +        template: suggestTemplate,
  1.6909 +        enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED),
  1.6910 +        prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
  1.6911 +      }
  1.6912 +    });
  1.6913 +
  1.6914 +    // Send a speculative connection to the default engine.
  1.6915 +    let connector = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
  1.6916 +    let searchURI = Services.search.defaultEngine.getSubmission("dummy").uri;
  1.6917 +    let callbacks = window.QueryInterface(Ci.nsIInterfaceRequestor)
  1.6918 +                          .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsILoadContext);
  1.6919 +    try {
  1.6920 +      connector.speculativeConnect(searchURI, callbacks);
  1.6921 +    } catch (e) {}
  1.6922 +  },
  1.6923 +
  1.6924 +  // Helper method to extract the engine name from a JSON. Simplifies the observe function.
  1.6925 +  _extractEngineFromJSON: function _extractEngineFromJSON(aData) {
  1.6926 +    let data = JSON.parse(aData);
  1.6927 +    return Services.search.getEngineByName(data.engine);
  1.6928 +  },
  1.6929 +
  1.6930 +  observe: function observe(aSubject, aTopic, aData) {
  1.6931 +    let engine;
  1.6932 +    switch(aTopic) {
  1.6933 +      case "SearchEngines:Add":
  1.6934 +        this.displaySearchEnginesList(aData);
  1.6935 +        break;
  1.6936 +      case "SearchEngines:GetVisible":
  1.6937 +        Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
  1.6938 +        break;
  1.6939 +      case "SearchEngines:Remove":
  1.6940 +        // Make sure the engine isn't hidden before removing it, to make sure it's
  1.6941 +        // visible if the user later re-adds it (works around bug 341833)
  1.6942 +        engine = this._extractEngineFromJSON(aData);
  1.6943 +        engine.hidden = false;
  1.6944 +        Services.search.removeEngine(engine);
  1.6945 +        break;
  1.6946 +      case "SearchEngines:RestoreDefaults":
  1.6947 +        // Un-hides all default engines.
  1.6948 +        Services.search.restoreDefaultEngines();
  1.6949 +        break;
  1.6950 +      case "SearchEngines:SetDefault":
  1.6951 +        engine = this._extractEngineFromJSON(aData);
  1.6952 +        // Move the new default search engine to the top of the search engine list.
  1.6953 +        Services.search.moveEngine(engine, 0);
  1.6954 +        Services.search.defaultEngine = engine;
  1.6955 +        break;
  1.6956 +
  1.6957 +      default:
  1.6958 +        dump("Unexpected message type observed: " + aTopic);
  1.6959 +        break;
  1.6960 +    }
  1.6961 +  },
  1.6962 +
  1.6963 +  // Display context menu listing names of the search engines available to be added.
  1.6964 +  displaySearchEnginesList: function displaySearchEnginesList(aData) {
  1.6965 +    let data = JSON.parse(aData);
  1.6966 +    let tab = BrowserApp.getTabForId(data.tabId);
  1.6967 +
  1.6968 +    if (!tab)
  1.6969 +      return;
  1.6970 +
  1.6971 +    let browser = tab.browser;
  1.6972 +    let engines = browser.engines;
  1.6973 +
  1.6974 +    let p = new Prompt({
  1.6975 +      window: browser.contentWindow
  1.6976 +    }).setSingleChoiceItems(engines.map(function(e) {
  1.6977 +      return { label: e.title };
  1.6978 +    })).show((function(data) {
  1.6979 +      if (data.button == -1)
  1.6980 +        return;
  1.6981 +
  1.6982 +      this.addOpenSearchEngine(engines[data.button]);
  1.6983 +      engines.splice(data.button, 1);
  1.6984 +
  1.6985 +      if (engines.length < 1) {
  1.6986 +        // Broadcast message that there are no more add-able search engines.
  1.6987 +        let newEngineMessage = {
  1.6988 +          type: "Link:OpenSearch",
  1.6989 +          tabID: tab.id,
  1.6990 +          visible: false
  1.6991 +        };
  1.6992 +
  1.6993 +        sendMessageToJava(newEngineMessage);
  1.6994 +      }
  1.6995 +    }).bind(this));
  1.6996 +  },
  1.6997 +
  1.6998 +  addOpenSearchEngine: function addOpenSearchEngine(engine) {
  1.6999 +    Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, {
  1.7000 +      onSuccess: function() {
  1.7001 +        // Display a toast confirming addition of new search engine.
  1.7002 +        NativeWindow.toast.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), "long");
  1.7003 +      },
  1.7004 +
  1.7005 +      onError: function(aCode) {
  1.7006 +        let errorMessage;
  1.7007 +        if (aCode == 2) {
  1.7008 +          // Engine is a duplicate.
  1.7009 +          errorMessage = "alertSearchEngineDuplicateToast";
  1.7010 +
  1.7011 +        } else {
  1.7012 +          // Unknown failure. Display general error message.
  1.7013 +          errorMessage = "alertSearchEngineErrorToast";
  1.7014 +        }
  1.7015 +
  1.7016 +        NativeWindow.toast.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), "long");
  1.7017 +      }
  1.7018 +    });
  1.7019 +  },
  1.7020 +
  1.7021 +  addEngine: function addEngine(aElement) {
  1.7022 +    let form = aElement.form;
  1.7023 +    let charset = aElement.ownerDocument.characterSet;
  1.7024 +    let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null);
  1.7025 +    let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
  1.7026 +    let method = form.method.toUpperCase();
  1.7027 +    let formData = [];
  1.7028 +
  1.7029 +    for (let i = 0; i < form.elements.length; ++i) {
  1.7030 +      let el = form.elements[i];
  1.7031 +      if (!el.type)
  1.7032 +        continue;
  1.7033 +
  1.7034 +      // make this text field a generic search parameter
  1.7035 +      if (aElement == el) {
  1.7036 +        formData.push({ name: el.name, value: "{searchTerms}" });
  1.7037 +        continue;
  1.7038 +      }
  1.7039 +
  1.7040 +      let type = el.type.toLowerCase();
  1.7041 +      let escapedName = escape(el.name);
  1.7042 +      let escapedValue = escape(el.value);
  1.7043 +
  1.7044 +      // add other form elements as parameters
  1.7045 +      switch (el.type) {
  1.7046 +        case "checkbox":
  1.7047 +        case "radio":
  1.7048 +          if (!el.checked) break;
  1.7049 +        case "text":
  1.7050 +        case "hidden":
  1.7051 +        case "textarea":
  1.7052 +          formData.push({ name: escapedName, value: escapedValue });
  1.7053 +          break;
  1.7054 +        case "select-one":
  1.7055 +          for (let option of el.options) {
  1.7056 +            if (option.selected) {
  1.7057 +              formData.push({ name: escapedName, value: escapedValue });
  1.7058 +              break;
  1.7059 +            }
  1.7060 +          }
  1.7061 +      }
  1.7062 +    }
  1.7063 +
  1.7064 +    // prompt user for name of search engine
  1.7065 +    let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine");
  1.7066 +    let title = { value: (aElement.ownerDocument.title || docURI.host) };
  1.7067 +    if (!Services.prompt.prompt(null, promptTitle, null, title, null, {}))
  1.7068 +      return;
  1.7069 +
  1.7070 +    // fetch the favicon for this page
  1.7071 +    let dbFile = FileUtils.getFile("ProfD", ["browser.db"]);
  1.7072 +    let mDBConn = Services.storage.openDatabase(dbFile);
  1.7073 +    let stmts = [];
  1.7074 +    stmts[0] = mDBConn.createStatement("SELECT favicon FROM history_with_favicons WHERE url = ?");
  1.7075 +    stmts[0].bindStringParameter(0, docURI.spec);
  1.7076 +    let favicon = null;
  1.7077 +    Services.search.init(function addEngine_cb(rv) {
  1.7078 +      if (!Components.isSuccessCode(rv)) {
  1.7079 +        Cu.reportError("Could not initialize search service, bailing out.");
  1.7080 +        return;
  1.7081 +      }
  1.7082 +      mDBConn.executeAsync(stmts, stmts.length, {
  1.7083 +        handleResult: function (results) {
  1.7084 +          let bytes = results.getNextRow().getResultByName("favicon");
  1.7085 +          if (bytes && bytes.length) {
  1.7086 +            favicon = "data:image/x-icon;base64," + btoa(String.fromCharCode.apply(null, bytes));
  1.7087 +          }
  1.7088 +        },
  1.7089 +        handleCompletion: function (reason) {
  1.7090 +          // if there's already an engine with this name, add a number to
  1.7091 +          // make the name unique (e.g., "Google" becomes "Google 2")
  1.7092 +          let name = title.value;
  1.7093 +          for (let i = 2; Services.search.getEngineByName(name); i++)
  1.7094 +            name = title.value + " " + i;
  1.7095 +
  1.7096 +          Services.search.addEngineWithDetails(name, favicon, null, null, method, formURL);
  1.7097 +          let engine = Services.search.getEngineByName(name);
  1.7098 +          engine.wrappedJSObject._queryCharset = charset;
  1.7099 +          for (let i = 0; i < formData.length; ++i) {
  1.7100 +            let param = formData[i];
  1.7101 +            if (param.name && param.value)
  1.7102 +              engine.addParam(param.name, param.value, null);
  1.7103 +          }
  1.7104 +        }
  1.7105 +      });
  1.7106 +    });
  1.7107 +  }
  1.7108 +};
  1.7109 +
  1.7110 +var ActivityObserver = {
  1.7111 +  init: function ao_init() {
  1.7112 +    Services.obs.addObserver(this, "application-background", false);
  1.7113 +    Services.obs.addObserver(this, "application-foreground", false);
  1.7114 +  },
  1.7115 +
  1.7116 +  observe: function ao_observe(aSubject, aTopic, aData) {
  1.7117 +    let isForeground = false;
  1.7118 +    let tab = BrowserApp.selectedTab;
  1.7119 +
  1.7120 +    switch (aTopic) {
  1.7121 +      case "application-background" :
  1.7122 +        let doc = (tab ? tab.browser.contentDocument : null);
  1.7123 +        if (doc && doc.mozFullScreen) {
  1.7124 +          doc.mozCancelFullScreen();
  1.7125 +        }
  1.7126 +        isForeground = false;
  1.7127 +        break;
  1.7128 +      case "application-foreground" :
  1.7129 +        isForeground = true;
  1.7130 +        break;
  1.7131 +    }
  1.7132 +
  1.7133 +    if (tab && tab.getActive() != isForeground) {
  1.7134 +      tab.setActive(isForeground);
  1.7135 +    }
  1.7136 +  }
  1.7137 +};
  1.7138 +
  1.7139 +#ifndef MOZ_ANDROID_SYNTHAPKS
  1.7140 +var WebappsUI = {
  1.7141 +  init: function init() {
  1.7142 +    Cu.import("resource://gre/modules/Webapps.jsm");
  1.7143 +    Cu.import("resource://gre/modules/AppsUtils.jsm");
  1.7144 +    DOMApplicationRegistry.allAppsLaunchable = true;
  1.7145 +
  1.7146 +    Services.obs.addObserver(this, "webapps-ask-install", false);
  1.7147 +    Services.obs.addObserver(this, "webapps-launch", false);
  1.7148 +    Services.obs.addObserver(this, "webapps-uninstall", false);
  1.7149 +    Services.obs.addObserver(this, "webapps-install-error", false);
  1.7150 +  },
  1.7151 +
  1.7152 +  uninit: function unint() {
  1.7153 +    Services.obs.removeObserver(this, "webapps-ask-install");
  1.7154 +    Services.obs.removeObserver(this, "webapps-launch");
  1.7155 +    Services.obs.removeObserver(this, "webapps-uninstall");
  1.7156 +    Services.obs.removeObserver(this, "webapps-install-error");
  1.7157 +  },
  1.7158 +
  1.7159 +  DEFAULT_ICON: "chrome://browser/skin/images/default-app-icon.png",
  1.7160 +  DEFAULT_PREFS_FILENAME: "default-prefs.js",
  1.7161 +
  1.7162 +  observe: function observe(aSubject, aTopic, aData) {
  1.7163 +    let data = {};
  1.7164 +    try {
  1.7165 +      data = JSON.parse(aData);
  1.7166 +      data.mm = aSubject;
  1.7167 +    } catch(ex) { }
  1.7168 +    switch (aTopic) {
  1.7169 +      case "webapps-install-error":
  1.7170 +        let msg = "";
  1.7171 +        switch (aData) {
  1.7172 +          case "INVALID_MANIFEST":
  1.7173 +          case "MANIFEST_PARSE_ERROR":
  1.7174 +            msg = Strings.browser.GetStringFromName("webapps.manifestInstallError");
  1.7175 +            break;
  1.7176 +          case "NETWORK_ERROR":
  1.7177 +          case "MANIFEST_URL_ERROR":
  1.7178 +            msg = Strings.browser.GetStringFromName("webapps.networkInstallError");
  1.7179 +            break;
  1.7180 +          default:
  1.7181 +            msg = Strings.browser.GetStringFromName("webapps.installError");
  1.7182 +        }
  1.7183 +        NativeWindow.toast.show(msg, "short");
  1.7184 +        console.log("Error installing app: " + aData);
  1.7185 +        break;
  1.7186 +      case "webapps-ask-install":
  1.7187 +        this.doInstall(data);
  1.7188 +        break;
  1.7189 +      case "webapps-launch":
  1.7190 +        this.openURL(data.manifestURL, data.origin);
  1.7191 +        break;
  1.7192 +      case "webapps-uninstall":
  1.7193 +        sendMessageToJava({
  1.7194 +          type: "Webapps:Uninstall",
  1.7195 +          origin: data.origin
  1.7196 +        });
  1.7197 +        break;
  1.7198 +    }
  1.7199 +  },
  1.7200 +
  1.7201 +  doInstall: function doInstall(aData) {
  1.7202 +    let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
  1.7203 +    let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
  1.7204 +
  1.7205 +    if (Services.prompt.confirm(null, Strings.browser.GetStringFromName("webapps.installTitle"), manifest.name + "\n" + aData.app.origin)) {
  1.7206 +      // Get a profile for the app to be installed in. We'll download everything before creating the icons.
  1.7207 +      let origin = aData.app.origin;
  1.7208 +      sendMessageToJava({
  1.7209 +         type: "Webapps:Preinstall",
  1.7210 +         name: manifest.name,
  1.7211 +         manifestURL: aData.app.manifestURL,
  1.7212 +         origin: origin
  1.7213 +      }, (data) => {
  1.7214 +        let profilePath = data.profile;
  1.7215 +        if (!profilePath)
  1.7216 +          return;
  1.7217 +
  1.7218 +        let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
  1.7219 +        file.initWithPath(profilePath);
  1.7220 +
  1.7221 +        let self = this;
  1.7222 +        DOMApplicationRegistry.confirmInstall(aData, file,
  1.7223 +          function (aManifest) {
  1.7224 +            let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
  1.7225 +
  1.7226 +            // the manifest argument is the manifest from within the zip file,
  1.7227 +            // TODO so now would be a good time to ask about permissions.
  1.7228 +            self.makeBase64Icon(localeManifest.biggestIconURL || this.DEFAULT_ICON,
  1.7229 +              function(scaledIcon, fullsizeIcon) {
  1.7230 +                // if java returned a profile path to us, try to use it to pre-populate the app cache
  1.7231 +                // also save the icon so that it can be used in the splash screen
  1.7232 +                try {
  1.7233 +                  let iconFile = file.clone();
  1.7234 +                  iconFile.append("logo.png");
  1.7235 +                  let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
  1.7236 +                  persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
  1.7237 +                  persist.persistFlags |= Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
  1.7238 +
  1.7239 +                  let source = Services.io.newURI(fullsizeIcon, "UTF8", null);
  1.7240 +                  persist.saveURI(source, null, null, null, null, iconFile, null);
  1.7241 +
  1.7242 +                  // aData.app.origin may now point to the app: url that hosts this app
  1.7243 +                  sendMessageToJava({
  1.7244 +                    type: "Webapps:Postinstall",
  1.7245 +                    name: localeManifest.name,
  1.7246 +                    manifestURL: aData.app.manifestURL,
  1.7247 +                    originalOrigin: origin,
  1.7248 +                    origin: aData.app.origin,
  1.7249 +                    iconURL: fullsizeIcon
  1.7250 +                  });
  1.7251 +                  if (!!aData.isPackage) {
  1.7252 +                    // For packaged apps, put a notification in the notification bar.
  1.7253 +                    let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
  1.7254 +                    let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
  1.7255 +                    alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", {
  1.7256 +                      observe: function () {
  1.7257 +                        self.openURL(aData.app.manifestURL, aData.app.origin);
  1.7258 +                      }
  1.7259 +                    }, "webapp");
  1.7260 +                  }
  1.7261 +
  1.7262 +                  // Create a system notification allowing the user to launch the app
  1.7263 +                  let observer = {
  1.7264 +                    observe: function (aSubject, aTopic) {
  1.7265 +                      if (aTopic == "alertclickcallback") {
  1.7266 +                        WebappsUI.openURL(aData.app.manifestURL, origin);
  1.7267 +                      }
  1.7268 +                    }
  1.7269 +                  };
  1.7270 +
  1.7271 +                  let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
  1.7272 +                  let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
  1.7273 +                  alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", observer, "webapp");
  1.7274 +                } catch(ex) {
  1.7275 +                  console.log(ex);
  1.7276 +                }
  1.7277 +                self.writeDefaultPrefs(file, localeManifest);
  1.7278 +              }
  1.7279 +            );
  1.7280 +          }
  1.7281 +        );
  1.7282 +      });
  1.7283 +    } else {
  1.7284 +      DOMApplicationRegistry.denyInstall(aData);
  1.7285 +    }
  1.7286 +  },
  1.7287 +
  1.7288 +  writeDefaultPrefs: function webapps_writeDefaultPrefs(aProfile, aManifest) {
  1.7289 +      // build any app specific default prefs
  1.7290 +      let prefs = [];
  1.7291 +      if (aManifest.orientation) {
  1.7292 +        prefs.push({name:"app.orientation.default", value: aManifest.orientation.join(",") });
  1.7293 +      }
  1.7294 +
  1.7295 +      // write them into the app profile
  1.7296 +      let defaultPrefsFile = aProfile.clone();
  1.7297 +      defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME);
  1.7298 +      this._writeData(defaultPrefsFile, prefs);
  1.7299 +  },
  1.7300 +
  1.7301 +  _writeData: function(aFile, aPrefs) {
  1.7302 +    if (aPrefs.length > 0) {
  1.7303 +      let array = new TextEncoder().encode(JSON.stringify(aPrefs));
  1.7304 +      OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) {
  1.7305 +        console.log("Error writing default prefs: " + reason);
  1.7306 +      });
  1.7307 +    }
  1.7308 +  },
  1.7309 +
  1.7310 +  openURL: function openURL(aManifestURL, aOrigin) {
  1.7311 +    sendMessageToJava({
  1.7312 +      type: "Webapps:Open",
  1.7313 +      manifestURL: aManifestURL,
  1.7314 +      origin: aOrigin
  1.7315 +    });
  1.7316 +  },
  1.7317 +
  1.7318 +  get iconSize() {
  1.7319 +    let iconSize = 64;
  1.7320 +    try {
  1.7321 +      let jni = new JNI();
  1.7322 +      let cls = jni.findClass("org/mozilla/gecko/GeckoAppShell");
  1.7323 +      let method = jni.getStaticMethodID(cls, "getPreferredIconSize", "()I");
  1.7324 +      iconSize = jni.callStaticIntMethod(cls, method);
  1.7325 +      jni.close();
  1.7326 +    } catch(ex) {
  1.7327 +      console.log(ex);
  1.7328 +    }
  1.7329 +
  1.7330 +    delete this.iconSize;
  1.7331 +    return this.iconSize = iconSize;
  1.7332 +  },
  1.7333 +
  1.7334 +  makeBase64Icon: function loadAndMakeBase64Icon(aIconURL, aCallbackFunction) {
  1.7335 +    let size = this.iconSize;
  1.7336 +
  1.7337 +    let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
  1.7338 +    canvas.width = canvas.height = size;
  1.7339 +    let ctx = canvas.getContext("2d");
  1.7340 +    let favicon = new Image();
  1.7341 +    favicon.onload = function() {
  1.7342 +      ctx.drawImage(favicon, 0, 0, size, size);
  1.7343 +      let scaledIcon = canvas.toDataURL("image/png", "");
  1.7344 +
  1.7345 +      canvas.width = favicon.width;
  1.7346 +      canvas.height = favicon.height;
  1.7347 +      ctx.drawImage(favicon, 0, 0, favicon.width, favicon.height);
  1.7348 +      let fullsizeIcon = canvas.toDataURL("image/png", "");
  1.7349 +
  1.7350 +      canvas = null;
  1.7351 +      aCallbackFunction.call(null, scaledIcon, fullsizeIcon);
  1.7352 +    };
  1.7353 +    favicon.onerror = function() {
  1.7354 +      Cu.reportError("CreateShortcut: favicon image load error");
  1.7355 +
  1.7356 +      // if the image failed to load, and it was not our default icon, attempt to
  1.7357 +      // use our default as a fallback
  1.7358 +      if (favicon.src != WebappsUI.DEFAULT_ICON) {
  1.7359 +        favicon.src = WebappsUI.DEFAULT_ICON;
  1.7360 +      }
  1.7361 +    };
  1.7362 +  
  1.7363 +    favicon.src = aIconURL;
  1.7364 +  },
  1.7365 +
  1.7366 +  createShortcut: function createShortcut(aTitle, aURL, aIconURL, aType) {
  1.7367 +    this.makeBase64Icon(aIconURL, function _createShortcut(icon) {
  1.7368 +      try {
  1.7369 +        let shell = Cc["@mozilla.org/browser/shell-service;1"].createInstance(Ci.nsIShellService);
  1.7370 +        shell.createShortcut(aTitle, aURL, icon, aType);
  1.7371 +      } catch(e) {
  1.7372 +        Cu.reportError(e);
  1.7373 +      }
  1.7374 +    });
  1.7375 +  }
  1.7376 +}
  1.7377 +#endif
  1.7378 +
  1.7379 +var RemoteDebugger = {
  1.7380 +  init: function rd_init() {
  1.7381 +    Services.prefs.addObserver("devtools.debugger.", this, false);
  1.7382 +
  1.7383 +    if (this._isEnabled())
  1.7384 +      this._start();
  1.7385 +  },
  1.7386 +
  1.7387 +  observe: function rd_observe(aSubject, aTopic, aData) {
  1.7388 +    if (aTopic != "nsPref:changed")
  1.7389 +      return;
  1.7390 +
  1.7391 +    switch (aData) {
  1.7392 +      case "devtools.debugger.remote-enabled":
  1.7393 +        if (this._isEnabled())
  1.7394 +          this._start();
  1.7395 +        else
  1.7396 +          this._stop();
  1.7397 +        break;
  1.7398 +
  1.7399 +      case "devtools.debugger.remote-port":
  1.7400 +        if (this._isEnabled())
  1.7401 +          this._restart();
  1.7402 +        break;
  1.7403 +    }
  1.7404 +  },
  1.7405 +
  1.7406 +  uninit: function rd_uninit() {
  1.7407 +    Services.prefs.removeObserver("devtools.debugger.", this);
  1.7408 +    this._stop();
  1.7409 +  },
  1.7410 +
  1.7411 +  _getPort: function _rd_getPort() {
  1.7412 +    return Services.prefs.getIntPref("devtools.debugger.remote-port");
  1.7413 +  },
  1.7414 +
  1.7415 +  _isEnabled: function rd_isEnabled() {
  1.7416 +    return Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
  1.7417 +  },
  1.7418 +
  1.7419 +  /**
  1.7420 +   * Prompt the user to accept or decline the incoming connection.
  1.7421 +   * This is passed to DebuggerService.init as a callback.
  1.7422 +   *
  1.7423 +   * @return true if the connection should be permitted, false otherwise
  1.7424 +   */
  1.7425 +  _showConnectionPrompt: function rd_showConnectionPrompt() {
  1.7426 +    let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
  1.7427 +    let msg = Strings.browser.GetStringFromName("remoteIncomingPromptMessage");
  1.7428 +    let disable = Strings.browser.GetStringFromName("remoteIncomingPromptDisable");
  1.7429 +    let cancel = Strings.browser.GetStringFromName("remoteIncomingPromptCancel");
  1.7430 +    let agree = Strings.browser.GetStringFromName("remoteIncomingPromptAccept");
  1.7431 +
  1.7432 +    // Make prompt. Note: button order is in reverse.
  1.7433 +    let prompt = new Prompt({
  1.7434 +      window: null,
  1.7435 +      hint: "remotedebug",
  1.7436 +      title: title,
  1.7437 +      message: msg,
  1.7438 +      buttons: [ agree, cancel, disable ],
  1.7439 +      priority: 1
  1.7440 +    });
  1.7441 +
  1.7442 +    // The debugger server expects a synchronous response, so spin on result since Prompt is async.
  1.7443 +    let result = null;
  1.7444 +
  1.7445 +    prompt.show(function(data) {
  1.7446 +      result = data.button;
  1.7447 +    });
  1.7448 +
  1.7449 +    // Spin this thread while we wait for a result.
  1.7450 +    let thread = Services.tm.currentThread;
  1.7451 +    while (result == null)
  1.7452 +      thread.processNextEvent(true);
  1.7453 +
  1.7454 +    if (result === 0)
  1.7455 +      return true;
  1.7456 +    if (result === 2) {
  1.7457 +      Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
  1.7458 +      this._stop();
  1.7459 +    }
  1.7460 +    return false;
  1.7461 +  },
  1.7462 +
  1.7463 +  _restart: function rd_restart() {
  1.7464 +    this._stop();
  1.7465 +    this._start();
  1.7466 +  },
  1.7467 +
  1.7468 +  _start: function rd_start() {
  1.7469 +    try {
  1.7470 +      if (!DebuggerServer.initialized) {
  1.7471 +        DebuggerServer.init(this._showConnectionPrompt.bind(this));
  1.7472 +        DebuggerServer.addBrowserActors();
  1.7473 +        DebuggerServer.addActors("chrome://browser/content/dbg-browser-actors.js");
  1.7474 +      }
  1.7475 +
  1.7476 +      let port = this._getPort();
  1.7477 +      DebuggerServer.openListener(port);
  1.7478 +      dump("Remote debugger listening on port " + port);
  1.7479 +    } catch(e) {
  1.7480 +      dump("Remote debugger didn't start: " + e);
  1.7481 +    }
  1.7482 +  },
  1.7483 +
  1.7484 +  _stop: function rd_start() {
  1.7485 +    DebuggerServer.closeListener();
  1.7486 +    dump("Remote debugger stopped");
  1.7487 +  }
  1.7488 +};
  1.7489 +
  1.7490 +var Telemetry = {
  1.7491 +  addData: function addData(aHistogramId, aValue) {
  1.7492 +    let histogram = Services.telemetry.getHistogramById(aHistogramId);
  1.7493 +    histogram.add(aValue);
  1.7494 +  },
  1.7495 +};
  1.7496 +
  1.7497 +let Reader = {
  1.7498 +  // Version of the cache database schema
  1.7499 +  DB_VERSION: 1,
  1.7500 +
  1.7501 +  DEBUG: 0,
  1.7502 +
  1.7503 +  READER_ADD_SUCCESS: 0,
  1.7504 +  READER_ADD_FAILED: 1,
  1.7505 +  READER_ADD_DUPLICATE: 2,
  1.7506 +
  1.7507 +  // Don't try to parse the page if it has too many elements (for memory and
  1.7508 +  // performance reasons)
  1.7509 +  MAX_ELEMS_TO_PARSE: 3000,
  1.7510 +
  1.7511 +  isEnabledForParseOnLoad: false,
  1.7512 +
  1.7513 +  init: function Reader_init() {
  1.7514 +    this.log("Init()");
  1.7515 +    this._requests = {};
  1.7516 +
  1.7517 +    this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
  1.7518 +
  1.7519 +    Services.obs.addObserver(this, "Reader:Add", false);
  1.7520 +    Services.obs.addObserver(this, "Reader:Remove", false);
  1.7521 +
  1.7522 +    Services.prefs.addObserver("reader.parse-on-load.", this, false);
  1.7523 +  },
  1.7524 +
  1.7525 +  pageAction: {
  1.7526 +    readerModeCallback: function(){
  1.7527 +      sendMessageToJava({
  1.7528 +        type: "Reader:Click",
  1.7529 +      });
  1.7530 +    },
  1.7531 +
  1.7532 +    readerModeActiveCallback: function(){
  1.7533 +      sendMessageToJava({
  1.7534 +        type: "Reader:LongClick",
  1.7535 +      });
  1.7536 +
  1.7537 +      // Create a relative timestamp for telemetry
  1.7538 +      let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized;
  1.7539 +      UITelemetry.addEvent("save.1", "pageaction", uptime, "reader");
  1.7540 +    },
  1.7541 +  },
  1.7542 +
  1.7543 +  updatePageAction: function(tab) {
  1.7544 +    if (this.pageAction.id) {
  1.7545 +      NativeWindow.pageactions.remove(this.pageAction.id);
  1.7546 +      delete this.pageAction.id;
  1.7547 +    }
  1.7548 +
  1.7549 +    // Create a relative timestamp for telemetry
  1.7550 +    let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized;
  1.7551 +
  1.7552 +    if (tab.readerActive) {
  1.7553 +      this.pageAction.id = NativeWindow.pageactions.add({
  1.7554 +        title: Strings.browser.GetStringFromName("readerMode.exit"),
  1.7555 +        icon: "drawable://reader_active",
  1.7556 +        clickCallback: this.pageAction.readerModeCallback,
  1.7557 +        important: true
  1.7558 +      });
  1.7559 +
  1.7560 +      // Only start a reader session if the viewer is in the foreground. We do
  1.7561 +      // not track background reader viewers.
  1.7562 +      UITelemetry.startSession("reader.1", uptime);
  1.7563 +      return;
  1.7564 +    }
  1.7565 +
  1.7566 +    // Only stop a reader session if the foreground viewer is not visible.
  1.7567 +    UITelemetry.stopSession("reader.1", "", uptime);
  1.7568 +
  1.7569 +    if (tab.readerEnabled) {
  1.7570 +      this.pageAction.id = NativeWindow.pageactions.add({
  1.7571 +        title: Strings.browser.GetStringFromName("readerMode.enter"),
  1.7572 +        icon: "drawable://reader",
  1.7573 +        clickCallback:this.pageAction.readerModeCallback,
  1.7574 +        longClickCallback: this.pageAction.readerModeActiveCallback,
  1.7575 +        important: true
  1.7576 +      });
  1.7577 +    }
  1.7578 +  },
  1.7579 +
  1.7580 +  observe: function(aMessage, aTopic, aData) {
  1.7581 +    switch(aTopic) {
  1.7582 +      case "Reader:Add": {
  1.7583 +        let args = JSON.parse(aData);
  1.7584 +        if ('fromAboutReader' in args) {
  1.7585 +          // Ignore adds initiated from aboutReader menu banner
  1.7586 +          break;
  1.7587 +        }
  1.7588 +
  1.7589 +        let tabID = null;
  1.7590 +        let url, urlWithoutRef;
  1.7591 +
  1.7592 +        if ('tabID' in args) {
  1.7593 +          tabID = args.tabID;
  1.7594 +
  1.7595 +          let tab = BrowserApp.getTabForId(tabID);
  1.7596 +          let currentURI = tab.browser.currentURI;
  1.7597 +
  1.7598 +          url = currentURI.spec;
  1.7599 +          urlWithoutRef = currentURI.specIgnoringRef;
  1.7600 +        } else if ('url' in args) {
  1.7601 +          let uri = Services.io.newURI(args.url, null, null);
  1.7602 +          url = uri.spec;
  1.7603 +          urlWithoutRef = uri.specIgnoringRef;
  1.7604 +        } else {
  1.7605 +          throw new Error("Reader:Add requires a tabID or an URL as argument");
  1.7606 +        }
  1.7607 +
  1.7608 +        let sendResult = function(result, article) {
  1.7609 +          article = article || {};
  1.7610 +          this.log("Reader:Add success=" + result + ", url=" + url + ", title=" + article.title + ", excerpt=" + article.excerpt);
  1.7611 +
  1.7612 +          sendMessageToJava({
  1.7613 +            type: "Reader:Added",
  1.7614 +            result: result,
  1.7615 +            title: article.title,
  1.7616 +            url: url,
  1.7617 +            length: article.length,
  1.7618 +            excerpt: article.excerpt
  1.7619 +          });
  1.7620 +        }.bind(this);
  1.7621 +
  1.7622 +        let handleArticle = function(article) {
  1.7623 +          if (!article) {
  1.7624 +            sendResult(this.READER_ADD_FAILED, null);
  1.7625 +            return;
  1.7626 +          }
  1.7627 +
  1.7628 +          this.storeArticleInCache(article, function(success) {
  1.7629 +            let result = (success ? this.READER_ADD_SUCCESS : this.READER_ADD_FAILED);
  1.7630 +            sendResult(result, article);
  1.7631 +          }.bind(this));
  1.7632 +        }.bind(this);
  1.7633 +
  1.7634 +        this.getArticleFromCache(urlWithoutRef, function (article) {
  1.7635 +          // If the article is already in reading list, bail
  1.7636 +          if (article) {
  1.7637 +            sendResult(this.READER_ADD_DUPLICATE, null);
  1.7638 +            return;
  1.7639 +          }
  1.7640 +
  1.7641 +          if (tabID != null) {
  1.7642 +            this.getArticleForTab(tabID, urlWithoutRef, handleArticle);
  1.7643 +          } else {
  1.7644 +            this.parseDocumentFromURL(urlWithoutRef, handleArticle);
  1.7645 +          }
  1.7646 +        }.bind(this));
  1.7647 +        break;
  1.7648 +      }
  1.7649 +
  1.7650 +      case "Reader:Remove": {
  1.7651 +        let url = aData;
  1.7652 +        this.removeArticleFromCache(url, function(success) {
  1.7653 +          this.log("Reader:Remove success=" + success + ", url=" + url);
  1.7654 +
  1.7655 +          if (success) {
  1.7656 +            sendMessageToJava({
  1.7657 +              type: "Reader:Removed",
  1.7658 +              url: url
  1.7659 +            });
  1.7660 +          }
  1.7661 +        }.bind(this));
  1.7662 +        break;
  1.7663 +      }
  1.7664 +
  1.7665 +      case "nsPref:changed": {
  1.7666 +        if (aData.startsWith("reader.parse-on-load.")) {
  1.7667 +          this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
  1.7668 +        }
  1.7669 +        break;
  1.7670 +      }
  1.7671 +    }
  1.7672 +  },
  1.7673 +
  1.7674 +  getStateForParseOnLoad: function Reader_getStateForParseOnLoad() {
  1.7675 +    let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
  1.7676 +    let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
  1.7677 +    // For low-memory devices, don't allow reader mode since it takes up a lot of memory.
  1.7678 +    // See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
  1.7679 +    return isForceEnabled || (isEnabled && !BrowserApp.isOnLowMemoryPlatform);
  1.7680 +  },
  1.7681 +
  1.7682 +  parseDocumentFromURL: function Reader_parseDocumentFromURL(url, callback) {
  1.7683 +    // If there's an on-going request for the same URL, simply append one
  1.7684 +    // more callback to it to be called when the request is done.
  1.7685 +    if (url in this._requests) {
  1.7686 +      let request = this._requests[url];
  1.7687 +      request.callbacks.push(callback);
  1.7688 +      return;
  1.7689 +    }
  1.7690 +
  1.7691 +    let request = { url: url, callbacks: [callback] };
  1.7692 +    this._requests[url] = request;
  1.7693 +
  1.7694 +    try {
  1.7695 +      this.log("parseDocumentFromURL: " + url);
  1.7696 +
  1.7697 +      // First, try to find a cached parsed article in the DB
  1.7698 +      this.getArticleFromCache(url, function(article) {
  1.7699 +        if (article) {
  1.7700 +          this.log("Page found in cache, return article immediately");
  1.7701 +          this._runCallbacksAndFinish(request, article);
  1.7702 +          return;
  1.7703 +        }
  1.7704 +
  1.7705 +        if (!this._requests) {
  1.7706 +          this.log("Reader has been destroyed, abort");
  1.7707 +          return;
  1.7708 +        }
  1.7709 +
  1.7710 +        // Article hasn't been found in the cache DB, we need to
  1.7711 +        // download the page and parse the article out of it.
  1.7712 +        this._downloadAndParseDocument(url, request);
  1.7713 +      }.bind(this));
  1.7714 +    } catch (e) {
  1.7715 +      this.log("Error parsing document from URL: " + e);
  1.7716 +      this._runCallbacksAndFinish(request, null);
  1.7717 +    }
  1.7718 +  },
  1.7719 +
  1.7720 +  getArticleForTab: function Reader_getArticleForTab(tabId, url, callback) {
  1.7721 +    let tab = BrowserApp.getTabForId(tabId);
  1.7722 +    if (tab) {
  1.7723 +      let article = tab.savedArticle;
  1.7724 +      if (article && article.url == url) {
  1.7725 +        this.log("Saved article found in tab");
  1.7726 +        callback(article);
  1.7727 +        return;
  1.7728 +      }
  1.7729 +    }
  1.7730 +
  1.7731 +    this.parseDocumentFromURL(url, callback);
  1.7732 +  },
  1.7733 +
  1.7734 +  parseDocumentFromTab: function(tabId, callback) {
  1.7735 +    try {
  1.7736 +      this.log("parseDocumentFromTab: " + tabId);
  1.7737 +
  1.7738 +      let tab = BrowserApp.getTabForId(tabId);
  1.7739 +      let url = tab.browser.contentWindow.location.href;
  1.7740 +      let uri = Services.io.newURI(url, null, null);
  1.7741 +
  1.7742 +      if (!this._shouldCheckUri(uri)) {
  1.7743 +        callback(null);
  1.7744 +        return;
  1.7745 +      }
  1.7746 +
  1.7747 +      // First, try to find a cached parsed article in the DB
  1.7748 +      this.getArticleFromCache(url, function(article) {
  1.7749 +        if (article) {
  1.7750 +          this.log("Page found in cache, return article immediately");
  1.7751 +          callback(article);
  1.7752 +          return;
  1.7753 +        }
  1.7754 +
  1.7755 +        let doc = tab.browser.contentWindow.document;
  1.7756 +        this._readerParse(uri, doc, function (article) {
  1.7757 +          if (!article) {
  1.7758 +            this.log("Failed to parse page");
  1.7759 +            callback(null);
  1.7760 +            return;
  1.7761 +          }
  1.7762 +
  1.7763 +          callback(article);
  1.7764 +        }.bind(this));
  1.7765 +      }.bind(this));
  1.7766 +    } catch (e) {
  1.7767 +      this.log("Error parsing document from tab: " + e);
  1.7768 +      callback(null);
  1.7769 +    }
  1.7770 +  },
  1.7771 +
  1.7772 +  getArticleFromCache: function Reader_getArticleFromCache(url, callback) {
  1.7773 +    this._getCacheDB(function(cacheDB) {
  1.7774 +      if (!cacheDB) {
  1.7775 +        callback(false);
  1.7776 +        return;
  1.7777 +      }
  1.7778 +
  1.7779 +      let transaction = cacheDB.transaction(cacheDB.objectStoreNames);
  1.7780 +      let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
  1.7781 +
  1.7782 +      let request = articles.get(url);
  1.7783 +
  1.7784 +      request.onerror = function(event) {
  1.7785 +        this.log("Error getting article from the cache DB: " + url);
  1.7786 +        callback(null);
  1.7787 +      }.bind(this);
  1.7788 +
  1.7789 +      request.onsuccess = function(event) {
  1.7790 +        this.log("Got article from the cache DB: " + event.target.result);
  1.7791 +        callback(event.target.result);
  1.7792 +      }.bind(this);
  1.7793 +    }.bind(this));
  1.7794 +  },
  1.7795 +
  1.7796 +  storeArticleInCache: function Reader_storeArticleInCache(article, callback) {
  1.7797 +    this._getCacheDB(function(cacheDB) {
  1.7798 +      if (!cacheDB) {
  1.7799 +        callback(false);
  1.7800 +        return;
  1.7801 +      }
  1.7802 +
  1.7803 +      let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
  1.7804 +      let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
  1.7805 +
  1.7806 +      let request = articles.add(article);
  1.7807 +
  1.7808 +      request.onerror = function(event) {
  1.7809 +        this.log("Error storing article in the cache DB: " + article.url);
  1.7810 +        callback(false);
  1.7811 +      }.bind(this);
  1.7812 +
  1.7813 +      request.onsuccess = function(event) {
  1.7814 +        this.log("Stored article in the cache DB: " + article.url);
  1.7815 +        callback(true);
  1.7816 +      }.bind(this);
  1.7817 +    }.bind(this));
  1.7818 +  },
  1.7819 +
  1.7820 +  removeArticleFromCache: function Reader_removeArticleFromCache(url, callback) {
  1.7821 +    this._getCacheDB(function(cacheDB) {
  1.7822 +      if (!cacheDB) {
  1.7823 +        callback(false);
  1.7824 +        return;
  1.7825 +      }
  1.7826 +
  1.7827 +      let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
  1.7828 +      let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
  1.7829 +
  1.7830 +      let request = articles.delete(url);
  1.7831 +
  1.7832 +      request.onerror = function(event) {
  1.7833 +        this.log("Error removing article from the cache DB: " + url);
  1.7834 +        callback(false);
  1.7835 +      }.bind(this);
  1.7836 +
  1.7837 +      request.onsuccess = function(event) {
  1.7838 +        this.log("Removed article from the cache DB: " + url);
  1.7839 +        callback(true);
  1.7840 +      }.bind(this);
  1.7841 +    }.bind(this));
  1.7842 +  },
  1.7843 +
  1.7844 +  uninit: function Reader_uninit() {
  1.7845 +    Services.prefs.removeObserver("reader.parse-on-load.", this);
  1.7846 +
  1.7847 +    Services.obs.removeObserver(this, "Reader:Add");
  1.7848 +    Services.obs.removeObserver(this, "Reader:Remove");
  1.7849 +
  1.7850 +    let requests = this._requests;
  1.7851 +    for (let url in requests) {
  1.7852 +      let request = requests[url];
  1.7853 +      if (request.browser) {
  1.7854 +        let browser = request.browser;
  1.7855 +        browser.parentNode.removeChild(browser);
  1.7856 +      }
  1.7857 +    }
  1.7858 +    delete this._requests;
  1.7859 +
  1.7860 +    if (this._cacheDB) {
  1.7861 +      this._cacheDB.close();
  1.7862 +      delete this._cacheDB;
  1.7863 +    }
  1.7864 +  },
  1.7865 +
  1.7866 +  log: function(msg) {
  1.7867 +    if (this.DEBUG)
  1.7868 +      dump("Reader: " + msg);
  1.7869 +  },
  1.7870 +
  1.7871 +  _shouldCheckUri: function Reader_shouldCheckUri(uri) {
  1.7872 +    if ((uri.prePath + "/") === uri.spec) {
  1.7873 +      this.log("Not parsing home page: " + uri.spec);
  1.7874 +      return false;
  1.7875 +    }
  1.7876 +
  1.7877 +    if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
  1.7878 +      this.log("Not parsing URI scheme: " + uri.scheme);
  1.7879 +      return false;
  1.7880 +    }
  1.7881 +
  1.7882 +    return true;
  1.7883 +  },
  1.7884 +
  1.7885 +  _readerParse: function Reader_readerParse(uri, doc, callback) {
  1.7886 +    let numTags = doc.getElementsByTagName("*").length;
  1.7887 +    if (numTags > this.MAX_ELEMS_TO_PARSE) {
  1.7888 +      this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found");
  1.7889 +      callback(null);
  1.7890 +      return;
  1.7891 +    }
  1.7892 +
  1.7893 +    let worker = new ChromeWorker("readerWorker.js");
  1.7894 +    worker.onmessage = function (evt) {
  1.7895 +      let article = evt.data;
  1.7896 +
  1.7897 +      // Append URL to the article data. specIgnoringRef will ignore any hash
  1.7898 +      // in the URL.
  1.7899 +      if (article) {
  1.7900 +        article.url = uri.specIgnoringRef;
  1.7901 +        let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
  1.7902 +        article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
  1.7903 +                                                        .convertToPlainText(article.title, flags, 0);
  1.7904 +      }
  1.7905 +
  1.7906 +      callback(article);
  1.7907 +    };
  1.7908 +
  1.7909 +    try {
  1.7910 +      worker.postMessage({
  1.7911 +        uri: {
  1.7912 +          spec: uri.spec,
  1.7913 +          host: uri.host,
  1.7914 +          prePath: uri.prePath,
  1.7915 +          scheme: uri.scheme,
  1.7916 +          pathBase: Services.io.newURI(".", null, uri).spec
  1.7917 +        },
  1.7918 +        doc: new XMLSerializer().serializeToString(doc)
  1.7919 +      });
  1.7920 +    } catch (e) {
  1.7921 +      dump("Reader: could not build Readability arguments: " + e);
  1.7922 +      callback(null);
  1.7923 +    }
  1.7924 +  },
  1.7925 +
  1.7926 +  _runCallbacksAndFinish: function Reader_runCallbacksAndFinish(request, result) {
  1.7927 +    delete this._requests[request.url];
  1.7928 +
  1.7929 +    request.callbacks.forEach(function(callback) {
  1.7930 +      callback(result);
  1.7931 +    });
  1.7932 +  },
  1.7933 +
  1.7934 +  _downloadDocument: function Reader_downloadDocument(url, callback) {
  1.7935 +    // We want to parse those arbitrary pages safely, outside the privileged
  1.7936 +    // context of chrome. We create a hidden browser element to fetch the
  1.7937 +    // loaded page's document object then discard the browser element.
  1.7938 +
  1.7939 +    let browser = document.createElement("browser");
  1.7940 +    browser.setAttribute("type", "content");
  1.7941 +    browser.setAttribute("collapsed", "true");
  1.7942 +    browser.setAttribute("disablehistory", "true");
  1.7943 +
  1.7944 +    document.documentElement.appendChild(browser);
  1.7945 +    browser.stop();
  1.7946 +
  1.7947 +    browser.webNavigation.allowAuth = false;
  1.7948 +    browser.webNavigation.allowImages = false;
  1.7949 +    browser.webNavigation.allowJavascript = false;
  1.7950 +    browser.webNavigation.allowMetaRedirects = true;
  1.7951 +    browser.webNavigation.allowPlugins = false;
  1.7952 +
  1.7953 +    browser.addEventListener("DOMContentLoaded", function (event) {
  1.7954 +      let doc = event.originalTarget;
  1.7955 +
  1.7956 +      // ignore on frames and other documents
  1.7957 +      if (doc != browser.contentDocument)
  1.7958 +        return;
  1.7959 +
  1.7960 +      this.log("Done loading: " + doc);
  1.7961 +      if (doc.location.href == "about:blank") {
  1.7962 +        callback(null);
  1.7963 +
  1.7964 +        // Request has finished with error, remove browser element
  1.7965 +        browser.parentNode.removeChild(browser);
  1.7966 +        return;
  1.7967 +      }
  1.7968 +
  1.7969 +      callback(doc);
  1.7970 +    }.bind(this));
  1.7971 +
  1.7972 +    browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
  1.7973 +                             null, null, null);
  1.7974 +
  1.7975 +    return browser;
  1.7976 +  },
  1.7977 +
  1.7978 +  _downloadAndParseDocument: function Reader_downloadAndParseDocument(url, request) {
  1.7979 +    try {
  1.7980 +      this.log("Needs to fetch page, creating request: " + url);
  1.7981 +
  1.7982 +      request.browser = this._downloadDocument(url, function(doc) {
  1.7983 +        this.log("Finished loading page: " + doc);
  1.7984 +
  1.7985 +        if (!doc) {
  1.7986 +          this.log("Error loading page");
  1.7987 +          this._runCallbacksAndFinish(request, null);
  1.7988 +          return;
  1.7989 +        }
  1.7990 +
  1.7991 +        this.log("Parsing response with Readability");
  1.7992 +
  1.7993 +        let uri = Services.io.newURI(url, null, null);
  1.7994 +        this._readerParse(uri, doc, function (article) {
  1.7995 +          // Delete reference to the browser element as we've finished parsing.
  1.7996 +          let browser = request.browser;
  1.7997 +          if (browser) {
  1.7998 +            browser.parentNode.removeChild(browser);
  1.7999 +            delete request.browser;
  1.8000 +          }
  1.8001 +
  1.8002 +          if (!article) {
  1.8003 +            this.log("Failed to parse page");
  1.8004 +            this._runCallbacksAndFinish(request, null);
  1.8005 +            return;
  1.8006 +          }
  1.8007 +
  1.8008 +          this.log("Parsing has been successful");
  1.8009 +
  1.8010 +          this._runCallbacksAndFinish(request, article);
  1.8011 +        }.bind(this));
  1.8012 +      }.bind(this));
  1.8013 +    } catch (e) {
  1.8014 +      this.log("Error downloading and parsing document: " + e);
  1.8015 +      this._runCallbacksAndFinish(request, null);
  1.8016 +    }
  1.8017 +  },
  1.8018 +
  1.8019 +  _getCacheDB: function Reader_getCacheDB(callback) {
  1.8020 +    if (this._cacheDB) {
  1.8021 +      callback(this._cacheDB);
  1.8022 +      return;
  1.8023 +    }
  1.8024 +
  1.8025 +    let request = window.indexedDB.open("about:reader", this.DB_VERSION);
  1.8026 +
  1.8027 +    request.onerror = function(event) {
  1.8028 +      this.log("Error connecting to the cache DB");
  1.8029 +      this._cacheDB = null;
  1.8030 +      callback(null);
  1.8031 +    }.bind(this);
  1.8032 +
  1.8033 +    request.onsuccess = function(event) {
  1.8034 +      this.log("Successfully connected to the cache DB");
  1.8035 +      this._cacheDB = event.target.result;
  1.8036 +      callback(this._cacheDB);
  1.8037 +    }.bind(this);
  1.8038 +
  1.8039 +    request.onupgradeneeded = function(event) {
  1.8040 +      this.log("Database schema upgrade from " +
  1.8041 +           event.oldVersion + " to " + event.newVersion);
  1.8042 +
  1.8043 +      let cacheDB = event.target.result;
  1.8044 +
  1.8045 +      // Create the articles object store
  1.8046 +      this.log("Creating articles object store");
  1.8047 +      cacheDB.createObjectStore("articles", { keyPath: "url" });
  1.8048 +
  1.8049 +      this.log("Database upgrade done: " + this.DB_VERSION);
  1.8050 +    }.bind(this);
  1.8051 +  }
  1.8052 +};
  1.8053 +
  1.8054 +var ExternalApps = {
  1.8055 +  _contextMenuId: null,
  1.8056 +
  1.8057 +  // extend _getLink to pickup html5 media links.
  1.8058 +  _getMediaLink: function(aElement) {
  1.8059 +    let uri = NativeWindow.contextmenus._getLink(aElement);
  1.8060 +    if (uri == null && aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement)) {
  1.8061 +      try {
  1.8062 +        let mediaSrc = aElement.currentSrc || aElement.src;
  1.8063 +        uri = ContentAreaUtils.makeURI(mediaSrc, null, null);
  1.8064 +      } catch (e) {}
  1.8065 +    }
  1.8066 +    return uri;
  1.8067 +  },
  1.8068 +
  1.8069 +  init: function helper_init() {
  1.8070 +    this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) {
  1.8071 +      let uri = null;
  1.8072 +      var node = aElement;
  1.8073 +      while (node && !uri) {
  1.8074 +        uri = ExternalApps._getMediaLink(node);
  1.8075 +        node = node.parentNode;
  1.8076 +      }
  1.8077 +      let apps = [];
  1.8078 +      if (uri)
  1.8079 +        apps = HelperApps.getAppsForUri(uri);
  1.8080 +
  1.8081 +      return apps.length == 1 ? Strings.browser.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1) :
  1.8082 +                                Strings.browser.GetStringFromName("helperapps.openWithList2");
  1.8083 +    }, this.filter, this.openExternal);
  1.8084 +  },
  1.8085 +
  1.8086 +  uninit: function helper_uninit() {
  1.8087 +    if (this._contextMenuId !== null) {
  1.8088 +      NativeWindow.contextmenus.remove(this._contextMenuId);
  1.8089 +    }
  1.8090 +    this._contextMenuId = null;
  1.8091 +  },
  1.8092 +
  1.8093 +  filter: {
  1.8094 +    matches: function(aElement) {
  1.8095 +      let uri = ExternalApps._getMediaLink(aElement);
  1.8096 +      let apps = [];
  1.8097 +      if (uri) {
  1.8098 +        apps = HelperApps.getAppsForUri(uri);
  1.8099 +      }
  1.8100 +      return apps.length > 0;
  1.8101 +    }
  1.8102 +  },
  1.8103 +
  1.8104 +  openExternal: function(aElement) {
  1.8105 +    let uri = ExternalApps._getMediaLink(aElement);
  1.8106 +    HelperApps.launchUri(uri);
  1.8107 +  },
  1.8108 +
  1.8109 +  shouldCheckUri: function(uri) {
  1.8110 +    if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
  1.8111 +      return false;
  1.8112 +    }
  1.8113 +
  1.8114 +    return true;
  1.8115 +  },
  1.8116 +
  1.8117 +  updatePageAction: function updatePageAction(uri) {
  1.8118 +    HelperApps.getAppsForUri(uri, { filterHttp: true }, (apps) => {
  1.8119 +      this.clearPageAction();
  1.8120 +      if (apps.length > 0)
  1.8121 +        this._setUriForPageAction(uri, apps);
  1.8122 +    });
  1.8123 +  },
  1.8124 +
  1.8125 +  updatePageActionUri: function updatePageActionUri(uri) {
  1.8126 +    this._pageActionUri = uri;
  1.8127 +  },
  1.8128 +
  1.8129 +  _setUriForPageAction: function setUriForPageAction(uri, apps) {
  1.8130 +    this.updatePageActionUri(uri);
  1.8131 +
  1.8132 +    // If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
  1.8133 +    if (this._pageActionId != undefined)
  1.8134 +      return;
  1.8135 +
  1.8136 +    this._pageActionId = NativeWindow.pageactions.add({
  1.8137 +      title: Strings.browser.GetStringFromName("openInApp.pageAction"),
  1.8138 +      icon: "drawable://icon_openinapp",
  1.8139 +
  1.8140 +      clickCallback: () => {
  1.8141 +        // Create a relative timestamp for telemetry
  1.8142 +        let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized;
  1.8143 +        UITelemetry.addEvent("launch.1", "pageaction", uptime, "helper");
  1.8144 +
  1.8145 +        if (apps.length > 1) {
  1.8146 +          // Use the HelperApps prompt here to filter out any Http handlers
  1.8147 +          HelperApps.prompt(apps, {
  1.8148 +            title: Strings.browser.GetStringFromName("openInApp.pageAction"),
  1.8149 +            buttons: [
  1.8150 +              Strings.browser.GetStringFromName("openInApp.ok"),
  1.8151 +              Strings.browser.GetStringFromName("openInApp.cancel")
  1.8152 +            ]
  1.8153 +          }, (result) => {
  1.8154 +            if (result.button != 0) {
  1.8155 +              return;
  1.8156 +            }
  1.8157 +            apps[result.icongrid0].launch(this._pageActionUri);
  1.8158 +          });
  1.8159 +        } else {
  1.8160 +          apps[0].launch(this._pageActionUri);
  1.8161 +        }
  1.8162 +      }
  1.8163 +    });
  1.8164 +  },
  1.8165 +
  1.8166 +  clearPageAction: function clearPageAction() {
  1.8167 +    if(!this._pageActionId)
  1.8168 +      return;
  1.8169 +
  1.8170 +    NativeWindow.pageactions.remove(this._pageActionId);
  1.8171 +    delete this._pageActionId;
  1.8172 +  },
  1.8173 +};
  1.8174 +
  1.8175 +var Distribution = {
  1.8176 +  // File used to store campaign data
  1.8177 +  _file: null,
  1.8178 +
  1.8179 +  init: function dc_init() {
  1.8180 +    Services.obs.addObserver(this, "Distribution:Set", false);
  1.8181 +    Services.obs.addObserver(this, "prefservice:after-app-defaults", false);
  1.8182 +    Services.obs.addObserver(this, "Campaign:Set", false);
  1.8183 +
  1.8184 +    // Look for file outside the APK:
  1.8185 +    // /data/data/org.mozilla.xxx/distribution.json
  1.8186 +    this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
  1.8187 +    this._file.append("distribution.json");
  1.8188 +    this.readJSON(this._file, this.update);
  1.8189 +  },
  1.8190 +
  1.8191 +  uninit: function dc_uninit() {
  1.8192 +    Services.obs.removeObserver(this, "Distribution:Set");
  1.8193 +    Services.obs.removeObserver(this, "prefservice:after-app-defaults");
  1.8194 +    Services.obs.removeObserver(this, "Campaign:Set");
  1.8195 +  },
  1.8196 +
  1.8197 +  observe: function dc_observe(aSubject, aTopic, aData) {
  1.8198 +    switch (aTopic) {
  1.8199 +      case "Distribution:Set":
  1.8200 +        // Reload the default prefs so we can observe "prefservice:after-app-defaults"
  1.8201 +        Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
  1.8202 +        break;
  1.8203 +
  1.8204 +      case "prefservice:after-app-defaults":
  1.8205 +        this.getPrefs();
  1.8206 +        break;
  1.8207 +
  1.8208 +      case "Campaign:Set": {
  1.8209 +        // Update the prefs for this session
  1.8210 +        try {
  1.8211 +          this.update(JSON.parse(aData));
  1.8212 +        } catch (ex) {
  1.8213 +          Cu.reportError("Distribution: Could not parse JSON: " + ex);
  1.8214 +          return;
  1.8215 +        }
  1.8216 +
  1.8217 +        // Asynchronously copy the data to the file.
  1.8218 +        let array = new TextEncoder().encode(aData);
  1.8219 +        OS.File.writeAtomic(this._file.path, array, { tmpPath: this._file.path + ".tmp" });
  1.8220 +        break;
  1.8221 +      }
  1.8222 +    }
  1.8223 +  },
  1.8224 +
  1.8225 +  update: function dc_update(aData) {
  1.8226 +    // Force the distribution preferences on the default branch
  1.8227 +    let defaults = Services.prefs.getDefaultBranch(null);
  1.8228 +    defaults.setCharPref("distribution.id", aData.id);
  1.8229 +    defaults.setCharPref("distribution.version", aData.version);
  1.8230 +  },
  1.8231 +
  1.8232 +  getPrefs: function dc_getPrefs() {
  1.8233 +    // Get the distribution directory, and bail if it doesn't exist.
  1.8234 +    let file = FileUtils.getDir("XREAppDist", [], false);
  1.8235 +    if (!file.exists())
  1.8236 +      return;
  1.8237 +
  1.8238 +    file.append("preferences.json");
  1.8239 +    this.readJSON(file, this.applyPrefs);
  1.8240 +  },
  1.8241 +
  1.8242 +  applyPrefs: function dc_applyPrefs(aData) {
  1.8243 +    // Check for required Global preferences
  1.8244 +    let global = aData["Global"];
  1.8245 +    if (!(global && global["id"] && global["version"] && global["about"])) {
  1.8246 +      Cu.reportError("Distribution: missing or incomplete Global preferences");
  1.8247 +      return;
  1.8248 +    }
  1.8249 +
  1.8250 +    // Force the distribution preferences on the default branch
  1.8251 +    let defaults = Services.prefs.getDefaultBranch(null);
  1.8252 +    defaults.setCharPref("distribution.id", global["id"]);
  1.8253 +    defaults.setCharPref("distribution.version", global["version"]);
  1.8254 +
  1.8255 +    let locale = Services.prefs.getCharPref("general.useragent.locale");
  1.8256 +    let aboutString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
  1.8257 +    aboutString.data = global["about." + locale] || global["about"];
  1.8258 +    defaults.setComplexValue("distribution.about", Ci.nsISupportsString, aboutString);
  1.8259 +
  1.8260 +    let prefs = aData["Preferences"];
  1.8261 +    for (let key in prefs) {
  1.8262 +      try {
  1.8263 +        let value = prefs[key];
  1.8264 +        switch (typeof value) {
  1.8265 +          case "boolean":
  1.8266 +            defaults.setBoolPref(key, value);
  1.8267 +            break;
  1.8268 +          case "number":
  1.8269 +            defaults.setIntPref(key, value);
  1.8270 +            break;
  1.8271 +          case "string":
  1.8272 +          case "undefined":
  1.8273 +            defaults.setCharPref(key, value);
  1.8274 +            break;
  1.8275 +        }
  1.8276 +      } catch (e) { /* ignore bad prefs and move on */ }
  1.8277 +    }
  1.8278 +
  1.8279 +    // Apply a lightweight theme if necessary
  1.8280 +    if (prefs["lightweightThemes.isThemeSelected"])
  1.8281 +      Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
  1.8282 +
  1.8283 +    let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
  1.8284 +    let localizeablePrefs = aData["LocalizablePreferences"];
  1.8285 +    for (let key in localizeablePrefs) {
  1.8286 +      try {
  1.8287 +        let value = localizeablePrefs[key];
  1.8288 +        value = value.replace("%LOCALE%", locale, "g");
  1.8289 +        localizedString.data = "data:text/plain," + key + "=" + value;
  1.8290 +        defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
  1.8291 +      } catch (e) { /* ignore bad prefs and move on */ }
  1.8292 +    }
  1.8293 +
  1.8294 +    let localizeablePrefsOverrides = aData["LocalizablePreferences." + locale];
  1.8295 +    for (let key in localizeablePrefsOverrides) {
  1.8296 +      try {
  1.8297 +        let value = localizeablePrefsOverrides[key];
  1.8298 +        localizedString.data = "data:text/plain," + key + "=" + value;
  1.8299 +        defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
  1.8300 +      } catch (e) { /* ignore bad prefs and move on */ }
  1.8301 +    }
  1.8302 +
  1.8303 +    sendMessageToJava({ type: "Distribution:Set:OK" });
  1.8304 +  },
  1.8305 +
  1.8306 +  // aFile is an nsIFile
  1.8307 +  // aCallback takes the parsed JSON object as a parameter
  1.8308 +  readJSON: function dc_readJSON(aFile, aCallback) {
  1.8309 +    Task.spawn(function() {
  1.8310 +      let bytes = yield OS.File.read(aFile.path);
  1.8311 +      let raw = new TextDecoder().decode(bytes) || "";
  1.8312 +
  1.8313 +      try {
  1.8314 +        aCallback(JSON.parse(raw));
  1.8315 +      } catch (e) {
  1.8316 +        Cu.reportError("Distribution: Could not parse JSON: " + e);
  1.8317 +      }
  1.8318 +    }).then(null, function onError(reason) {
  1.8319 +      if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
  1.8320 +        Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file");
  1.8321 +      }
  1.8322 +    });
  1.8323 +  }
  1.8324 +};
  1.8325 +
  1.8326 +var Tabs = {
  1.8327 +  _enableTabExpiration: false,
  1.8328 +  _domains: new Set(),
  1.8329 +
  1.8330 +  init: function() {
  1.8331 +    // On low-memory platforms, always allow tab expiration. On high-mem
  1.8332 +    // platforms, allow it to be turned on once we hit a low-mem situation.
  1.8333 +    if (BrowserApp.isOnLowMemoryPlatform) {
  1.8334 +      this._enableTabExpiration = true;
  1.8335 +    } else {
  1.8336 +      Services.obs.addObserver(this, "memory-pressure", false);
  1.8337 +    }
  1.8338 +
  1.8339 +    Services.obs.addObserver(this, "Session:Prefetch", false);
  1.8340 +
  1.8341 +    BrowserApp.deck.addEventListener("pageshow", this, false);
  1.8342 +    BrowserApp.deck.addEventListener("TabOpen", this, false);
  1.8343 +  },
  1.8344 +
  1.8345 +  uninit: function() {
  1.8346 +    if (!this._enableTabExpiration) {
  1.8347 +      // If _enableTabExpiration is true then we won't have this
  1.8348 +      // observer registered any more.
  1.8349 +      Services.obs.removeObserver(this, "memory-pressure");
  1.8350 +    }
  1.8351 +
  1.8352 +    Services.obs.removeObserver(this, "Session:Prefetch");
  1.8353 +
  1.8354 +    BrowserApp.deck.removeEventListener("pageshow", this);
  1.8355 +    BrowserApp.deck.removeEventListener("TabOpen", this);
  1.8356 +  },
  1.8357 +
  1.8358 +  observe: function(aSubject, aTopic, aData) {
  1.8359 +    switch (aTopic) {
  1.8360 +      case "memory-pressure":
  1.8361 +        if (aData != "heap-minimize") {
  1.8362 +          // We received a low-memory related notification. This will enable
  1.8363 +          // expirations.
  1.8364 +          this._enableTabExpiration = true;
  1.8365 +          Services.obs.removeObserver(this, "memory-pressure");
  1.8366 +        } else {
  1.8367 +          // Use "heap-minimize" as a trigger to expire the most stale tab.
  1.8368 +          this.expireLruTab();
  1.8369 +        }
  1.8370 +        break;
  1.8371 +      case "Session:Prefetch":
  1.8372 +        if (aData) {
  1.8373 +          let uri = Services.io.newURI(aData, null, null);
  1.8374 +          if (uri && !this._domains.has(uri.host)) {
  1.8375 +            try {
  1.8376 +              Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
  1.8377 +              this._domains.add(uri.host);
  1.8378 +            } catch (e) {}
  1.8379 +          }
  1.8380 +        }
  1.8381 +        break;
  1.8382 +    }
  1.8383 +  },
  1.8384 +
  1.8385 +  handleEvent: function(aEvent) {
  1.8386 +    switch (aEvent.type) {
  1.8387 +      case "pageshow":
  1.8388 +        // Clear the domain cache whenever a page get loaded into any browser.
  1.8389 +        this._domains.clear();
  1.8390 +        break;
  1.8391 +      case "TabOpen":
  1.8392 +        // Use opening a new tab as a trigger to expire the most stale tab.
  1.8393 +        this.expireLruTab();
  1.8394 +        break;
  1.8395 +    }
  1.8396 +  },
  1.8397 +
  1.8398 +  // Manage the most-recently-used list of tabs. Each tab has a timestamp
  1.8399 +  // associated with it that indicates when it was last touched.
  1.8400 +  expireLruTab: function() {
  1.8401 +    if (!this._enableTabExpiration) {
  1.8402 +      return false;
  1.8403 +    }
  1.8404 +    let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000;
  1.8405 +    if (expireTimeMs < 0) {
  1.8406 +      // This behaviour is disabled.
  1.8407 +      return false;
  1.8408 +    }
  1.8409 +    let tabs = BrowserApp.tabs;
  1.8410 +    let selected = BrowserApp.selectedTab;
  1.8411 +    let lruTab = null;
  1.8412 +    // Find the least recently used non-zombie tab.
  1.8413 +    for (let i = 0; i < tabs.length; i++) {
  1.8414 +      if (tabs[i] == selected || tabs[i].browser.__SS_restore) {
  1.8415 +        // This tab is selected or already a zombie, skip it.
  1.8416 +        continue;
  1.8417 +      }
  1.8418 +      if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) {
  1.8419 +        lruTab = tabs[i];
  1.8420 +      }
  1.8421 +    }
  1.8422 +    // If the tab was last touched more than browser.tabs.expireTime seconds ago,
  1.8423 +    // zombify it.
  1.8424 +    if (lruTab) {
  1.8425 +      let tabAgeMs = Date.now() - lruTab.lastTouchedAt;
  1.8426 +      if (tabAgeMs > expireTimeMs) {
  1.8427 +        MemoryObserver.zombify(lruTab);
  1.8428 +        Telemetry.addData("FENNEC_TAB_EXPIRED", tabAgeMs / 1000);
  1.8429 +        return true;
  1.8430 +      }
  1.8431 +    }
  1.8432 +    return false;
  1.8433 +  },
  1.8434 +
  1.8435 +  // For debugging
  1.8436 +  dump: function(aPrefix) {
  1.8437 +    let tabs = BrowserApp.tabs;
  1.8438 +    for (let i = 0; i < tabs.length; i++) {
  1.8439 +      dump(aPrefix + " | " + "Tab [" + tabs[i].browser.contentWindow.location.href + "]: lastTouchedAt:" + tabs[i].lastTouchedAt + ", zombie:" + tabs[i].browser.__SS_restore);
  1.8440 +    }
  1.8441 +  },
  1.8442 +};
  1.8443 +
  1.8444 +function ContextMenuItem(args) {
  1.8445 +  this.id = uuidgen.generateUUID().toString();
  1.8446 +  this.args = args;
  1.8447 +}
  1.8448 +
  1.8449 +ContextMenuItem.prototype = {
  1.8450 +  get order() {
  1.8451 +    return this.args.order || 0;
  1.8452 +  },
  1.8453 +
  1.8454 +  matches: function(elt, x, y) {
  1.8455 +    return this.args.selector.matches(elt, x, y);
  1.8456 +  },
  1.8457 +
  1.8458 +  callback: function(elt) {
  1.8459 +    this.args.callback(elt);
  1.8460 +  },
  1.8461 +
  1.8462 +  addVal: function(name, elt, defaultValue) {
  1.8463 +    if (!(name in this.args))
  1.8464 +      return defaultValue;
  1.8465 +
  1.8466 +    if (typeof this.args[name] == "function")
  1.8467 +      return this.args[name](elt);
  1.8468 +
  1.8469 +    return this.args[name];
  1.8470 +  },
  1.8471 +
  1.8472 +  getValue: function(elt) {
  1.8473 +    return {
  1.8474 +      id: this.id,
  1.8475 +      label: this.addVal("label", elt),
  1.8476 +      showAsActions: this.addVal("showAsActions", elt),
  1.8477 +      icon: this.addVal("icon", elt),
  1.8478 +      isGroup: this.addVal("isGroup", elt, false),
  1.8479 +      inGroup: this.addVal("inGroup", elt, false),
  1.8480 +      disabled: this.addVal("disabled", elt, false),
  1.8481 +      selected: this.addVal("selected", elt, false),
  1.8482 +      isParent: this.addVal("isParent", elt, false),
  1.8483 +    };
  1.8484 +  }
  1.8485 +}
  1.8486 +
  1.8487 +function HTMLContextMenuItem(elt, target) {
  1.8488 +  ContextMenuItem.call(this, { });
  1.8489 +
  1.8490 +  this.menuElementRef = Cu.getWeakReference(elt);
  1.8491 +  this.targetElementRef = Cu.getWeakReference(target);
  1.8492 +}
  1.8493 +
  1.8494 +HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
  1.8495 +  order: {
  1.8496 +    value: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER
  1.8497 +  },
  1.8498 +
  1.8499 +  matches: {
  1.8500 +    value: function(target) {
  1.8501 +      let t = this.targetElementRef.get();
  1.8502 +      return t === target;
  1.8503 +    },
  1.8504 +  },
  1.8505 +
  1.8506 +  callback: {
  1.8507 +    value: function(target) {
  1.8508 +      let elt = this.menuElementRef.get();
  1.8509 +      if (!elt) {
  1.8510 +        return;
  1.8511 +      }
  1.8512 +
  1.8513 +      // If this is a menu item, show a new context menu with the submenu in it
  1.8514 +      if (elt instanceof Ci.nsIDOMHTMLMenuElement) {
  1.8515 +        try {
  1.8516 +          NativeWindow.contextmenus.menus = {};
  1.8517 +
  1.8518 +          let elt = this.menuElementRef.get();
  1.8519 +          let target = this.targetElementRef.get();
  1.8520 +          if (!elt) {
  1.8521 +            return;
  1.8522 +          }
  1.8523 +
  1.8524 +          var items = NativeWindow.contextmenus._getHTMLContextMenuItemsForMenu(elt, target);
  1.8525 +          // This menu will always only have one context, but we still make sure its the "right" one.
  1.8526 +          var context = NativeWindow.contextmenus._getContextType(target);
  1.8527 +          if (items.length > 0) {
  1.8528 +            NativeWindow.contextmenus._addMenuItems(items, context);
  1.8529 +          }
  1.8530 +
  1.8531 +        } catch(ex) {
  1.8532 +          Cu.reportError(ex);
  1.8533 +        }
  1.8534 +      } else {
  1.8535 +        // otherwise just click the menu item
  1.8536 +        elt.click();
  1.8537 +      }
  1.8538 +    },
  1.8539 +  },
  1.8540 +
  1.8541 +  getValue: {
  1.8542 +    value: function(target) {
  1.8543 +      let elt = this.menuElementRef.get();
  1.8544 +      if (!elt) {
  1.8545 +        return null;
  1.8546 +      }
  1.8547 +
  1.8548 +      if (elt.hasAttribute("hidden")) {
  1.8549 +        return null;
  1.8550 +      }
  1.8551 +
  1.8552 +      return {
  1.8553 +        id: this.id,
  1.8554 +        icon: elt.icon,
  1.8555 +        label: elt.label,
  1.8556 +        disabled: elt.disabled,
  1.8557 +        menu: elt instanceof Ci.nsIDOMHTMLMenuElement
  1.8558 +      };
  1.8559 +    }
  1.8560 +  },
  1.8561 +});

mercurial