mobile/android/chrome/content/browser.js

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 #filter substitution
michael@0 2 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6 "use strict";
michael@0 7
michael@0 8 let Cc = Components.classes;
michael@0 9 let Ci = Components.interfaces;
michael@0 10 let Cu = Components.utils;
michael@0 11 let Cr = Components.results;
michael@0 12
michael@0 13 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 14 Cu.import("resource://gre/modules/Services.jsm");
michael@0 15 Cu.import("resource://gre/modules/AddonManager.jsm");
michael@0 16 Cu.import("resource://gre/modules/FileUtils.jsm");
michael@0 17 Cu.import("resource://gre/modules/JNI.jsm");
michael@0 18 Cu.import('resource://gre/modules/Payment.jsm');
michael@0 19 Cu.import("resource://gre/modules/NotificationDB.jsm");
michael@0 20 Cu.import("resource://gre/modules/SpatialNavigation.jsm");
michael@0 21 Cu.import("resource://gre/modules/UITelemetry.jsm");
michael@0 22
michael@0 23 #ifdef ACCESSIBILITY
michael@0 24 Cu.import("resource://gre/modules/accessibility/AccessFu.jsm");
michael@0 25 #endif
michael@0 26
michael@0 27 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
michael@0 28 "resource://gre/modules/PluralForm.jsm");
michael@0 29
michael@0 30 XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava",
michael@0 31 "resource://gre/modules/Messaging.jsm");
michael@0 32
michael@0 33 XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
michael@0 34 "resource://gre/modules/devtools/dbg-server.jsm");
michael@0 35
michael@0 36 XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides",
michael@0 37 "resource://gre/modules/UserAgentOverrides.jsm");
michael@0 38
michael@0 39 XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent",
michael@0 40 "resource://gre/modules/LoginManagerContent.jsm");
michael@0 41
michael@0 42 XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
michael@0 43 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
michael@0 44
michael@0 45 #ifdef MOZ_SAFE_BROWSING
michael@0 46 XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing",
michael@0 47 "resource://gre/modules/SafeBrowsing.jsm");
michael@0 48 #endif
michael@0 49
michael@0 50 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
michael@0 51 "resource://gre/modules/PrivateBrowsingUtils.jsm");
michael@0 52
michael@0 53 XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer",
michael@0 54 "resource://gre/modules/Sanitizer.jsm");
michael@0 55
michael@0 56 XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
michael@0 57 "resource://gre/modules/Prompt.jsm");
michael@0 58
michael@0 59 XPCOMUtils.defineLazyModuleGetter(this, "HelperApps",
michael@0 60 "resource://gre/modules/HelperApps.jsm");
michael@0 61
michael@0 62 XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions",
michael@0 63 "resource://gre/modules/SSLExceptions.jsm");
michael@0 64
michael@0 65 XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
michael@0 66 "resource://gre/modules/FormHistory.jsm");
michael@0 67
michael@0 68 XPCOMUtils.defineLazyServiceGetter(this, "uuidgen",
michael@0 69 "@mozilla.org/uuid-generator;1",
michael@0 70 "nsIUUIDGenerator");
michael@0 71
michael@0 72 XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery",
michael@0 73 "resource://gre/modules/SimpleServiceDiscovery.jsm");
michael@0 74
michael@0 75 #ifdef NIGHTLY_BUILD
michael@0 76 XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils",
michael@0 77 "resource://shumway/ShumwayUtils.jsm");
michael@0 78 #endif
michael@0 79
michael@0 80 #ifdef MOZ_ANDROID_SYNTHAPKS
michael@0 81 XPCOMUtils.defineLazyModuleGetter(this, "WebappManager",
michael@0 82 "resource://gre/modules/WebappManager.jsm");
michael@0 83 #endif
michael@0 84
michael@0 85 XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu",
michael@0 86 "resource://gre/modules/CharsetMenu.jsm");
michael@0 87
michael@0 88 // Lazily-loaded browser scripts:
michael@0 89 [
michael@0 90 ["SelectHelper", "chrome://browser/content/SelectHelper.js"],
michael@0 91 ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"],
michael@0 92 ["AboutReader", "chrome://browser/content/aboutReader.js"],
michael@0 93 ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
michael@0 94 ["PluginHelper", "chrome://browser/content/PluginHelper.js"],
michael@0 95 ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
michael@0 96 ["Linkifier", "chrome://browser/content/Linkify.js"],
michael@0 97 ["ZoomHelper", "chrome://browser/content/ZoomHelper.js"],
michael@0 98 ["CastingApps", "chrome://browser/content/CastingApps.js"],
michael@0 99 ].forEach(function (aScript) {
michael@0 100 let [name, script] = aScript;
michael@0 101 XPCOMUtils.defineLazyGetter(window, name, function() {
michael@0 102 let sandbox = {};
michael@0 103 Services.scriptloader.loadSubScript(script, sandbox);
michael@0 104 return sandbox[name];
michael@0 105 });
michael@0 106 });
michael@0 107
michael@0 108 [
michael@0 109 #ifdef MOZ_WEBRTC
michael@0 110 ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"],
michael@0 111 #endif
michael@0 112 ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
michael@0 113 ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
michael@0 114 ["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
michael@0 115 ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
michael@0 116 ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
michael@0 117 ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
michael@0 118 ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
michael@0 119 ].forEach(function (aScript) {
michael@0 120 let [name, notifications, script] = aScript;
michael@0 121 XPCOMUtils.defineLazyGetter(window, name, function() {
michael@0 122 let sandbox = {};
michael@0 123 Services.scriptloader.loadSubScript(script, sandbox);
michael@0 124 return sandbox[name];
michael@0 125 });
michael@0 126 notifications.forEach(function (aNotification) {
michael@0 127 Services.obs.addObserver(function(s, t, d) {
michael@0 128 window[name].observe(s, t, d)
michael@0 129 }, aNotification, false);
michael@0 130 });
michael@0 131 });
michael@0 132
michael@0 133 // Lazily-loaded JS modules that use observer notifications
michael@0 134 [
michael@0 135 ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView",
michael@0 136 "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"],
michael@0 137 ].forEach(module => {
michael@0 138 let [name, notifications, resource] = module;
michael@0 139 XPCOMUtils.defineLazyModuleGetter(this, name, resource);
michael@0 140 notifications.forEach(notification => {
michael@0 141 Services.obs.addObserver((s,t,d) => {
michael@0 142 this[name].observe(s,t,d)
michael@0 143 }, notification, false);
michael@0 144 });
michael@0 145 });
michael@0 146
michael@0 147 XPCOMUtils.defineLazyServiceGetter(this, "Haptic",
michael@0 148 "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback");
michael@0 149
michael@0 150 XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils",
michael@0 151 "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
michael@0 152
michael@0 153 XPCOMUtils.defineLazyServiceGetter(window, "URIFixup",
michael@0 154 "@mozilla.org/docshell/urifixup;1", "nsIURIFixup");
michael@0 155
michael@0 156 #ifdef MOZ_WEBRTC
michael@0 157 XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService",
michael@0 158 "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService");
michael@0 159 #endif
michael@0 160
michael@0 161 const kStateActive = 0x00000001; // :active pseudoclass for elements
michael@0 162
michael@0 163 const kXLinkNamespace = "http://www.w3.org/1999/xlink";
michael@0 164
michael@0 165 const kDefaultCSSViewportWidth = 980;
michael@0 166 const kDefaultCSSViewportHeight = 480;
michael@0 167
michael@0 168 const kViewportRemeasureThrottle = 500;
michael@0 169
michael@0 170 const kDoNotTrackPrefState = Object.freeze({
michael@0 171 NO_PREF: "0",
michael@0 172 DISALLOW_TRACKING: "1",
michael@0 173 ALLOW_TRACKING: "2",
michael@0 174 });
michael@0 175
michael@0 176 function dump(a) {
michael@0 177 Services.console.logStringMessage(a);
michael@0 178 }
michael@0 179
michael@0 180 function doChangeMaxLineBoxWidth(aWidth) {
michael@0 181 gReflowPending = null;
michael@0 182 let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
michael@0 183 let docShell = webNav.QueryInterface(Ci.nsIDocShell);
michael@0 184 let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
michael@0 185
michael@0 186 let range = null;
michael@0 187 if (BrowserApp.selectedTab._mReflozPoint) {
michael@0 188 range = BrowserApp.selectedTab._mReflozPoint.range;
michael@0 189 }
michael@0 190
michael@0 191 try {
michael@0 192 docViewer.pausePainting();
michael@0 193 docViewer.changeMaxLineBoxWidth(aWidth);
michael@0 194
michael@0 195 if (range) {
michael@0 196 ZoomHelper.zoomInAndSnapToRange(range);
michael@0 197 } else {
michael@0 198 // In this case, we actually didn't zoom into a specific range. It
michael@0 199 // probably happened from a page load reflow-on-zoom event, so we
michael@0 200 // need to make sure painting is re-enabled.
michael@0 201 BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
michael@0 202 }
michael@0 203 } finally {
michael@0 204 docViewer.resumePainting();
michael@0 205 }
michael@0 206 }
michael@0 207
michael@0 208 function fuzzyEquals(a, b) {
michael@0 209 return (Math.abs(a - b) < 1e-6);
michael@0 210 }
michael@0 211
michael@0 212 /**
michael@0 213 * Convert a font size to CSS pixels (px) from twentieiths-of-a-point
michael@0 214 * (twips).
michael@0 215 */
michael@0 216 function convertFromTwipsToPx(aSize) {
michael@0 217 return aSize/240 * 16.0;
michael@0 218 }
michael@0 219
michael@0 220 #ifdef MOZ_CRASHREPORTER
michael@0 221 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 222 XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
michael@0 223 "@mozilla.org/xre/app-info;1", "nsICrashReporter");
michael@0 224 #endif
michael@0 225
michael@0 226 XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
michael@0 227 let ContentAreaUtils = {};
michael@0 228 Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
michael@0 229 return ContentAreaUtils;
michael@0 230 });
michael@0 231
michael@0 232 XPCOMUtils.defineLazyModuleGetter(this, "Rect",
michael@0 233 "resource://gre/modules/Geometry.jsm");
michael@0 234
michael@0 235 function resolveGeckoURI(aURI) {
michael@0 236 if (!aURI)
michael@0 237 throw "Can't resolve an empty uri";
michael@0 238
michael@0 239 if (aURI.startsWith("chrome://")) {
michael@0 240 let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
michael@0 241 return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec;
michael@0 242 } else if (aURI.startsWith("resource://")) {
michael@0 243 let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
michael@0 244 return handler.resolveURI(Services.io.newURI(aURI, null, null));
michael@0 245 }
michael@0 246 return aURI;
michael@0 247 }
michael@0 248
michael@0 249 /**
michael@0 250 * Cache of commonly used string bundles.
michael@0 251 */
michael@0 252 var Strings = {};
michael@0 253 [
michael@0 254 ["brand", "chrome://branding/locale/brand.properties"],
michael@0 255 ["browser", "chrome://browser/locale/browser.properties"]
michael@0 256 ].forEach(function (aStringBundle) {
michael@0 257 let [name, bundle] = aStringBundle;
michael@0 258 XPCOMUtils.defineLazyGetter(Strings, name, function() {
michael@0 259 return Services.strings.createBundle(bundle);
michael@0 260 });
michael@0 261 });
michael@0 262
michael@0 263 const kFormHelperModeDisabled = 0;
michael@0 264 const kFormHelperModeEnabled = 1;
michael@0 265 const kFormHelperModeDynamic = 2; // disabled on tablets
michael@0 266
michael@0 267 var BrowserApp = {
michael@0 268 _tabs: [],
michael@0 269 _selectedTab: null,
michael@0 270 _prefObservers: [],
michael@0 271 isGuest: false,
michael@0 272
michael@0 273 get isTablet() {
michael@0 274 let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
michael@0 275 delete this.isTablet;
michael@0 276 return this.isTablet = sysInfo.get("tablet");
michael@0 277 },
michael@0 278
michael@0 279 get isOnLowMemoryPlatform() {
michael@0 280 let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory);
michael@0 281 delete this.isOnLowMemoryPlatform;
michael@0 282 return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform();
michael@0 283 },
michael@0 284
michael@0 285 deck: null,
michael@0 286
michael@0 287 startup: function startup() {
michael@0 288 window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
michael@0 289 dump("zerdatime " + Date.now() + " - browser chrome startup finished.");
michael@0 290
michael@0 291 this.deck = document.getElementById("browsers");
michael@0 292 this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
michael@0 293 try {
michael@0 294 BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
michael@0 295 Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
michael@0 296 sendMessageToJava({ type: "Gecko:DelayedStartup" });
michael@0 297 } catch(ex) { console.log(ex); }
michael@0 298 }, false);
michael@0 299
michael@0 300 BrowserEventHandler.init();
michael@0 301 ViewportHandler.init();
michael@0 302
michael@0 303 Services.androidBridge.browserApp = this;
michael@0 304
michael@0 305 Services.obs.addObserver(this, "Locale:Changed", false);
michael@0 306 Services.obs.addObserver(this, "Tab:Load", false);
michael@0 307 Services.obs.addObserver(this, "Tab:Selected", false);
michael@0 308 Services.obs.addObserver(this, "Tab:Closed", false);
michael@0 309 Services.obs.addObserver(this, "Session:Back", false);
michael@0 310 Services.obs.addObserver(this, "Session:ShowHistory", false);
michael@0 311 Services.obs.addObserver(this, "Session:Forward", false);
michael@0 312 Services.obs.addObserver(this, "Session:Reload", false);
michael@0 313 Services.obs.addObserver(this, "Session:Stop", false);
michael@0 314 Services.obs.addObserver(this, "SaveAs:PDF", false);
michael@0 315 Services.obs.addObserver(this, "Browser:Quit", false);
michael@0 316 Services.obs.addObserver(this, "Preferences:Set", false);
michael@0 317 Services.obs.addObserver(this, "ScrollTo:FocusedInput", false);
michael@0 318 Services.obs.addObserver(this, "Sanitize:ClearData", false);
michael@0 319 Services.obs.addObserver(this, "FullScreen:Exit", false);
michael@0 320 Services.obs.addObserver(this, "Viewport:Change", false);
michael@0 321 Services.obs.addObserver(this, "Viewport:Flush", false);
michael@0 322 Services.obs.addObserver(this, "Viewport:FixedMarginsChanged", false);
michael@0 323 Services.obs.addObserver(this, "Passwords:Init", false);
michael@0 324 Services.obs.addObserver(this, "FormHistory:Init", false);
michael@0 325 Services.obs.addObserver(this, "gather-telemetry", false);
michael@0 326 Services.obs.addObserver(this, "keyword-search", false);
michael@0 327 #ifdef MOZ_ANDROID_SYNTHAPKS
michael@0 328 Services.obs.addObserver(this, "webapps-runtime-install", false);
michael@0 329 Services.obs.addObserver(this, "webapps-runtime-install-package", false);
michael@0 330 Services.obs.addObserver(this, "webapps-ask-install", false);
michael@0 331 Services.obs.addObserver(this, "webapps-launch", false);
michael@0 332 Services.obs.addObserver(this, "webapps-uninstall", false);
michael@0 333 Services.obs.addObserver(this, "Webapps:AutoInstall", false);
michael@0 334 Services.obs.addObserver(this, "Webapps:Load", false);
michael@0 335 Services.obs.addObserver(this, "Webapps:AutoUninstall", false);
michael@0 336 #endif
michael@0 337 Services.obs.addObserver(this, "sessionstore-state-purge-complete", false);
michael@0 338
michael@0 339 function showFullScreenWarning() {
michael@0 340 NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
michael@0 341 }
michael@0 342
michael@0 343 window.addEventListener("fullscreen", function() {
michael@0 344 sendMessageToJava({
michael@0 345 type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide"
michael@0 346 });
michael@0 347 }, false);
michael@0 348
michael@0 349 window.addEventListener("mozfullscreenchange", function() {
michael@0 350 sendMessageToJava({
michael@0 351 type: document.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop"
michael@0 352 });
michael@0 353
michael@0 354 if (document.mozFullScreen)
michael@0 355 showFullScreenWarning();
michael@0 356 }, false);
michael@0 357
michael@0 358 // When a restricted key is pressed in DOM full-screen mode, we should display
michael@0 359 // the "Press ESC to exit" warning message.
michael@0 360 window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true);
michael@0 361
michael@0 362 NativeWindow.init();
michael@0 363 LightWeightThemeWebInstaller.init();
michael@0 364 Downloads.init();
michael@0 365 FormAssistant.init();
michael@0 366 IndexedDB.init();
michael@0 367 HealthReportStatusListener.init();
michael@0 368 XPInstallObserver.init();
michael@0 369 CharacterEncoding.init();
michael@0 370 ActivityObserver.init();
michael@0 371 #ifdef MOZ_ANDROID_SYNTHAPKS
michael@0 372 // TODO: replace with Android implementation of WebappOSUtils.isLaunchable.
michael@0 373 Cu.import("resource://gre/modules/Webapps.jsm");
michael@0 374 DOMApplicationRegistry.allAppsLaunchable = true;
michael@0 375 #else
michael@0 376 WebappsUI.init();
michael@0 377 #endif
michael@0 378 RemoteDebugger.init();
michael@0 379 Reader.init();
michael@0 380 UserAgentOverrides.init();
michael@0 381 DesktopUserAgent.init();
michael@0 382 CastingApps.init();
michael@0 383 Distribution.init();
michael@0 384 Tabs.init();
michael@0 385 #ifdef ACCESSIBILITY
michael@0 386 AccessFu.attach(window);
michael@0 387 #endif
michael@0 388 #ifdef NIGHTLY_BUILD
michael@0 389 ShumwayUtils.init();
michael@0 390 #endif
michael@0 391
michael@0 392 // Init LoginManager
michael@0 393 Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
michael@0 394
michael@0 395 let url = null;
michael@0 396 let pinned = false;
michael@0 397 if ("arguments" in window) {
michael@0 398 if (window.arguments[0])
michael@0 399 url = window.arguments[0];
michael@0 400 if (window.arguments[1])
michael@0 401 gScreenWidth = window.arguments[1];
michael@0 402 if (window.arguments[2])
michael@0 403 gScreenHeight = window.arguments[2];
michael@0 404 if (window.arguments[3])
michael@0 405 pinned = window.arguments[3];
michael@0 406 if (window.arguments[4])
michael@0 407 this.isGuest = window.arguments[4];
michael@0 408 }
michael@0 409
michael@0 410 if (pinned) {
michael@0 411 this._initRuntime(this._startupStatus, url, aUrl => this.addTab(aUrl));
michael@0 412 } else {
michael@0 413 SearchEngines.init();
michael@0 414 this.initContextMenu();
michael@0 415 }
michael@0 416 // The order that context menu items are added is important
michael@0 417 // Make sure the "Open in App" context menu item appears at the bottom of the list
michael@0 418 ExternalApps.init();
michael@0 419
michael@0 420 // XXX maybe we don't do this if the launch was kicked off from external
michael@0 421 Services.io.offline = false;
michael@0 422
michael@0 423 // Broadcast a UIReady message so add-ons know we are finished with startup
michael@0 424 let event = document.createEvent("Events");
michael@0 425 event.initEvent("UIReady", true, false);
michael@0 426 window.dispatchEvent(event);
michael@0 427
michael@0 428 if (this._startupStatus)
michael@0 429 this.onAppUpdated();
michael@0 430
michael@0 431 // Store the low-precision buffer pref
michael@0 432 this.gUseLowPrecision = Services.prefs.getBoolPref("layers.low-precision-buffer");
michael@0 433
michael@0 434 // notify java that gecko has loaded
michael@0 435 sendMessageToJava({ type: "Gecko:Ready" });
michael@0 436
michael@0 437 #ifdef MOZ_SAFE_BROWSING
michael@0 438 // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
michael@0 439 setTimeout(function() { SafeBrowsing.init(); }, 5000);
michael@0 440 #endif
michael@0 441 },
michael@0 442
michael@0 443 get _startupStatus() {
michael@0 444 delete this._startupStatus;
michael@0 445
michael@0 446 let savedMilestone = null;
michael@0 447 try {
michael@0 448 savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
michael@0 449 } catch (e) {
michael@0 450 }
michael@0 451 #expand let ourMilestone = "__MOZ_APP_VERSION__";
michael@0 452 this._startupStatus = "";
michael@0 453 if (ourMilestone != savedMilestone) {
michael@0 454 Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone);
michael@0 455 this._startupStatus = savedMilestone ? "upgrade" : "new";
michael@0 456 }
michael@0 457
michael@0 458 return this._startupStatus;
michael@0 459 },
michael@0 460
michael@0 461 /**
michael@0 462 * Pass this a locale string, such as "fr" or "es_ES".
michael@0 463 */
michael@0 464 setLocale: function (locale) {
michael@0 465 console.log("browser.js: requesting locale set: " + locale);
michael@0 466 sendMessageToJava({ type: "Locale:Set", locale: locale });
michael@0 467 },
michael@0 468
michael@0 469 _initRuntime: function(status, url, callback) {
michael@0 470 let sandbox = {};
michael@0 471 Services.scriptloader.loadSubScript("chrome://browser/content/WebappRT.js", sandbox);
michael@0 472 window.WebappRT = sandbox.WebappRT;
michael@0 473 WebappRT.init(status, url, callback);
michael@0 474 },
michael@0 475
michael@0 476 initContextMenu: function ba_initContextMenu() {
michael@0 477 // TODO: These should eventually move into more appropriate classes
michael@0 478 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"),
michael@0 479 NativeWindow.contextmenus.linkOpenableNonPrivateContext,
michael@0 480 function(aTarget) {
michael@0 481 UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab");
michael@0 482 UITelemetry.addEvent("loadurl.1", "contextmenu", null);
michael@0 483
michael@0 484 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
michael@0 485 ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
michael@0 486 BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id });
michael@0 487
michael@0 488 let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened");
michael@0 489 let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
michael@0 490 NativeWindow.toast.show(label, "short");
michael@0 491 });
michael@0 492
michael@0 493 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInPrivateTab"),
michael@0 494 NativeWindow.contextmenus.linkOpenableContext,
michael@0 495 function(aTarget) {
michael@0 496 UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_private_tab");
michael@0 497 UITelemetry.addEvent("loadurl.1", "contextmenu", null);
michael@0 498
michael@0 499 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
michael@0 500 ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal);
michael@0 501 BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true });
michael@0 502
michael@0 503 let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened");
michael@0 504 let label = PluralForm.get(1, newtabStrings).replace("#1", 1);
michael@0 505 NativeWindow.toast.show(label, "short");
michael@0 506 });
michael@0 507
michael@0 508 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyLink"),
michael@0 509 NativeWindow.contextmenus.linkCopyableContext,
michael@0 510 function(aTarget) {
michael@0 511 UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_link");
michael@0 512
michael@0 513 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
michael@0 514 NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
michael@0 515 });
michael@0 516
michael@0 517 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyEmailAddress"),
michael@0 518 NativeWindow.contextmenus.emailLinkContext,
michael@0 519 function(aTarget) {
michael@0 520 UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_email");
michael@0 521
michael@0 522 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
michael@0 523 let emailAddr = NativeWindow.contextmenus._stripScheme(url);
michael@0 524 NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr);
michael@0 525 });
michael@0 526
michael@0 527 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyPhoneNumber"),
michael@0 528 NativeWindow.contextmenus.phoneNumberLinkContext,
michael@0 529 function(aTarget) {
michael@0 530 UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_phone");
michael@0 531
michael@0 532 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
michael@0 533 let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
michael@0 534 NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber);
michael@0 535 });
michael@0 536
michael@0 537 NativeWindow.contextmenus.add({
michael@0 538 label: Strings.browser.GetStringFromName("contextmenu.shareLink"),
michael@0 539 order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
michael@0 540 selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkShareableContext),
michael@0 541 showAsActions: function(aElement) {
michael@0 542 return {
michael@0 543 title: aElement.textContent.trim() || aElement.title.trim(),
michael@0 544 uri: NativeWindow.contextmenus._getLinkURL(aElement),
michael@0 545 };
michael@0 546 },
michael@0 547 icon: "drawable://ic_menu_share",
michael@0 548 callback: function(aTarget) {
michael@0 549 UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link");
michael@0 550 }
michael@0 551 });
michael@0 552
michael@0 553 NativeWindow.contextmenus.add({
michael@0 554 label: Strings.browser.GetStringFromName("contextmenu.shareEmailAddress"),
michael@0 555 order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
michael@0 556 selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
michael@0 557 showAsActions: function(aElement) {
michael@0 558 let url = NativeWindow.contextmenus._getLinkURL(aElement);
michael@0 559 let emailAddr = NativeWindow.contextmenus._stripScheme(url);
michael@0 560 let title = aElement.textContent || aElement.title;
michael@0 561 return {
michael@0 562 title: title,
michael@0 563 uri: emailAddr,
michael@0 564 };
michael@0 565 },
michael@0 566 icon: "drawable://ic_menu_share",
michael@0 567 callback: function(aTarget) {
michael@0 568 UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email");
michael@0 569 }
michael@0 570 });
michael@0 571
michael@0 572 NativeWindow.contextmenus.add({
michael@0 573 label: Strings.browser.GetStringFromName("contextmenu.sharePhoneNumber"),
michael@0 574 order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
michael@0 575 selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
michael@0 576 showAsActions: function(aElement) {
michael@0 577 let url = NativeWindow.contextmenus._getLinkURL(aElement);
michael@0 578 let phoneNumber = NativeWindow.contextmenus._stripScheme(url);
michael@0 579 let title = aElement.textContent || aElement.title;
michael@0 580 return {
michael@0 581 title: title,
michael@0 582 uri: phoneNumber,
michael@0 583 };
michael@0 584 },
michael@0 585 icon: "drawable://ic_menu_share",
michael@0 586 callback: function(aTarget) {
michael@0 587 UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone");
michael@0 588 }
michael@0 589 });
michael@0 590
michael@0 591 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
michael@0 592 NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext),
michael@0 593 function(aTarget) {
michael@0 594 UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email");
michael@0 595
michael@0 596 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
michael@0 597 sendMessageToJava({
michael@0 598 type: "Contact:Add",
michael@0 599 email: url
michael@0 600 });
michael@0 601 });
michael@0 602
michael@0 603 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"),
michael@0 604 NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext),
michael@0 605 function(aTarget) {
michael@0 606 UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone");
michael@0 607
michael@0 608 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
michael@0 609 sendMessageToJava({
michael@0 610 type: "Contact:Add",
michael@0 611 phone: url
michael@0 612 });
michael@0 613 });
michael@0 614
michael@0 615 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"),
michael@0 616 NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkBookmarkableContext),
michael@0 617 function(aTarget) {
michael@0 618 UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark");
michael@0 619
michael@0 620 let url = NativeWindow.contextmenus._getLinkURL(aTarget);
michael@0 621 let title = aTarget.textContent || aTarget.title || url;
michael@0 622 sendMessageToJava({
michael@0 623 type: "Bookmark:Insert",
michael@0 624 url: url,
michael@0 625 title: title
michael@0 626 });
michael@0 627 });
michael@0 628
michael@0 629 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.playMedia"),
michael@0 630 NativeWindow.contextmenus.mediaContext("media-paused"),
michael@0 631 function(aTarget) {
michael@0 632 UITelemetry.addEvent("action.1", "contextmenu", null, "web_play");
michael@0 633 aTarget.play();
michael@0 634 });
michael@0 635
michael@0 636 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.pauseMedia"),
michael@0 637 NativeWindow.contextmenus.mediaContext("media-playing"),
michael@0 638 function(aTarget) {
michael@0 639 UITelemetry.addEvent("action.1", "contextmenu", null, "web_pause");
michael@0 640 aTarget.pause();
michael@0 641 });
michael@0 642
michael@0 643 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.showControls2"),
michael@0 644 NativeWindow.contextmenus.mediaContext("media-hidingcontrols"),
michael@0 645 function(aTarget) {
michael@0 646 UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media");
michael@0 647 aTarget.setAttribute("controls", true);
michael@0 648 });
michael@0 649
michael@0 650 NativeWindow.contextmenus.add({
michael@0 651 label: Strings.browser.GetStringFromName("contextmenu.shareMedia"),
michael@0 652 order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1,
michael@0 653 selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.SelectorContext("video")),
michael@0 654 showAsActions: function(aElement) {
michael@0 655 let url = (aElement.currentSrc || aElement.src);
michael@0 656 let title = aElement.textContent || aElement.title;
michael@0 657 return {
michael@0 658 title: title,
michael@0 659 uri: url,
michael@0 660 type: "video/*",
michael@0 661 };
michael@0 662 },
michael@0 663 icon: "drawable://ic_menu_share",
michael@0 664 callback: function(aTarget) {
michael@0 665 UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media");
michael@0 666 }
michael@0 667 });
michael@0 668
michael@0 669 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.fullScreen"),
michael@0 670 NativeWindow.contextmenus.SelectorContext("video:not(:-moz-full-screen)"),
michael@0 671 function(aTarget) {
michael@0 672 UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen");
michael@0 673 aTarget.mozRequestFullScreen();
michael@0 674 });
michael@0 675
michael@0 676 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.mute"),
michael@0 677 NativeWindow.contextmenus.mediaContext("media-unmuted"),
michael@0 678 function(aTarget) {
michael@0 679 UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute");
michael@0 680 aTarget.muted = true;
michael@0 681 });
michael@0 682
michael@0 683 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"),
michael@0 684 NativeWindow.contextmenus.mediaContext("media-muted"),
michael@0 685 function(aTarget) {
michael@0 686 UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute");
michael@0 687 aTarget.muted = false;
michael@0 688 });
michael@0 689
michael@0 690 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyImageLocation"),
michael@0 691 NativeWindow.contextmenus.imageLocationCopyableContext,
michael@0 692 function(aTarget) {
michael@0 693 UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image");
michael@0 694
michael@0 695 let url = aTarget.src;
michael@0 696 NativeWindow.contextmenus._copyStringToDefaultClipboard(url);
michael@0 697 });
michael@0 698
michael@0 699 NativeWindow.contextmenus.add({
michael@0 700 label: Strings.browser.GetStringFromName("contextmenu.shareImage"),
michael@0 701 selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
michael@0 702 order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items
michael@0 703 showAsActions: function(aTarget) {
michael@0 704 let doc = aTarget.ownerDocument;
michael@0 705 let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
michael@0 706 .getImgCacheForDocument(doc);
michael@0 707 let props = imageCache.findEntryProperties(aTarget.currentURI, doc.characterSet);
michael@0 708 let src = aTarget.src;
michael@0 709 return {
michael@0 710 title: src,
michael@0 711 uri: src,
michael@0 712 type: "image/*",
michael@0 713 };
michael@0 714 },
michael@0 715 icon: "drawable://ic_menu_share",
michael@0 716 menu: true,
michael@0 717 callback: function(aTarget) {
michael@0 718 UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image");
michael@0 719 }
michael@0 720 });
michael@0 721
michael@0 722 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.saveImage"),
michael@0 723 NativeWindow.contextmenus.imageSaveableContext,
michael@0 724 function(aTarget) {
michael@0 725 UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image");
michael@0 726
michael@0 727 ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle",
michael@0 728 false, true, aTarget.ownerDocument.documentURIObject,
michael@0 729 aTarget.ownerDocument);
michael@0 730 });
michael@0 731
michael@0 732 NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.setImageAs"),
michael@0 733 NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext),
michael@0 734 function(aTarget) {
michael@0 735 UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image");
michael@0 736
michael@0 737 let src = aTarget.src;
michael@0 738 sendMessageToJava({
michael@0 739 type: "Image:SetAs",
michael@0 740 url: src
michael@0 741 });
michael@0 742 });
michael@0 743
michael@0 744 NativeWindow.contextmenus.add(
michael@0 745 function(aTarget) {
michael@0 746 if (aTarget instanceof HTMLVideoElement) {
michael@0 747 // If a video element is zero width or height, its essentially
michael@0 748 // an HTMLAudioElement.
michael@0 749 if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 )
michael@0 750 return Strings.browser.GetStringFromName("contextmenu.saveAudio");
michael@0 751 return Strings.browser.GetStringFromName("contextmenu.saveVideo");
michael@0 752 } else if (aTarget instanceof HTMLAudioElement) {
michael@0 753 return Strings.browser.GetStringFromName("contextmenu.saveAudio");
michael@0 754 }
michael@0 755 return Strings.browser.GetStringFromName("contextmenu.saveVideo");
michael@0 756 }, NativeWindow.contextmenus.mediaSaveableContext,
michael@0 757 function(aTarget) {
michael@0 758 UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media");
michael@0 759
michael@0 760 let url = aTarget.currentSrc || aTarget.src;
michael@0 761 let filePickerTitleKey = (aTarget instanceof HTMLVideoElement &&
michael@0 762 (aTarget.videoWidth != 0 && aTarget.videoHeight != 0))
michael@0 763 ? "SaveVideoTitle" : "SaveAudioTitle";
michael@0 764 // Skipped trying to pull MIME type out of cache for now
michael@0 765 ContentAreaUtils.internalSave(url, null, null, null, null, false,
michael@0 766 filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject,
michael@0 767 aTarget.ownerDocument, true, null);
michael@0 768 });
michael@0 769 },
michael@0 770
michael@0 771 onAppUpdated: function() {
michael@0 772 // initialize the form history and passwords databases on upgrades
michael@0 773 Services.obs.notifyObservers(null, "FormHistory:Init", "");
michael@0 774 Services.obs.notifyObservers(null, "Passwords:Init", "");
michael@0 775
michael@0 776 // Migrate user-set "plugins.click_to_play" pref. See bug 884694.
michael@0 777 // Because the default value is true, a user-set pref means that the pref was set to false.
michael@0 778 if (Services.prefs.prefHasUserValue("plugins.click_to_play")) {
michael@0 779 Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED);
michael@0 780 Services.prefs.clearUserPref("plugins.click_to_play");
michael@0 781 }
michael@0 782 },
michael@0 783
michael@0 784 shutdown: function shutdown() {
michael@0 785 NativeWindow.uninit();
michael@0 786 LightWeightThemeWebInstaller.uninit();
michael@0 787 FormAssistant.uninit();
michael@0 788 IndexedDB.uninit();
michael@0 789 ViewportHandler.uninit();
michael@0 790 XPInstallObserver.uninit();
michael@0 791 HealthReportStatusListener.uninit();
michael@0 792 CharacterEncoding.uninit();
michael@0 793 SearchEngines.uninit();
michael@0 794 #ifndef MOZ_ANDROID_SYNTHAPKS
michael@0 795 WebappsUI.uninit();
michael@0 796 #endif
michael@0 797 RemoteDebugger.uninit();
michael@0 798 Reader.uninit();
michael@0 799 UserAgentOverrides.uninit();
michael@0 800 DesktopUserAgent.uninit();
michael@0 801 ExternalApps.uninit();
michael@0 802 CastingApps.uninit();
michael@0 803 Distribution.uninit();
michael@0 804 Tabs.uninit();
michael@0 805 },
michael@0 806
michael@0 807 // This function returns false during periods where the browser displayed document is
michael@0 808 // different from the browser content document, so user actions and some kinds of viewport
michael@0 809 // updates should be ignored. This period starts when we start loading a new page or
michael@0 810 // switch tabs, and ends when the new browser content document has been drawn and handed
michael@0 811 // off to the compositor.
michael@0 812 isBrowserContentDocumentDisplayed: function() {
michael@0 813 try {
michael@0 814 if (!Services.androidBridge.isContentDocumentDisplayed())
michael@0 815 return false;
michael@0 816 } catch (e) {
michael@0 817 return false;
michael@0 818 }
michael@0 819
michael@0 820 let tab = this.selectedTab;
michael@0 821 if (!tab)
michael@0 822 return false;
michael@0 823 return tab.contentDocumentIsDisplayed;
michael@0 824 },
michael@0 825
michael@0 826 contentDocumentChanged: function() {
michael@0 827 window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true;
michael@0 828 Services.androidBridge.contentDocumentChanged();
michael@0 829 },
michael@0 830
michael@0 831 get tabs() {
michael@0 832 return this._tabs;
michael@0 833 },
michael@0 834
michael@0 835 get selectedTab() {
michael@0 836 return this._selectedTab;
michael@0 837 },
michael@0 838
michael@0 839 set selectedTab(aTab) {
michael@0 840 if (this._selectedTab == aTab)
michael@0 841 return;
michael@0 842
michael@0 843 if (this._selectedTab) {
michael@0 844 this._selectedTab.setActive(false);
michael@0 845 }
michael@0 846
michael@0 847 this._selectedTab = aTab;
michael@0 848 if (!aTab)
michael@0 849 return;
michael@0 850
michael@0 851 aTab.setActive(true);
michael@0 852 aTab.setResolution(aTab._zoom, true);
michael@0 853 this.contentDocumentChanged();
michael@0 854 this.deck.selectedPanel = aTab.browser;
michael@0 855 // Focus the browser so that things like selection will be styled correctly.
michael@0 856 aTab.browser.focus();
michael@0 857 },
michael@0 858
michael@0 859 get selectedBrowser() {
michael@0 860 if (this._selectedTab)
michael@0 861 return this._selectedTab.browser;
michael@0 862 return null;
michael@0 863 },
michael@0 864
michael@0 865 getTabForId: function getTabForId(aId) {
michael@0 866 let tabs = this._tabs;
michael@0 867 for (let i=0; i < tabs.length; i++) {
michael@0 868 if (tabs[i].id == aId)
michael@0 869 return tabs[i];
michael@0 870 }
michael@0 871 return null;
michael@0 872 },
michael@0 873
michael@0 874 getTabForBrowser: function getTabForBrowser(aBrowser) {
michael@0 875 let tabs = this._tabs;
michael@0 876 for (let i = 0; i < tabs.length; i++) {
michael@0 877 if (tabs[i].browser == aBrowser)
michael@0 878 return tabs[i];
michael@0 879 }
michael@0 880 return null;
michael@0 881 },
michael@0 882
michael@0 883 getTabForWindow: function getTabForWindow(aWindow) {
michael@0 884 let tabs = this._tabs;
michael@0 885 for (let i = 0; i < tabs.length; i++) {
michael@0 886 if (tabs[i].browser.contentWindow == aWindow)
michael@0 887 return tabs[i];
michael@0 888 }
michael@0 889 return null;
michael@0 890 },
michael@0 891
michael@0 892 getBrowserForWindow: function getBrowserForWindow(aWindow) {
michael@0 893 let tabs = this._tabs;
michael@0 894 for (let i = 0; i < tabs.length; i++) {
michael@0 895 if (tabs[i].browser.contentWindow == aWindow)
michael@0 896 return tabs[i].browser;
michael@0 897 }
michael@0 898 return null;
michael@0 899 },
michael@0 900
michael@0 901 getBrowserForDocument: function getBrowserForDocument(aDocument) {
michael@0 902 let tabs = this._tabs;
michael@0 903 for (let i = 0; i < tabs.length; i++) {
michael@0 904 if (tabs[i].browser.contentDocument == aDocument)
michael@0 905 return tabs[i].browser;
michael@0 906 }
michael@0 907 return null;
michael@0 908 },
michael@0 909
michael@0 910 loadURI: function loadURI(aURI, aBrowser, aParams) {
michael@0 911 aBrowser = aBrowser || this.selectedBrowser;
michael@0 912 if (!aBrowser)
michael@0 913 return;
michael@0 914
michael@0 915 aParams = aParams || {};
michael@0 916
michael@0 917 let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
michael@0 918 let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null;
michael@0 919 let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
michael@0 920 let charset = "charset" in aParams ? aParams.charset : null;
michael@0 921
michael@0 922 let tab = this.getTabForBrowser(aBrowser);
michael@0 923 if (tab) {
michael@0 924 if ("userSearch" in aParams) tab.userSearch = aParams.userSearch;
michael@0 925 }
michael@0 926
michael@0 927 try {
michael@0 928 aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
michael@0 929 } catch(e) {
michael@0 930 if (tab) {
michael@0 931 let message = {
michael@0 932 type: "Content:LoadError",
michael@0 933 tabID: tab.id
michael@0 934 };
michael@0 935 sendMessageToJava(message);
michael@0 936 dump("Handled load error: " + e)
michael@0 937 }
michael@0 938 }
michael@0 939 },
michael@0 940
michael@0 941 addTab: function addTab(aURI, aParams) {
michael@0 942 aParams = aParams || {};
michael@0 943
michael@0 944 let newTab = new Tab(aURI, aParams);
michael@0 945 this._tabs.push(newTab);
michael@0 946
michael@0 947 let selected = "selected" in aParams ? aParams.selected : true;
michael@0 948 if (selected)
michael@0 949 this.selectedTab = newTab;
michael@0 950
michael@0 951 let pinned = "pinned" in aParams ? aParams.pinned : false;
michael@0 952 if (pinned) {
michael@0 953 let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
michael@0 954 ss.setTabValue(newTab, "appOrigin", aURI);
michael@0 955 }
michael@0 956
michael@0 957 let evt = document.createEvent("UIEvents");
michael@0 958 evt.initUIEvent("TabOpen", true, false, window, null);
michael@0 959 newTab.browser.dispatchEvent(evt);
michael@0 960
michael@0 961 return newTab;
michael@0 962 },
michael@0 963
michael@0 964 // Use this method to close a tab from JS. This method sends a message
michael@0 965 // to Java to close the tab in the Java UI (we'll get a Tab:Closed message
michael@0 966 // back from Java when that happens).
michael@0 967 closeTab: function closeTab(aTab) {
michael@0 968 if (!aTab) {
michael@0 969 Cu.reportError("Error trying to close tab (tab doesn't exist)");
michael@0 970 return;
michael@0 971 }
michael@0 972
michael@0 973 let message = {
michael@0 974 type: "Tab:Close",
michael@0 975 tabID: aTab.id
michael@0 976 };
michael@0 977 sendMessageToJava(message);
michael@0 978 },
michael@0 979
michael@0 980 #ifdef MOZ_ANDROID_SYNTHAPKS
michael@0 981 _loadWebapp: function(aMessage) {
michael@0 982
michael@0 983 this._initRuntime(this._startupStatus, aMessage.url, aUrl => {
michael@0 984 this.manifestUrl = aMessage.url;
michael@0 985 this.addTab(aUrl, { title: aMessage.name });
michael@0 986 });
michael@0 987 },
michael@0 988 #endif
michael@0 989
michael@0 990 // Calling this will update the state in BrowserApp after a tab has been
michael@0 991 // closed in the Java UI.
michael@0 992 _handleTabClosed: function _handleTabClosed(aTab) {
michael@0 993 if (aTab == this.selectedTab)
michael@0 994 this.selectedTab = null;
michael@0 995
michael@0 996 let evt = document.createEvent("UIEvents");
michael@0 997 evt.initUIEvent("TabClose", true, false, window, null);
michael@0 998 aTab.browser.dispatchEvent(evt);
michael@0 999
michael@0 1000 aTab.destroy();
michael@0 1001 this._tabs.splice(this._tabs.indexOf(aTab), 1);
michael@0 1002 },
michael@0 1003
michael@0 1004 // Use this method to select a tab from JS. This method sends a message
michael@0 1005 // to Java to select the tab in the Java UI (we'll get a Tab:Selected message
michael@0 1006 // back from Java when that happens).
michael@0 1007 selectTab: function selectTab(aTab) {
michael@0 1008 if (!aTab) {
michael@0 1009 Cu.reportError("Error trying to select tab (tab doesn't exist)");
michael@0 1010 return;
michael@0 1011 }
michael@0 1012
michael@0 1013 // There's nothing to do if the tab is already selected
michael@0 1014 if (aTab == this.selectedTab)
michael@0 1015 return;
michael@0 1016
michael@0 1017 let message = {
michael@0 1018 type: "Tab:Select",
michael@0 1019 tabID: aTab.id
michael@0 1020 };
michael@0 1021 sendMessageToJava(message);
michael@0 1022 },
michael@0 1023
michael@0 1024 /**
michael@0 1025 * Gets an open tab with the given URL.
michael@0 1026 *
michael@0 1027 * @param aURL URL to look for
michael@0 1028 * @return the tab with the given URL, or null if no such tab exists
michael@0 1029 */
michael@0 1030 getTabWithURL: function getTabWithURL(aURL) {
michael@0 1031 let uri = Services.io.newURI(aURL, null, null);
michael@0 1032 for (let i = 0; i < this._tabs.length; ++i) {
michael@0 1033 let tab = this._tabs[i];
michael@0 1034 if (tab.browser.currentURI.equals(uri)) {
michael@0 1035 return tab;
michael@0 1036 }
michael@0 1037 }
michael@0 1038 return null;
michael@0 1039 },
michael@0 1040
michael@0 1041 /**
michael@0 1042 * If a tab with the given URL already exists, that tab is selected.
michael@0 1043 * Otherwise, a new tab is opened with the given URL.
michael@0 1044 *
michael@0 1045 * @param aURL URL to open
michael@0 1046 */
michael@0 1047 selectOrOpenTab: function selectOrOpenTab(aURL) {
michael@0 1048 let tab = this.getTabWithURL(aURL);
michael@0 1049 if (tab == null) {
michael@0 1050 this.addTab(aURL);
michael@0 1051 } else {
michael@0 1052 this.selectTab(tab);
michael@0 1053 }
michael@0 1054 },
michael@0 1055
michael@0 1056 // This method updates the state in BrowserApp after a tab has been selected
michael@0 1057 // in the Java UI.
michael@0 1058 _handleTabSelected: function _handleTabSelected(aTab) {
michael@0 1059 this.selectedTab = aTab;
michael@0 1060
michael@0 1061 let evt = document.createEvent("UIEvents");
michael@0 1062 evt.initUIEvent("TabSelect", true, false, window, null);
michael@0 1063 aTab.browser.dispatchEvent(evt);
michael@0 1064 },
michael@0 1065
michael@0 1066 quit: function quit() {
michael@0 1067 // Figure out if there's at least one other browser window around.
michael@0 1068 let lastBrowser = true;
michael@0 1069 let e = Services.wm.getEnumerator("navigator:browser");
michael@0 1070 while (e.hasMoreElements() && lastBrowser) {
michael@0 1071 let win = e.getNext();
michael@0 1072 if (!win.closed && win != window)
michael@0 1073 lastBrowser = false;
michael@0 1074 }
michael@0 1075
michael@0 1076 if (lastBrowser) {
michael@0 1077 // Let everyone know we are closing the last browser window
michael@0 1078 let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
michael@0 1079 Services.obs.notifyObservers(closingCanceled, "browser-lastwindow-close-requested", null);
michael@0 1080 if (closingCanceled.data)
michael@0 1081 return;
michael@0 1082
michael@0 1083 Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null);
michael@0 1084 }
michael@0 1085
michael@0 1086 window.QueryInterface(Ci.nsIDOMChromeWindow).minimize();
michael@0 1087 window.close();
michael@0 1088 },
michael@0 1089
michael@0 1090 saveAsPDF: function saveAsPDF(aBrowser) {
michael@0 1091 // Create the final destination file location
michael@0 1092 let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null);
michael@0 1093 fileName = fileName.trim() + ".pdf";
michael@0 1094
michael@0 1095 let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
michael@0 1096 let downloadsDir = dm.defaultDownloadsDirectory;
michael@0 1097
michael@0 1098 let file = downloadsDir.clone();
michael@0 1099 file.append(fileName);
michael@0 1100 file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8));
michael@0 1101
michael@0 1102 let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings;
michael@0 1103 printSettings.printSilent = true;
michael@0 1104 printSettings.showPrintProgress = false;
michael@0 1105 printSettings.printBGImages = true;
michael@0 1106 printSettings.printBGColors = true;
michael@0 1107 printSettings.printToFile = true;
michael@0 1108 printSettings.toFileName = file.path;
michael@0 1109 printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs;
michael@0 1110 printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF;
michael@0 1111
michael@0 1112 //XXX we probably need a preference here, the header can be useful
michael@0 1113 printSettings.footerStrCenter = "";
michael@0 1114 printSettings.footerStrLeft = "";
michael@0 1115 printSettings.footerStrRight = "";
michael@0 1116 printSettings.headerStrCenter = "";
michael@0 1117 printSettings.headerStrLeft = "";
michael@0 1118 printSettings.headerStrRight = "";
michael@0 1119
michael@0 1120 // Create a valid mimeInfo for the PDF
michael@0 1121 let ms = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService);
michael@0 1122 let mimeInfo = ms.getFromTypeAndExtension("application/pdf", "pdf");
michael@0 1123
michael@0 1124 let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 1125 .getInterface(Ci.nsIWebBrowserPrint);
michael@0 1126
michael@0 1127 let cancelable = {
michael@0 1128 cancel: function (aReason) {
michael@0 1129 webBrowserPrint.cancel();
michael@0 1130 }
michael@0 1131 }
michael@0 1132 let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aBrowser.contentWindow);
michael@0 1133 let download = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD,
michael@0 1134 aBrowser.currentURI,
michael@0 1135 Services.io.newFileURI(file), "", mimeInfo,
michael@0 1136 Date.now() * 1000, null, cancelable, isPrivate);
michael@0 1137
michael@0 1138 webBrowserPrint.print(printSettings, download);
michael@0 1139 },
michael@0 1140
michael@0 1141 notifyPrefObservers: function(aPref) {
michael@0 1142 this._prefObservers[aPref].forEach(function(aRequestId) {
michael@0 1143 this.getPreferences(aRequestId, [aPref], 1);
michael@0 1144 }, this);
michael@0 1145 },
michael@0 1146
michael@0 1147 handlePreferencesRequest: function handlePreferencesRequest(aRequestId,
michael@0 1148 aPrefNames,
michael@0 1149 aListen) {
michael@0 1150
michael@0 1151 let prefs = [];
michael@0 1152
michael@0 1153 for (let prefName of aPrefNames) {
michael@0 1154 let pref = {
michael@0 1155 name: prefName,
michael@0 1156 type: "",
michael@0 1157 value: null
michael@0 1158 };
michael@0 1159
michael@0 1160 if (aListen) {
michael@0 1161 if (this._prefObservers[prefName])
michael@0 1162 this._prefObservers[prefName].push(aRequestId);
michael@0 1163 else
michael@0 1164 this._prefObservers[prefName] = [ aRequestId ];
michael@0 1165 Services.prefs.addObserver(prefName, this, false);
michael@0 1166 }
michael@0 1167
michael@0 1168 // These pref names are not "real" pref names.
michael@0 1169 // They are used in the setting menu,
michael@0 1170 // and these are passed when initializing the setting menu.
michael@0 1171 switch (prefName) {
michael@0 1172 // The plugin pref is actually two separate prefs, so
michael@0 1173 // we need to handle it differently
michael@0 1174 case "plugin.enable":
michael@0 1175 pref.type = "string";// Use a string type for java's ListPreference
michael@0 1176 pref.value = PluginHelper.getPluginPreference();
michael@0 1177 prefs.push(pref);
michael@0 1178 continue;
michael@0 1179 // Handle master password
michael@0 1180 case "privacy.masterpassword.enabled":
michael@0 1181 pref.type = "bool";
michael@0 1182 pref.value = MasterPassword.enabled;
michael@0 1183 prefs.push(pref);
michael@0 1184 continue;
michael@0 1185 // Handle do-not-track preference
michael@0 1186 case "privacy.donottrackheader":
michael@0 1187 pref.type = "string";
michael@0 1188
michael@0 1189 let enableDNT = Services.prefs.getBoolPref("privacy.donottrackheader.enabled");
michael@0 1190 if (!enableDNT) {
michael@0 1191 pref.value = kDoNotTrackPrefState.NO_PREF;
michael@0 1192 } else {
michael@0 1193 let dntState = Services.prefs.getIntPref("privacy.donottrackheader.value");
michael@0 1194 pref.value = (dntState === 0) ? kDoNotTrackPrefState.ALLOW_TRACKING :
michael@0 1195 kDoNotTrackPrefState.DISALLOW_TRACKING;
michael@0 1196 }
michael@0 1197
michael@0 1198 prefs.push(pref);
michael@0 1199 continue;
michael@0 1200 #ifdef MOZ_CRASHREPORTER
michael@0 1201 // Crash reporter submit pref must be fetched from nsICrashReporter service.
michael@0 1202 case "datareporting.crashreporter.submitEnabled":
michael@0 1203 pref.type = "bool";
michael@0 1204 pref.value = CrashReporter.submitReports;
michael@0 1205 prefs.push(pref);
michael@0 1206 continue;
michael@0 1207 #endif
michael@0 1208 }
michael@0 1209
michael@0 1210 try {
michael@0 1211 switch (Services.prefs.getPrefType(prefName)) {
michael@0 1212 case Ci.nsIPrefBranch.PREF_BOOL:
michael@0 1213 pref.type = "bool";
michael@0 1214 pref.value = Services.prefs.getBoolPref(prefName);
michael@0 1215 break;
michael@0 1216 case Ci.nsIPrefBranch.PREF_INT:
michael@0 1217 pref.type = "int";
michael@0 1218 pref.value = Services.prefs.getIntPref(prefName);
michael@0 1219 break;
michael@0 1220 case Ci.nsIPrefBranch.PREF_STRING:
michael@0 1221 default:
michael@0 1222 pref.type = "string";
michael@0 1223 try {
michael@0 1224 // Try in case it's a localized string (will throw an exception if not)
michael@0 1225 pref.value = Services.prefs.getComplexValue(prefName, Ci.nsIPrefLocalizedString).data;
michael@0 1226 } catch (e) {
michael@0 1227 pref.value = Services.prefs.getCharPref(prefName);
michael@0 1228 }
michael@0 1229 break;
michael@0 1230 }
michael@0 1231 } catch (e) {
michael@0 1232 dump("Error reading pref [" + prefName + "]: " + e);
michael@0 1233 // preference does not exist; do not send it
michael@0 1234 continue;
michael@0 1235 }
michael@0 1236
michael@0 1237 // Some Gecko preferences use integers or strings to reference
michael@0 1238 // state instead of directly representing the value.
michael@0 1239 // Since the Java UI uses the type to determine which ui elements
michael@0 1240 // to show and how to handle them, we need to normalize these
michael@0 1241 // preferences to the correct type.
michael@0 1242 switch (prefName) {
michael@0 1243 // (string) index for determining which multiple choice value to display.
michael@0 1244 case "browser.chrome.titlebarMode":
michael@0 1245 case "network.cookie.cookieBehavior":
michael@0 1246 case "font.size.inflation.minTwips":
michael@0 1247 case "home.sync.updateMode":
michael@0 1248 pref.type = "string";
michael@0 1249 pref.value = pref.value.toString();
michael@0 1250 break;
michael@0 1251 }
michael@0 1252
michael@0 1253 prefs.push(pref);
michael@0 1254 }
michael@0 1255
michael@0 1256 sendMessageToJava({
michael@0 1257 type: "Preferences:Data",
michael@0 1258 requestId: aRequestId, // opaque request identifier, can be any string/int/whatever
michael@0 1259 preferences: prefs
michael@0 1260 });
michael@0 1261 },
michael@0 1262
michael@0 1263 setPreferences: function setPreferences(aPref) {
michael@0 1264 let json = JSON.parse(aPref);
michael@0 1265
michael@0 1266 switch (json.name) {
michael@0 1267 // The plugin pref is actually two separate prefs, so
michael@0 1268 // we need to handle it differently
michael@0 1269 case "plugin.enable":
michael@0 1270 PluginHelper.setPluginPreference(json.value);
michael@0 1271 return;
michael@0 1272
michael@0 1273 // MasterPassword pref is not real, we just need take action and leave
michael@0 1274 case "privacy.masterpassword.enabled":
michael@0 1275 if (MasterPassword.enabled)
michael@0 1276 MasterPassword.removePassword(json.value);
michael@0 1277 else
michael@0 1278 MasterPassword.setPassword(json.value);
michael@0 1279 return;
michael@0 1280
michael@0 1281 // "privacy.donottrackheader" is not "real" pref name, it's used in the setting menu.
michael@0 1282 case "privacy.donottrackheader":
michael@0 1283 switch (json.value) {
michael@0 1284 // Don't tell anything about tracking me
michael@0 1285 case kDoNotTrackPrefState.NO_PREF:
michael@0 1286 Services.prefs.setBoolPref("privacy.donottrackheader.enabled", false);
michael@0 1287 Services.prefs.clearUserPref("privacy.donottrackheader.value");
michael@0 1288 break;
michael@0 1289 // Accept tracking me
michael@0 1290 case kDoNotTrackPrefState.ALLOW_TRACKING:
michael@0 1291 Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true);
michael@0 1292 Services.prefs.setIntPref("privacy.donottrackheader.value", 0);
michael@0 1293 break;
michael@0 1294 // Not accept tracking me
michael@0 1295 case kDoNotTrackPrefState.DISALLOW_TRACKING:
michael@0 1296 Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true);
michael@0 1297 Services.prefs.setIntPref("privacy.donottrackheader.value", 1);
michael@0 1298 break;
michael@0 1299 }
michael@0 1300 return;
michael@0 1301
michael@0 1302 // Enabling or disabling suggestions will prevent future prompts
michael@0 1303 case SearchEngines.PREF_SUGGEST_ENABLED:
michael@0 1304 Services.prefs.setBoolPref(SearchEngines.PREF_SUGGEST_PROMPTED, true);
michael@0 1305 break;
michael@0 1306
michael@0 1307 #ifdef MOZ_CRASHREPORTER
michael@0 1308 // Crash reporter preference is in a service; set and return.
michael@0 1309 case "datareporting.crashreporter.submitEnabled":
michael@0 1310 CrashReporter.submitReports = json.value;
michael@0 1311 return;
michael@0 1312 #endif
michael@0 1313 // When sending to Java, we normalized special preferences that use
michael@0 1314 // integers and strings to represent booleans. Here, we convert them back
michael@0 1315 // to their actual types so we can store them.
michael@0 1316 case "browser.chrome.titlebarMode":
michael@0 1317 case "network.cookie.cookieBehavior":
michael@0 1318 case "font.size.inflation.minTwips":
michael@0 1319 case "home.sync.updateMode":
michael@0 1320 json.type = "int";
michael@0 1321 json.value = parseInt(json.value);
michael@0 1322 break;
michael@0 1323 }
michael@0 1324
michael@0 1325 switch (json.type) {
michael@0 1326 case "bool":
michael@0 1327 Services.prefs.setBoolPref(json.name, json.value);
michael@0 1328 break;
michael@0 1329 case "int":
michael@0 1330 Services.prefs.setIntPref(json.name, json.value);
michael@0 1331 break;
michael@0 1332 default: {
michael@0 1333 let pref = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
michael@0 1334 pref.data = json.value;
michael@0 1335 Services.prefs.setComplexValue(json.name, Ci.nsISupportsString, pref);
michael@0 1336 break;
michael@0 1337 }
michael@0 1338 }
michael@0 1339 },
michael@0 1340
michael@0 1341 sanitize: function (aItems) {
michael@0 1342 let json = JSON.parse(aItems);
michael@0 1343 let success = true;
michael@0 1344
michael@0 1345 for (let key in json) {
michael@0 1346 if (!json[key])
michael@0 1347 continue;
michael@0 1348
michael@0 1349 try {
michael@0 1350 switch (key) {
michael@0 1351 case "cookies_sessions":
michael@0 1352 Sanitizer.clearItem("cookies");
michael@0 1353 Sanitizer.clearItem("sessions");
michael@0 1354 break;
michael@0 1355 default:
michael@0 1356 Sanitizer.clearItem(key);
michael@0 1357 }
michael@0 1358 } catch (e) {
michael@0 1359 dump("sanitize error: " + e);
michael@0 1360 success = false;
michael@0 1361 }
michael@0 1362 }
michael@0 1363
michael@0 1364 sendMessageToJava({
michael@0 1365 type: "Sanitize:Finished",
michael@0 1366 success: success
michael@0 1367 });
michael@0 1368 },
michael@0 1369
michael@0 1370 getFocusedInput: function(aBrowser, aOnlyInputElements = false) {
michael@0 1371 if (!aBrowser)
michael@0 1372 return null;
michael@0 1373
michael@0 1374 let doc = aBrowser.contentDocument;
michael@0 1375 if (!doc)
michael@0 1376 return null;
michael@0 1377
michael@0 1378 let focused = doc.activeElement;
michael@0 1379 while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) {
michael@0 1380 doc = focused.contentDocument;
michael@0 1381 focused = doc.activeElement;
michael@0 1382 }
michael@0 1383
michael@0 1384 if (focused instanceof HTMLInputElement && focused.mozIsTextField(false))
michael@0 1385 return focused;
michael@0 1386
michael@0 1387 if (aOnlyInputElements)
michael@0 1388 return null;
michael@0 1389
michael@0 1390 if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) {
michael@0 1391
michael@0 1392 if (focused instanceof HTMLBodyElement) {
michael@0 1393 // we are putting focus into a contentEditable frame. scroll the frame into
michael@0 1394 // view instead of the contentEditable document contained within, because that
michael@0 1395 // results in a better user experience
michael@0 1396 focused = focused.ownerDocument.defaultView.frameElement;
michael@0 1397 }
michael@0 1398 return focused;
michael@0 1399 }
michael@0 1400 return null;
michael@0 1401 },
michael@0 1402
michael@0 1403 scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
michael@0 1404 let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
michael@0 1405 if (formHelperMode == kFormHelperModeDisabled)
michael@0 1406 return;
michael@0 1407
michael@0 1408 let focused = this.getFocusedInput(aBrowser);
michael@0 1409
michael@0 1410 if (focused) {
michael@0 1411 let shouldZoom = Services.prefs.getBoolPref("formhelper.autozoom");
michael@0 1412 if (formHelperMode == kFormHelperModeDynamic && this.isTablet)
michael@0 1413 shouldZoom = false;
michael@0 1414 // ZoomHelper.zoomToElement will handle not sending any message if this input is already mostly filling the screen
michael@0 1415 ZoomHelper.zoomToElement(focused, -1, false,
michael@0 1416 aAllowZoom && shouldZoom && !ViewportHandler.getViewportMetadata(aBrowser.contentWindow).isSpecified);
michael@0 1417 }
michael@0 1418 },
michael@0 1419
michael@0 1420 observe: function(aSubject, aTopic, aData) {
michael@0 1421 let browser = this.selectedBrowser;
michael@0 1422
michael@0 1423 switch (aTopic) {
michael@0 1424
michael@0 1425 case "Session:Back":
michael@0 1426 browser.goBack();
michael@0 1427 break;
michael@0 1428
michael@0 1429 case "Session:Forward":
michael@0 1430 browser.goForward();
michael@0 1431 break;
michael@0 1432
michael@0 1433 case "Session:Reload": {
michael@0 1434 let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
michael@0 1435
michael@0 1436 // Check to see if this is a message to enable/disable mixed content blocking.
michael@0 1437 if (aData) {
michael@0 1438 let allowMixedContent = JSON.parse(aData).allowMixedContent;
michael@0 1439 if (allowMixedContent) {
michael@0 1440 // Set a flag to disable mixed content blocking.
michael@0 1441 flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
michael@0 1442 } else {
michael@0 1443 // Set mixedContentChannel to null to re-enable mixed content blocking.
michael@0 1444 let docShell = browser.webNavigation.QueryInterface(Ci.nsIDocShell);
michael@0 1445 docShell.mixedContentChannel = null;
michael@0 1446 }
michael@0 1447 }
michael@0 1448
michael@0 1449 // Try to use the session history to reload so that framesets are
michael@0 1450 // handled properly. If the window has no session history, fall back
michael@0 1451 // to using the web navigation's reload method.
michael@0 1452 let webNav = browser.webNavigation;
michael@0 1453 try {
michael@0 1454 let sh = webNav.sessionHistory;
michael@0 1455 if (sh)
michael@0 1456 webNav = sh.QueryInterface(Ci.nsIWebNavigation);
michael@0 1457 } catch (e) {}
michael@0 1458 webNav.reload(flags);
michael@0 1459 break;
michael@0 1460 }
michael@0 1461
michael@0 1462 case "Session:Stop":
michael@0 1463 browser.stop();
michael@0 1464 break;
michael@0 1465
michael@0 1466 case "Session:ShowHistory": {
michael@0 1467 let data = JSON.parse(aData);
michael@0 1468 this.showHistory(data.fromIndex, data.toIndex, data.selIndex);
michael@0 1469 break;
michael@0 1470 }
michael@0 1471
michael@0 1472 case "Tab:Load": {
michael@0 1473 let data = JSON.parse(aData);
michael@0 1474
michael@0 1475 // Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from
michael@0 1476 // inheriting the currently loaded document's principal.
michael@0 1477 let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
michael@0 1478 Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
michael@0 1479 if (data.userEntered) {
michael@0 1480 flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER;
michael@0 1481 }
michael@0 1482
michael@0 1483 let delayLoad = ("delayLoad" in data) ? data.delayLoad : false;
michael@0 1484 let params = {
michael@0 1485 selected: ("selected" in data) ? data.selected : !delayLoad,
michael@0 1486 parentId: ("parentId" in data) ? data.parentId : -1,
michael@0 1487 flags: flags,
michael@0 1488 tabID: data.tabID,
michael@0 1489 isPrivate: (data.isPrivate === true),
michael@0 1490 pinned: (data.pinned === true),
michael@0 1491 delayLoad: (delayLoad === true),
michael@0 1492 desktopMode: (data.desktopMode === true)
michael@0 1493 };
michael@0 1494
michael@0 1495 let url = data.url;
michael@0 1496 if (data.engine) {
michael@0 1497 let engine = Services.search.getEngineByName(data.engine);
michael@0 1498 if (engine) {
michael@0 1499 params.userSearch = url;
michael@0 1500 let submission = engine.getSubmission(url);
michael@0 1501 url = submission.uri.spec;
michael@0 1502 params.postData = submission.postData;
michael@0 1503 }
michael@0 1504 }
michael@0 1505
michael@0 1506 if (data.newTab) {
michael@0 1507 this.addTab(url, params);
michael@0 1508 } else {
michael@0 1509 if (data.tabId) {
michael@0 1510 // Use a specific browser instead of the selected browser, if it exists
michael@0 1511 let specificBrowser = this.getTabForId(data.tabId).browser;
michael@0 1512 if (specificBrowser)
michael@0 1513 browser = specificBrowser;
michael@0 1514 }
michael@0 1515 this.loadURI(url, browser, params);
michael@0 1516 }
michael@0 1517 break;
michael@0 1518 }
michael@0 1519
michael@0 1520 case "Tab:Selected":
michael@0 1521 this._handleTabSelected(this.getTabForId(parseInt(aData)));
michael@0 1522 break;
michael@0 1523
michael@0 1524 case "Tab:Closed":
michael@0 1525 this._handleTabClosed(this.getTabForId(parseInt(aData)));
michael@0 1526 break;
michael@0 1527
michael@0 1528 case "keyword-search":
michael@0 1529 // This event refers to a search via the URL bar, not a bookmarks
michael@0 1530 // keyword search. Note that this code assumes that the user can only
michael@0 1531 // perform a keyword search on the selected tab.
michael@0 1532 this.selectedTab.userSearch = aData;
michael@0 1533
michael@0 1534 let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
michael@0 1535 sendMessageToJava({
michael@0 1536 type: "Search:Keyword",
michael@0 1537 identifier: engine.identifier,
michael@0 1538 name: engine.name,
michael@0 1539 });
michael@0 1540 break;
michael@0 1541
michael@0 1542 case "Browser:Quit":
michael@0 1543 this.quit();
michael@0 1544 break;
michael@0 1545
michael@0 1546 case "SaveAs:PDF":
michael@0 1547 this.saveAsPDF(browser);
michael@0 1548 break;
michael@0 1549
michael@0 1550 case "Preferences:Set":
michael@0 1551 this.setPreferences(aData);
michael@0 1552 break;
michael@0 1553
michael@0 1554 case "ScrollTo:FocusedInput":
michael@0 1555 // these messages come from a change in the viewable area and not user interaction
michael@0 1556 // we allow scrolling to the selected input, but not zooming the page
michael@0 1557 this.scrollToFocusedInput(browser, false);
michael@0 1558 break;
michael@0 1559
michael@0 1560 case "Sanitize:ClearData":
michael@0 1561 this.sanitize(aData);
michael@0 1562 break;
michael@0 1563
michael@0 1564 case "FullScreen:Exit":
michael@0 1565 browser.contentDocument.mozCancelFullScreen();
michael@0 1566 break;
michael@0 1567
michael@0 1568 case "Viewport:Change":
michael@0 1569 if (this.isBrowserContentDocumentDisplayed())
michael@0 1570 this.selectedTab.setViewport(JSON.parse(aData));
michael@0 1571 break;
michael@0 1572
michael@0 1573 case "Viewport:Flush":
michael@0 1574 this.contentDocumentChanged();
michael@0 1575 break;
michael@0 1576
michael@0 1577 case "Passwords:Init": {
michael@0 1578 let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"].
michael@0 1579 getService(Ci.nsILoginManagerStorage);
michael@0 1580 storage.init();
michael@0 1581 Services.obs.removeObserver(this, "Passwords:Init");
michael@0 1582 break;
michael@0 1583 }
michael@0 1584
michael@0 1585 case "FormHistory:Init": {
michael@0 1586 // Force creation/upgrade of formhistory.sqlite
michael@0 1587 FormHistory.count({});
michael@0 1588 Services.obs.removeObserver(this, "FormHistory:Init");
michael@0 1589 break;
michael@0 1590 }
michael@0 1591
michael@0 1592 case "sessionstore-state-purge-complete":
michael@0 1593 sendMessageToJava({ type: "Session:StatePurged" });
michael@0 1594 break;
michael@0 1595
michael@0 1596 case "gather-telemetry":
michael@0 1597 sendMessageToJava({ type: "Telemetry:Gather" });
michael@0 1598 break;
michael@0 1599
michael@0 1600 case "Viewport:FixedMarginsChanged":
michael@0 1601 gViewportMargins = JSON.parse(aData);
michael@0 1602 this.selectedTab.updateViewportSize(gScreenWidth);
michael@0 1603 break;
michael@0 1604
michael@0 1605 case "nsPref:changed":
michael@0 1606 this.notifyPrefObservers(aData);
michael@0 1607 break;
michael@0 1608
michael@0 1609 #ifdef MOZ_ANDROID_SYNTHAPKS
michael@0 1610 case "webapps-runtime-install":
michael@0 1611 WebappManager.install(JSON.parse(aData), aSubject);
michael@0 1612 break;
michael@0 1613
michael@0 1614 case "webapps-runtime-install-package":
michael@0 1615 WebappManager.installPackage(JSON.parse(aData), aSubject);
michael@0 1616 break;
michael@0 1617
michael@0 1618 case "webapps-ask-install":
michael@0 1619 WebappManager.askInstall(JSON.parse(aData));
michael@0 1620 break;
michael@0 1621
michael@0 1622 case "webapps-launch": {
michael@0 1623 WebappManager.launch(JSON.parse(aData));
michael@0 1624 break;
michael@0 1625 }
michael@0 1626
michael@0 1627 case "webapps-uninstall": {
michael@0 1628 WebappManager.uninstall(JSON.parse(aData));
michael@0 1629 break;
michael@0 1630 }
michael@0 1631
michael@0 1632 case "Webapps:AutoInstall":
michael@0 1633 WebappManager.autoInstall(JSON.parse(aData));
michael@0 1634 break;
michael@0 1635
michael@0 1636 case "Webapps:Load":
michael@0 1637 this._loadWebapp(JSON.parse(aData));
michael@0 1638 break;
michael@0 1639
michael@0 1640 case "Webapps:AutoUninstall":
michael@0 1641 WebappManager.autoUninstall(JSON.parse(aData));
michael@0 1642 break;
michael@0 1643 #endif
michael@0 1644
michael@0 1645 case "Locale:Changed":
michael@0 1646 // The value provided to Locale:Changed should be a BCP47 language tag
michael@0 1647 // understood by Gecko -- for example, "es-ES" or "de".
michael@0 1648 console.log("Locale:Changed: " + aData);
michael@0 1649
michael@0 1650 // TODO: do we need to be more nuanced here -- e.g., checking for the
michael@0 1651 // OS locale -- or should it always be false on Fennec?
michael@0 1652 Services.prefs.setBoolPref("intl.locale.matchOS", false);
michael@0 1653 Services.prefs.setCharPref("general.useragent.locale", aData);
michael@0 1654 break;
michael@0 1655
michael@0 1656 default:
michael@0 1657 dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n');
michael@0 1658 break;
michael@0 1659
michael@0 1660 }
michael@0 1661 },
michael@0 1662
michael@0 1663 get defaultBrowserWidth() {
michael@0 1664 delete this.defaultBrowserWidth;
michael@0 1665 let width = Services.prefs.getIntPref("browser.viewport.desktopWidth");
michael@0 1666 return this.defaultBrowserWidth = width;
michael@0 1667 },
michael@0 1668
michael@0 1669 // nsIAndroidBrowserApp
michael@0 1670 getBrowserTab: function(tabId) {
michael@0 1671 return this.getTabForId(tabId);
michael@0 1672 },
michael@0 1673
michael@0 1674 getUITelemetryObserver: function() {
michael@0 1675 return UITelemetry;
michael@0 1676 },
michael@0 1677
michael@0 1678 getPreferences: function getPreferences(requestId, prefNames, count) {
michael@0 1679 this.handlePreferencesRequest(requestId, prefNames, false);
michael@0 1680 },
michael@0 1681
michael@0 1682 observePreferences: function observePreferences(requestId, prefNames, count) {
michael@0 1683 this.handlePreferencesRequest(requestId, prefNames, true);
michael@0 1684 },
michael@0 1685
michael@0 1686 removePreferenceObservers: function removePreferenceObservers(aRequestId) {
michael@0 1687 let newPrefObservers = [];
michael@0 1688 for (let prefName in this._prefObservers) {
michael@0 1689 let requestIds = this._prefObservers[prefName];
michael@0 1690 // Remove the requestID from the preference handlers
michael@0 1691 let i = requestIds.indexOf(aRequestId);
michael@0 1692 if (i >= 0) {
michael@0 1693 requestIds.splice(i, 1);
michael@0 1694 }
michael@0 1695
michael@0 1696 // If there are no more request IDs, remove the observer
michael@0 1697 if (requestIds.length == 0) {
michael@0 1698 Services.prefs.removeObserver(prefName, this);
michael@0 1699 } else {
michael@0 1700 newPrefObservers[prefName] = requestIds;
michael@0 1701 }
michael@0 1702 }
michael@0 1703 this._prefObservers = newPrefObservers;
michael@0 1704 },
michael@0 1705
michael@0 1706 // This method will print a list from fromIndex to toIndex, optionally
michael@0 1707 // selecting selIndex(if fromIndex<=selIndex<=toIndex)
michael@0 1708 showHistory: function(fromIndex, toIndex, selIndex) {
michael@0 1709 let browser = this.selectedBrowser;
michael@0 1710 let hist = browser.sessionHistory;
michael@0 1711 let listitems = [];
michael@0 1712 for (let i = toIndex; i >= fromIndex; i--) {
michael@0 1713 let entry = hist.getEntryAtIndex(i, false);
michael@0 1714 let item = {
michael@0 1715 label: entry.title || entry.URI.spec,
michael@0 1716 selected: (i == selIndex)
michael@0 1717 };
michael@0 1718 listitems.push(item);
michael@0 1719 }
michael@0 1720
michael@0 1721 let p = new Prompt({
michael@0 1722 window: browser.contentWindow
michael@0 1723 }).setSingleChoiceItems(listitems).show(function(data) {
michael@0 1724 let selected = data.button;
michael@0 1725 if (selected == -1)
michael@0 1726 return;
michael@0 1727
michael@0 1728 browser.gotoIndex(toIndex-selected);
michael@0 1729 });
michael@0 1730 },
michael@0 1731 };
michael@0 1732
michael@0 1733 var NativeWindow = {
michael@0 1734 init: function() {
michael@0 1735 Services.obs.addObserver(this, "Menu:Clicked", false);
michael@0 1736 Services.obs.addObserver(this, "PageActions:Clicked", false);
michael@0 1737 Services.obs.addObserver(this, "PageActions:LongClicked", false);
michael@0 1738 Services.obs.addObserver(this, "Doorhanger:Reply", false);
michael@0 1739 Services.obs.addObserver(this, "Toast:Click", false);
michael@0 1740 Services.obs.addObserver(this, "Toast:Hidden", false);
michael@0 1741 this.contextmenus.init();
michael@0 1742 },
michael@0 1743
michael@0 1744 uninit: function() {
michael@0 1745 Services.obs.removeObserver(this, "Menu:Clicked");
michael@0 1746 Services.obs.removeObserver(this, "PageActions:Clicked");
michael@0 1747 Services.obs.removeObserver(this, "PageActions:LongClicked");
michael@0 1748 Services.obs.removeObserver(this, "Doorhanger:Reply");
michael@0 1749 Services.obs.removeObserver(this, "Toast:Click", false);
michael@0 1750 Services.obs.removeObserver(this, "Toast:Hidden", false);
michael@0 1751 this.contextmenus.uninit();
michael@0 1752 },
michael@0 1753
michael@0 1754 loadDex: function(zipFile, implClass) {
michael@0 1755 sendMessageToJava({
michael@0 1756 type: "Dex:Load",
michael@0 1757 zipfile: zipFile,
michael@0 1758 impl: implClass || "Main"
michael@0 1759 });
michael@0 1760 },
michael@0 1761
michael@0 1762 unloadDex: function(zipFile) {
michael@0 1763 sendMessageToJava({
michael@0 1764 type: "Dex:Unload",
michael@0 1765 zipfile: zipFile
michael@0 1766 });
michael@0 1767 },
michael@0 1768
michael@0 1769 toast: {
michael@0 1770 _callbacks: {},
michael@0 1771 show: function(aMessage, aDuration, aOptions) {
michael@0 1772 let msg = {
michael@0 1773 type: "Toast:Show",
michael@0 1774 message: aMessage,
michael@0 1775 duration: aDuration
michael@0 1776 };
michael@0 1777
michael@0 1778 if (aOptions && aOptions.button) {
michael@0 1779 msg.button = {
michael@0 1780 label: aOptions.button.label,
michael@0 1781 id: uuidgen.generateUUID().toString(),
michael@0 1782 // If the caller specified a button, make sure we convert any chrome urls
michael@0 1783 // to jar:jar urls so that the frontend can show them
michael@0 1784 icon: aOptions.button.icon ? resolveGeckoURI(aOptions.button.icon) : null,
michael@0 1785 };
michael@0 1786 this._callbacks[msg.button.id] = aOptions.button.callback;
michael@0 1787 }
michael@0 1788
michael@0 1789 sendMessageToJava(msg);
michael@0 1790 }
michael@0 1791 },
michael@0 1792
michael@0 1793 pageactions: {
michael@0 1794 _items: { },
michael@0 1795 add: function(aOptions) {
michael@0 1796 let id = uuidgen.generateUUID().toString();
michael@0 1797 sendMessageToJava({
michael@0 1798 type: "PageActions:Add",
michael@0 1799 id: id,
michael@0 1800 title: aOptions.title,
michael@0 1801 icon: resolveGeckoURI(aOptions.icon),
michael@0 1802 important: "important" in aOptions ? aOptions.important : false
michael@0 1803 });
michael@0 1804 this._items[id] = {
michael@0 1805 clickCallback: aOptions.clickCallback,
michael@0 1806 longClickCallback: aOptions.longClickCallback
michael@0 1807 };
michael@0 1808 return id;
michael@0 1809 },
michael@0 1810 remove: function(id) {
michael@0 1811 sendMessageToJava({
michael@0 1812 type: "PageActions:Remove",
michael@0 1813 id: id
michael@0 1814 });
michael@0 1815 delete this._items[id];
michael@0 1816 }
michael@0 1817 },
michael@0 1818
michael@0 1819 menu: {
michael@0 1820 _callbacks: [],
michael@0 1821 _menuId: 1,
michael@0 1822 toolsMenuID: -1,
michael@0 1823 add: function() {
michael@0 1824 let options;
michael@0 1825 if (arguments.length == 1) {
michael@0 1826 options = arguments[0];
michael@0 1827 } else if (arguments.length == 3) {
michael@0 1828 options = {
michael@0 1829 name: arguments[0],
michael@0 1830 icon: arguments[1],
michael@0 1831 callback: arguments[2]
michael@0 1832 };
michael@0 1833 } else {
michael@0 1834 throw "Incorrect number of parameters";
michael@0 1835 }
michael@0 1836
michael@0 1837 options.type = "Menu:Add";
michael@0 1838 options.id = this._menuId;
michael@0 1839
michael@0 1840 sendMessageToJava(options);
michael@0 1841 this._callbacks[this._menuId] = options.callback;
michael@0 1842 this._menuId++;
michael@0 1843 return this._menuId - 1;
michael@0 1844 },
michael@0 1845
michael@0 1846 remove: function(aId) {
michael@0 1847 sendMessageToJava({ type: "Menu:Remove", id: aId });
michael@0 1848 },
michael@0 1849
michael@0 1850 update: function(aId, aOptions) {
michael@0 1851 if (!aOptions)
michael@0 1852 return;
michael@0 1853
michael@0 1854 sendMessageToJava({
michael@0 1855 type: "Menu:Update",
michael@0 1856 id: aId,
michael@0 1857 options: aOptions
michael@0 1858 });
michael@0 1859 }
michael@0 1860 },
michael@0 1861
michael@0 1862 doorhanger: {
michael@0 1863 _callbacks: {},
michael@0 1864 _callbacksId: 0,
michael@0 1865 _promptId: 0,
michael@0 1866
michael@0 1867 /**
michael@0 1868 * @param aOptions
michael@0 1869 * An options JavaScript object holding additional properties for the
michael@0 1870 * notification. The following properties are currently supported:
michael@0 1871 * persistence: An integer. The notification will not automatically
michael@0 1872 * dismiss for this many page loads. If persistence is set
michael@0 1873 * to -1, the doorhanger will never automatically dismiss.
michael@0 1874 * persistWhileVisible:
michael@0 1875 * A boolean. If true, a visible notification will always
michael@0 1876 * persist across location changes.
michael@0 1877 * timeout: A time in milliseconds. The notification will not
michael@0 1878 * automatically dismiss before this time.
michael@0 1879 * checkbox: A string to appear next to a checkbox under the notification
michael@0 1880 * message. The button callback functions will be called with
michael@0 1881 * the checked state as an argument.
michael@0 1882 */
michael@0 1883 show: function(aMessage, aValue, aButtons, aTabID, aOptions) {
michael@0 1884 if (aButtons == null) {
michael@0 1885 aButtons = [];
michael@0 1886 }
michael@0 1887
michael@0 1888 aButtons.forEach((function(aButton) {
michael@0 1889 this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId };
michael@0 1890 aButton.callback = this._callbacksId;
michael@0 1891 this._callbacksId++;
michael@0 1892 }).bind(this));
michael@0 1893
michael@0 1894 this._promptId++;
michael@0 1895 let json = {
michael@0 1896 type: "Doorhanger:Add",
michael@0 1897 message: aMessage,
michael@0 1898 value: aValue,
michael@0 1899 buttons: aButtons,
michael@0 1900 // use the current tab if none is provided
michael@0 1901 tabID: aTabID || BrowserApp.selectedTab.id,
michael@0 1902 options: aOptions || {}
michael@0 1903 };
michael@0 1904 sendMessageToJava(json);
michael@0 1905 },
michael@0 1906
michael@0 1907 hide: function(aValue, aTabID) {
michael@0 1908 sendMessageToJava({
michael@0 1909 type: "Doorhanger:Remove",
michael@0 1910 value: aValue,
michael@0 1911 tabID: aTabID
michael@0 1912 });
michael@0 1913 }
michael@0 1914 },
michael@0 1915
michael@0 1916 observe: function(aSubject, aTopic, aData) {
michael@0 1917 if (aTopic == "Menu:Clicked") {
michael@0 1918 if (this.menu._callbacks[aData])
michael@0 1919 this.menu._callbacks[aData]();
michael@0 1920 } else if (aTopic == "PageActions:Clicked") {
michael@0 1921 if (this.pageactions._items[aData].clickCallback)
michael@0 1922 this.pageactions._items[aData].clickCallback();
michael@0 1923 } else if (aTopic == "PageActions:LongClicked") {
michael@0 1924 if (this.pageactions._items[aData].longClickCallback)
michael@0 1925 this.pageactions._items[aData].longClickCallback();
michael@0 1926 } else if (aTopic == "Toast:Click") {
michael@0 1927 if (this.toast._callbacks[aData]) {
michael@0 1928 this.toast._callbacks[aData]();
michael@0 1929 delete this.toast._callbacks[aData];
michael@0 1930 }
michael@0 1931 } else if (aTopic == "Toast:Hidden") {
michael@0 1932 if (this.toast._callbacks[aData])
michael@0 1933 delete this.toast._callbacks[aData];
michael@0 1934 } else if (aTopic == "Doorhanger:Reply") {
michael@0 1935 let data = JSON.parse(aData);
michael@0 1936 let reply_id = data["callback"];
michael@0 1937
michael@0 1938 if (this.doorhanger._callbacks[reply_id]) {
michael@0 1939 // Pass the value of the optional checkbox to the callback
michael@0 1940 let checked = data["checked"];
michael@0 1941 this.doorhanger._callbacks[reply_id].cb(checked, data.inputs);
michael@0 1942
michael@0 1943 let prompt = this.doorhanger._callbacks[reply_id].prompt;
michael@0 1944 for (let id in this.doorhanger._callbacks) {
michael@0 1945 if (this.doorhanger._callbacks[id].prompt == prompt) {
michael@0 1946 delete this.doorhanger._callbacks[id];
michael@0 1947 }
michael@0 1948 }
michael@0 1949 }
michael@0 1950 }
michael@0 1951 },
michael@0 1952 contextmenus: {
michael@0 1953 items: {}, // a list of context menu items that we may show
michael@0 1954 DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items
michael@0 1955
michael@0 1956 init: function() {
michael@0 1957 Services.obs.addObserver(this, "Gesture:LongPress", false);
michael@0 1958 },
michael@0 1959
michael@0 1960 uninit: function() {
michael@0 1961 Services.obs.removeObserver(this, "Gesture:LongPress");
michael@0 1962 },
michael@0 1963
michael@0 1964 add: function() {
michael@0 1965 let args;
michael@0 1966 if (arguments.length == 1) {
michael@0 1967 args = arguments[0];
michael@0 1968 } else if (arguments.length == 3) {
michael@0 1969 args = {
michael@0 1970 label : arguments[0],
michael@0 1971 selector: arguments[1],
michael@0 1972 callback: arguments[2]
michael@0 1973 };
michael@0 1974 } else {
michael@0 1975 throw "Incorrect number of parameters";
michael@0 1976 }
michael@0 1977
michael@0 1978 if (!args.label)
michael@0 1979 throw "Menu items must have a name";
michael@0 1980
michael@0 1981 let cmItem = new ContextMenuItem(args);
michael@0 1982 this.items[cmItem.id] = cmItem;
michael@0 1983 return cmItem.id;
michael@0 1984 },
michael@0 1985
michael@0 1986 remove: function(aId) {
michael@0 1987 delete this.items[aId];
michael@0 1988 },
michael@0 1989
michael@0 1990 SelectorContext: function(aSelector) {
michael@0 1991 return {
michael@0 1992 matches: function(aElt) {
michael@0 1993 if (aElt.mozMatchesSelector)
michael@0 1994 return aElt.mozMatchesSelector(aSelector);
michael@0 1995 return false;
michael@0 1996 }
michael@0 1997 };
michael@0 1998 },
michael@0 1999
michael@0 2000 linkOpenableNonPrivateContext: {
michael@0 2001 matches: function linkOpenableNonPrivateContextMatches(aElement) {
michael@0 2002 let doc = aElement.ownerDocument;
michael@0 2003 if (!doc || PrivateBrowsingUtils.isWindowPrivate(doc.defaultView)) {
michael@0 2004 return false;
michael@0 2005 }
michael@0 2006
michael@0 2007 return NativeWindow.contextmenus.linkOpenableContext.matches(aElement);
michael@0 2008 }
michael@0 2009 },
michael@0 2010
michael@0 2011 linkOpenableContext: {
michael@0 2012 matches: function linkOpenableContextMatches(aElement) {
michael@0 2013 let uri = NativeWindow.contextmenus._getLink(aElement);
michael@0 2014 if (uri) {
michael@0 2015 let scheme = uri.scheme;
michael@0 2016 let dontOpen = /^(javascript|mailto|news|snews|tel)$/;
michael@0 2017 return (scheme && !dontOpen.test(scheme));
michael@0 2018 }
michael@0 2019 return false;
michael@0 2020 }
michael@0 2021 },
michael@0 2022
michael@0 2023 linkCopyableContext: {
michael@0 2024 matches: function linkCopyableContextMatches(aElement) {
michael@0 2025 let uri = NativeWindow.contextmenus._getLink(aElement);
michael@0 2026 if (uri) {
michael@0 2027 let scheme = uri.scheme;
michael@0 2028 let dontCopy = /^(mailto|tel)$/;
michael@0 2029 return (scheme && !dontCopy.test(scheme));
michael@0 2030 }
michael@0 2031 return false;
michael@0 2032 }
michael@0 2033 },
michael@0 2034
michael@0 2035 linkShareableContext: {
michael@0 2036 matches: function linkShareableContextMatches(aElement) {
michael@0 2037 let uri = NativeWindow.contextmenus._getLink(aElement);
michael@0 2038 if (uri) {
michael@0 2039 let scheme = uri.scheme;
michael@0 2040 let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/;
michael@0 2041 return (scheme && !dontShare.test(scheme));
michael@0 2042 }
michael@0 2043 return false;
michael@0 2044 }
michael@0 2045 },
michael@0 2046
michael@0 2047 linkBookmarkableContext: {
michael@0 2048 matches: function linkBookmarkableContextMatches(aElement) {
michael@0 2049 let uri = NativeWindow.contextmenus._getLink(aElement);
michael@0 2050 if (uri) {
michael@0 2051 let scheme = uri.scheme;
michael@0 2052 let dontBookmark = /^(mailto|tel)$/;
michael@0 2053 return (scheme && !dontBookmark.test(scheme));
michael@0 2054 }
michael@0 2055 return false;
michael@0 2056 }
michael@0 2057 },
michael@0 2058
michael@0 2059 emailLinkContext: {
michael@0 2060 matches: function emailLinkContextMatches(aElement) {
michael@0 2061 let uri = NativeWindow.contextmenus._getLink(aElement);
michael@0 2062 if (uri)
michael@0 2063 return uri.schemeIs("mailto");
michael@0 2064 return false;
michael@0 2065 }
michael@0 2066 },
michael@0 2067
michael@0 2068 phoneNumberLinkContext: {
michael@0 2069 matches: function phoneNumberLinkContextMatches(aElement) {
michael@0 2070 let uri = NativeWindow.contextmenus._getLink(aElement);
michael@0 2071 if (uri)
michael@0 2072 return uri.schemeIs("tel");
michael@0 2073 return false;
michael@0 2074 }
michael@0 2075 },
michael@0 2076
michael@0 2077 imageLocationCopyableContext: {
michael@0 2078 matches: function imageLinkCopyableContextMatches(aElement) {
michael@0 2079 return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI);
michael@0 2080 }
michael@0 2081 },
michael@0 2082
michael@0 2083 imageSaveableContext: {
michael@0 2084 matches: function imageSaveableContextMatches(aElement) {
michael@0 2085 if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) {
michael@0 2086 // The image must be loaded to allow saving
michael@0 2087 let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
michael@0 2088 return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE));
michael@0 2089 }
michael@0 2090 return false;
michael@0 2091 }
michael@0 2092 },
michael@0 2093
michael@0 2094 mediaSaveableContext: {
michael@0 2095 matches: function mediaSaveableContextMatches(aElement) {
michael@0 2096 return (aElement instanceof HTMLVideoElement ||
michael@0 2097 aElement instanceof HTMLAudioElement);
michael@0 2098 }
michael@0 2099 },
michael@0 2100
michael@0 2101 mediaContext: function(aMode) {
michael@0 2102 return {
michael@0 2103 matches: function(aElt) {
michael@0 2104 if (aElt instanceof Ci.nsIDOMHTMLMediaElement) {
michael@0 2105 let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE;
michael@0 2106 if (hasError)
michael@0 2107 return false;
michael@0 2108
michael@0 2109 let paused = aElt.paused || aElt.ended;
michael@0 2110 if (paused && aMode == "media-paused")
michael@0 2111 return true;
michael@0 2112 if (!paused && aMode == "media-playing")
michael@0 2113 return true;
michael@0 2114 let controls = aElt.controls;
michael@0 2115 if (!controls && aMode == "media-hidingcontrols")
michael@0 2116 return true;
michael@0 2117
michael@0 2118 let muted = aElt.muted;
michael@0 2119 if (muted && aMode == "media-muted")
michael@0 2120 return true;
michael@0 2121 else if (!muted && aMode == "media-unmuted")
michael@0 2122 return true;
michael@0 2123 }
michael@0 2124 return false;
michael@0 2125 }
michael@0 2126 };
michael@0 2127 },
michael@0 2128
michael@0 2129 /* Holds a WeakRef to the original target element this context menu was shown for.
michael@0 2130 * Most API's will have to walk up the tree from this node to find the correct element
michael@0 2131 * to act on
michael@0 2132 */
michael@0 2133 get _target() {
michael@0 2134 if (this._targetRef)
michael@0 2135 return this._targetRef.get();
michael@0 2136 return null;
michael@0 2137 },
michael@0 2138
michael@0 2139 set _target(aTarget) {
michael@0 2140 if (aTarget)
michael@0 2141 this._targetRef = Cu.getWeakReference(aTarget);
michael@0 2142 else this._targetRef = null;
michael@0 2143 },
michael@0 2144
michael@0 2145 get defaultContext() {
michael@0 2146 delete this.defaultContext;
michael@0 2147 return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default");
michael@0 2148 },
michael@0 2149
michael@0 2150 /* Gets menuitems for an arbitrary node
michael@0 2151 * Parameters:
michael@0 2152 * element - The element to look at. If this element has a contextmenu attribute, the
michael@0 2153 * corresponding contextmenu will be used.
michael@0 2154 */
michael@0 2155 _getHTMLContextMenuItemsForElement: function(element) {
michael@0 2156 let htmlMenu = element.contextMenu;
michael@0 2157 if (!htmlMenu) {
michael@0 2158 return [];
michael@0 2159 }
michael@0 2160
michael@0 2161 htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
michael@0 2162 htmlMenu.sendShowEvent();
michael@0 2163
michael@0 2164 return this._getHTMLContextMenuItemsForMenu(htmlMenu, element);
michael@0 2165 },
michael@0 2166
michael@0 2167 /* Add a menuitem for an HTML <menu> node
michael@0 2168 * Parameters:
michael@0 2169 * menu - The <menu> element to iterate through for menuitems
michael@0 2170 * target - The target element these context menu items are attached to
michael@0 2171 */
michael@0 2172 _getHTMLContextMenuItemsForMenu: function(menu, target) {
michael@0 2173 let items = [];
michael@0 2174 for (let i = 0; i < menu.childNodes.length; i++) {
michael@0 2175 let elt = menu.childNodes[i];
michael@0 2176 if (!elt.label)
michael@0 2177 continue;
michael@0 2178
michael@0 2179 items.push(new HTMLContextMenuItem(elt, target));
michael@0 2180 }
michael@0 2181
michael@0 2182 return items;
michael@0 2183 },
michael@0 2184
michael@0 2185 // Searches the current list of menuitems to show for any that match this id
michael@0 2186 _findMenuItem: function(aId) {
michael@0 2187 if (!this.menus) {
michael@0 2188 return null;
michael@0 2189 }
michael@0 2190
michael@0 2191 for (let context in this.menus) {
michael@0 2192 let menu = this.menus[context];
michael@0 2193 for (let i = 0; i < menu.length; i++) {
michael@0 2194 if (menu[i].id === aId) {
michael@0 2195 return menu[i];
michael@0 2196 }
michael@0 2197 }
michael@0 2198 }
michael@0 2199 return null;
michael@0 2200 },
michael@0 2201
michael@0 2202 // Returns true if there are any context menu items to show
michael@0 2203 shouldShow: function() {
michael@0 2204 for (let context in this.menus) {
michael@0 2205 let menu = this.menus[context];
michael@0 2206 if (menu.length > 0) {
michael@0 2207 return true;
michael@0 2208 }
michael@0 2209 }
michael@0 2210 return false;
michael@0 2211 },
michael@0 2212
michael@0 2213 /* Returns a label to be shown in a tabbed ui if there are multiple "contexts". For instance, if this
michael@0 2214 * is an image inside an <a> tag, we may have a "link" context and an "image" one.
michael@0 2215 */
michael@0 2216 _getContextType: function(element) {
michael@0 2217 // For anchor nodes, we try to use the scheme to pick a string
michael@0 2218 if (element instanceof Ci.nsIDOMHTMLAnchorElement) {
michael@0 2219 let uri = this.makeURI(this._getLinkURL(element));
michael@0 2220 try {
michael@0 2221 return Strings.browser.GetStringFromName("browser.menu.context." + uri.scheme);
michael@0 2222 } catch(ex) { }
michael@0 2223 }
michael@0 2224
michael@0 2225 // Otherwise we try the nodeName
michael@0 2226 try {
michael@0 2227 return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase());
michael@0 2228 } catch(ex) { }
michael@0 2229
michael@0 2230 // Fallback to the default
michael@0 2231 return this.defaultContext;
michael@0 2232 },
michael@0 2233
michael@0 2234 // Adds context menu items added through the add-on api
michael@0 2235 _getNativeContextMenuItems: function(element, x, y) {
michael@0 2236 let res = [];
michael@0 2237 for (let itemId of Object.keys(this.items)) {
michael@0 2238 let item = this.items[itemId];
michael@0 2239
michael@0 2240 if (!this._findMenuItem(item.id) && item.matches(element, x, y)) {
michael@0 2241 res.push(item);
michael@0 2242 }
michael@0 2243 }
michael@0 2244
michael@0 2245 return res;
michael@0 2246 },
michael@0 2247
michael@0 2248 /* Checks if there are context menu items to show, and if it finds them
michael@0 2249 * sends a contextmenu event to content. We also send showing events to
michael@0 2250 * any html5 context menus we are about to show, and fire some local notifications
michael@0 2251 * for chrome consumers to do lazy menuitem construction
michael@0 2252 */
michael@0 2253 _sendToContent: function(x, y) {
michael@0 2254 let target = BrowserEventHandler._highlightElement || ElementTouchHelper.elementFromPoint(x, y);
michael@0 2255 if (!target)
michael@0 2256 target = ElementTouchHelper.anyElementFromPoint(x, y);
michael@0 2257
michael@0 2258 if (!target)
michael@0 2259 return;
michael@0 2260
michael@0 2261 this._target = target;
michael@0 2262
michael@0 2263 Services.obs.notifyObservers(null, "before-build-contextmenu", "");
michael@0 2264 this._buildMenu(x, y);
michael@0 2265
michael@0 2266 // only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap)
michael@0 2267 if (this.shouldShow()) {
michael@0 2268 let event = target.ownerDocument.createEvent("MouseEvent");
michael@0 2269 event.initMouseEvent("contextmenu", true, true, target.defaultView,
michael@0 2270 0, x, y, x, y, false, false, false, false,
michael@0 2271 0, null);
michael@0 2272 target.ownerDocument.defaultView.addEventListener("contextmenu", this, false);
michael@0 2273 target.dispatchEvent(event);
michael@0 2274 } else {
michael@0 2275 this.menus = null;
michael@0 2276 Services.obs.notifyObservers({target: target, x: x, y: y}, "context-menu-not-shown", "");
michael@0 2277
michael@0 2278 if (SelectionHandler.canSelect(target)) {
michael@0 2279 if (!SelectionHandler.startSelection(target, {
michael@0 2280 mode: SelectionHandler.SELECT_AT_POINT,
michael@0 2281 x: x,
michael@0 2282 y: y
michael@0 2283 })) {
michael@0 2284 SelectionHandler.attachCaret(target);
michael@0 2285 }
michael@0 2286 }
michael@0 2287 }
michael@0 2288 },
michael@0 2289
michael@0 2290 // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
michael@0 2291 _getTitle: function(node) {
michael@0 2292 if (node.hasAttribute && node.hasAttribute("title")) {
michael@0 2293 return node.getAttribute("title");
michael@0 2294 }
michael@0 2295 return this._getUrl(node);
michael@0 2296 },
michael@0 2297
michael@0 2298 // Returns a url associated with a node
michael@0 2299 _getUrl: function(node) {
michael@0 2300 if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) ||
michael@0 2301 (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) {
michael@0 2302 return this._getLinkURL(node);
michael@0 2303 } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) {
michael@0 2304 return node.currentURI.spec;
michael@0 2305 } else if (node instanceof Ci.nsIDOMHTMLMediaElement) {
michael@0 2306 return (node.currentSrc || node.src);
michael@0 2307 }
michael@0 2308
michael@0 2309 return "";
michael@0 2310 },
michael@0 2311
michael@0 2312 // Adds an array of menuitems to the current list of items to show, in the correct context
michael@0 2313 _addMenuItems: function(items, context) {
michael@0 2314 if (!this.menus[context]) {
michael@0 2315 this.menus[context] = [];
michael@0 2316 }
michael@0 2317 this.menus[context] = this.menus[context].concat(items);
michael@0 2318 },
michael@0 2319
michael@0 2320 /* Does the basic work of building a context menu to show. Will combine HTML and Native
michael@0 2321 * context menus items, as well as sorting menuitems into different menus based on context.
michael@0 2322 */
michael@0 2323 _buildMenu: function(x, y) {
michael@0 2324 // now walk up the tree and for each node look for any context menu items that apply
michael@0 2325 let element = this._target;
michael@0 2326
michael@0 2327 // this.menus holds a hashmap of "contexts" to menuitems associated with that context
michael@0 2328 // For instance, if the user taps an image inside a link, we'll have something like:
michael@0 2329 // {
michael@0 2330 // link: [ ContextMenuItem, ContextMenuItem ]
michael@0 2331 // image: [ ContextMenuItem, ContextMenuItem ]
michael@0 2332 // }
michael@0 2333 this.menus = {};
michael@0 2334
michael@0 2335 while (element) {
michael@0 2336 let context = this._getContextType(element);
michael@0 2337
michael@0 2338 // First check for any html5 context menus that might exist...
michael@0 2339 var items = this._getHTMLContextMenuItemsForElement(element);
michael@0 2340 if (items.length > 0) {
michael@0 2341 this._addMenuItems(items, context);
michael@0 2342 }
michael@0 2343
michael@0 2344 // then check for any context menu items registered in the ui.
michael@0 2345 items = this._getNativeContextMenuItems(element, x, y);
michael@0 2346 if (items.length > 0) {
michael@0 2347 this._addMenuItems(items, context);
michael@0 2348 }
michael@0 2349
michael@0 2350 // walk up the tree and find more items to show
michael@0 2351 element = element.parentNode;
michael@0 2352 }
michael@0 2353 },
michael@0 2354
michael@0 2355 // Actually shows the native context menu by passing a list of context menu items to
michael@0 2356 // show to the Java.
michael@0 2357 _show: function(aEvent) {
michael@0 2358 let popupNode = this._target;
michael@0 2359 this._target = null;
michael@0 2360 if (aEvent.defaultPrevented || !popupNode) {
michael@0 2361 return;
michael@0 2362 }
michael@0 2363 this._innerShow(popupNode, aEvent.clientX, aEvent.clientY);
michael@0 2364 },
michael@0 2365
michael@0 2366 // Walks the DOM tree to find a title from a node
michael@0 2367 _findTitle: function(node) {
michael@0 2368 let title = "";
michael@0 2369 while(node && !title) {
michael@0 2370 title = this._getTitle(node);
michael@0 2371 node = node.parentNode;
michael@0 2372 }
michael@0 2373 return title;
michael@0 2374 },
michael@0 2375
michael@0 2376 /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm
michael@0 2377 * If there is one menu, will return a flat array of menuitems. If there are multiple
michael@0 2378 * menus, will return an array with appropriate tabs/items inside it. i.e. :
michael@0 2379 * [
michael@0 2380 * { label: "link", items: [...] },
michael@0 2381 * { label: "image", items: [...] }
michael@0 2382 * ]
michael@0 2383 */
michael@0 2384 _reformatList: function(target) {
michael@0 2385 let contexts = Object.keys(this.menus);
michael@0 2386
michael@0 2387 if (contexts.length === 1) {
michael@0 2388 // If there's only one context, we'll only show a single flat single select list
michael@0 2389 return this._reformatMenuItems(target, this.menus[contexts[0]]);
michael@0 2390 }
michael@0 2391
michael@0 2392 // If there are multiple contexts, we'll only show a tabbed ui with multiple lists
michael@0 2393 return this._reformatListAsTabs(target, this.menus);
michael@0 2394 },
michael@0 2395
michael@0 2396 /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm's
michael@0 2397 * addTabs method. i.e. :
michael@0 2398 * { link: [...], image: [...] } becomes
michael@0 2399 * [ { label: "link", items: [...] } ]
michael@0 2400 *
michael@0 2401 * Also reformats items and resolves any parmaeters that aren't known until display time
michael@0 2402 * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
michael@0 2403 */
michael@0 2404 _reformatListAsTabs: function(target, menus) {
michael@0 2405 let itemArray = [];
michael@0 2406
michael@0 2407 // Sort the keys so that "link" is always first
michael@0 2408 let contexts = Object.keys(this.menus);
michael@0 2409 contexts.sort((context1, context2) => {
michael@0 2410 if (context1 === this.defaultContext) {
michael@0 2411 return -1;
michael@0 2412 } else if (context2 === this.defaultContext) {
michael@0 2413 return 1;
michael@0 2414 }
michael@0 2415 return 0;
michael@0 2416 });
michael@0 2417
michael@0 2418 contexts.forEach(context => {
michael@0 2419 itemArray.push({
michael@0 2420 label: context,
michael@0 2421 items: this._reformatMenuItems(target, menus[context])
michael@0 2422 });
michael@0 2423 });
michael@0 2424
michael@0 2425 return itemArray;
michael@0 2426 },
michael@0 2427
michael@0 2428 /* Reformats an array of ContextMenuItems into an array that can be handled by Prompt.jsm. Also reformats items
michael@0 2429 * and resolves any parmaeters that aren't known until display time
michael@0 2430 * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link).
michael@0 2431 */
michael@0 2432 _reformatMenuItems: function(target, menuitems) {
michael@0 2433 let itemArray = [];
michael@0 2434
michael@0 2435 for (let i = 0; i < menuitems.length; i++) {
michael@0 2436 let t = target;
michael@0 2437 while(t) {
michael@0 2438 if (menuitems[i].matches(t)) {
michael@0 2439 let val = menuitems[i].getValue(t);
michael@0 2440
michael@0 2441 // hidden menu items will return null from getValue
michael@0 2442 if (val) {
michael@0 2443 itemArray.push(val);
michael@0 2444 break;
michael@0 2445 }
michael@0 2446 }
michael@0 2447
michael@0 2448 t = t.parentNode;
michael@0 2449 }
michael@0 2450 }
michael@0 2451
michael@0 2452 return itemArray;
michael@0 2453 },
michael@0 2454
michael@0 2455 // Called where we're finally ready to actually show the contextmenu. Sorts the items and shows a prompt.
michael@0 2456 _innerShow: function(target, x, y) {
michael@0 2457 Haptic.performSimpleAction(Haptic.LongPress);
michael@0 2458
michael@0 2459 // spin through the tree looking for a title for this context menu
michael@0 2460 let title = this._findTitle(target);
michael@0 2461
michael@0 2462 for (let context in this.menus) {
michael@0 2463 let menu = this.menus[context];
michael@0 2464 menu.sort((a,b) => {
michael@0 2465 if (a.order === b.order) {
michael@0 2466 return 0;
michael@0 2467 }
michael@0 2468 return (a.order > b.order) ? 1 : -1;
michael@0 2469 });
michael@0 2470 }
michael@0 2471
michael@0 2472 let useTabs = Object.keys(this.menus).length > 1;
michael@0 2473 let prompt = new Prompt({
michael@0 2474 window: target.ownerDocument.defaultView,
michael@0 2475 title: useTabs ? undefined : title
michael@0 2476 });
michael@0 2477
michael@0 2478 let items = this._reformatList(target);
michael@0 2479 if (useTabs) {
michael@0 2480 prompt.addTabs({
michael@0 2481 id: "tabs",
michael@0 2482 items: items
michael@0 2483 });
michael@0 2484 } else {
michael@0 2485 prompt.setSingleChoiceItems(items);
michael@0 2486 }
michael@0 2487
michael@0 2488 prompt.show(this._promptDone.bind(this, target, x, y, items));
michael@0 2489 },
michael@0 2490
michael@0 2491 // Called when the contextmenu prompt is closed
michael@0 2492 _promptDone: function(target, x, y, items, data) {
michael@0 2493 if (data.button == -1) {
michael@0 2494 // Prompt was cancelled, or an ActionView was used.
michael@0 2495 return;
michael@0 2496 }
michael@0 2497
michael@0 2498 let selectedItemId;
michael@0 2499 if (data.tabs) {
michael@0 2500 let menu = items[data.tabs.tab];
michael@0 2501 selectedItemId = menu.items[data.tabs.item].id;
michael@0 2502 } else {
michael@0 2503 selectedItemId = items[data.list[0]].id
michael@0 2504 }
michael@0 2505
michael@0 2506 let selectedItem = this._findMenuItem(selectedItemId);
michael@0 2507 this.menus = null;
michael@0 2508
michael@0 2509 if (!selectedItem || !selectedItem.matches || !selectedItem.callback) {
michael@0 2510 return;
michael@0 2511 }
michael@0 2512
michael@0 2513 // for menuitems added using the native UI, pass the dom element that matched that item to the callback
michael@0 2514 while (target) {
michael@0 2515 if (selectedItem.matches(target, x, y)) {
michael@0 2516 selectedItem.callback(target, x, y);
michael@0 2517 break;
michael@0 2518 }
michael@0 2519 target = target.parentNode;
michael@0 2520 }
michael@0 2521 },
michael@0 2522
michael@0 2523 // Called when the contextmenu is done propagating to content. If the event wasn't cancelled, will show a contextmenu.
michael@0 2524 handleEvent: function(aEvent) {
michael@0 2525 BrowserEventHandler._cancelTapHighlight();
michael@0 2526 aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false);
michael@0 2527 this._show(aEvent);
michael@0 2528 },
michael@0 2529
michael@0 2530 // Called when a long press is observed in the native Java frontend. Will start the process of generating/showing a contextmenu.
michael@0 2531 observe: function(aSubject, aTopic, aData) {
michael@0 2532 let data = JSON.parse(aData);
michael@0 2533 // content gets first crack at cancelling context menus
michael@0 2534 this._sendToContent(data.x, data.y);
michael@0 2535 },
michael@0 2536
michael@0 2537 // XXX - These are stolen from Util.js, we should remove them if we bring it back
michael@0 2538 makeURLAbsolute: function makeURLAbsolute(base, url) {
michael@0 2539 // Note: makeURI() will throw if url is not a valid URI
michael@0 2540 return this.makeURI(url, null, this.makeURI(base)).spec;
michael@0 2541 },
michael@0 2542
michael@0 2543 makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
michael@0 2544 return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
michael@0 2545 },
michael@0 2546
michael@0 2547 _getLink: function(aElement) {
michael@0 2548 if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
michael@0 2549 ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
michael@0 2550 (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) ||
michael@0 2551 aElement instanceof Ci.nsIDOMHTMLLinkElement ||
michael@0 2552 aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) {
michael@0 2553 try {
michael@0 2554 let url = this._getLinkURL(aElement);
michael@0 2555 return Services.io.newURI(url, null, null);
michael@0 2556 } catch (e) {}
michael@0 2557 }
michael@0 2558 return null;
michael@0 2559 },
michael@0 2560
michael@0 2561 _disableInGuest: function _disableInGuest(selector) {
michael@0 2562 return {
michael@0 2563 matches: function _disableInGuestMatches(aElement, aX, aY) {
michael@0 2564 if (BrowserApp.isGuest)
michael@0 2565 return false;
michael@0 2566 return selector.matches(aElement, aX, aY);
michael@0 2567 }
michael@0 2568 };
michael@0 2569 },
michael@0 2570
michael@0 2571 _getLinkURL: function ch_getLinkURL(aLink) {
michael@0 2572 let href = aLink.href;
michael@0 2573 if (href)
michael@0 2574 return href;
michael@0 2575
michael@0 2576 href = aLink.getAttributeNS(kXLinkNamespace, "href");
michael@0 2577 if (!href || !href.match(/\S/)) {
michael@0 2578 // Without this we try to save as the current doc,
michael@0 2579 // for example, HTML case also throws if empty
michael@0 2580 throw "Empty href";
michael@0 2581 }
michael@0 2582
michael@0 2583 return this.makeURLAbsolute(aLink.baseURI, href);
michael@0 2584 },
michael@0 2585
michael@0 2586 _copyStringToDefaultClipboard: function(aString) {
michael@0 2587 let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
michael@0 2588 clipboard.copyString(aString);
michael@0 2589 },
michael@0 2590
michael@0 2591 _shareStringWithDefault: function(aSharedString, aTitle) {
michael@0 2592 let sharing = Cc["@mozilla.org/uriloader/external-sharing-app-service;1"].getService(Ci.nsIExternalSharingAppService);
michael@0 2593 sharing.shareWithDefault(aSharedString, "text/plain", aTitle);
michael@0 2594 },
michael@0 2595
michael@0 2596 _stripScheme: function(aString) {
michael@0 2597 let index = aString.indexOf(":");
michael@0 2598 return aString.slice(index + 1);
michael@0 2599 }
michael@0 2600 }
michael@0 2601 };
michael@0 2602
michael@0 2603 var LightWeightThemeWebInstaller = {
michael@0 2604 init: function sh_init() {
michael@0 2605 let temp = {};
michael@0 2606 Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp);
michael@0 2607 let theme = new temp.LightweightThemeConsumer(document);
michael@0 2608 BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
michael@0 2609 BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
michael@0 2610 BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
michael@0 2611 },
michael@0 2612
michael@0 2613 uninit: function() {
michael@0 2614 BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true);
michael@0 2615 BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true);
michael@0 2616 BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true);
michael@0 2617 },
michael@0 2618
michael@0 2619 handleEvent: function (event) {
michael@0 2620 switch (event.type) {
michael@0 2621 case "InstallBrowserTheme":
michael@0 2622 case "PreviewBrowserTheme":
michael@0 2623 case "ResetBrowserThemePreview":
michael@0 2624 // ignore requests from background tabs
michael@0 2625 if (event.target.ownerDocument.defaultView.top != content)
michael@0 2626 return;
michael@0 2627 }
michael@0 2628
michael@0 2629 switch (event.type) {
michael@0 2630 case "InstallBrowserTheme":
michael@0 2631 this._installRequest(event);
michael@0 2632 break;
michael@0 2633 case "PreviewBrowserTheme":
michael@0 2634 this._preview(event);
michael@0 2635 break;
michael@0 2636 case "ResetBrowserThemePreview":
michael@0 2637 this._resetPreview(event);
michael@0 2638 break;
michael@0 2639 case "pagehide":
michael@0 2640 case "TabSelect":
michael@0 2641 this._resetPreview();
michael@0 2642 break;
michael@0 2643 }
michael@0 2644 },
michael@0 2645
michael@0 2646 get _manager () {
michael@0 2647 let temp = {};
michael@0 2648 Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
michael@0 2649 delete this._manager;
michael@0 2650 return this._manager = temp.LightweightThemeManager;
michael@0 2651 },
michael@0 2652
michael@0 2653 _installRequest: function (event) {
michael@0 2654 let node = event.target;
michael@0 2655 let data = this._getThemeFromNode(node);
michael@0 2656 if (!data)
michael@0 2657 return;
michael@0 2658
michael@0 2659 if (this._isAllowed(node)) {
michael@0 2660 this._install(data);
michael@0 2661 return;
michael@0 2662 }
michael@0 2663
michael@0 2664 let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton");
michael@0 2665 let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1);
michael@0 2666 let buttons = [{
michael@0 2667 label: allowButtonText,
michael@0 2668 callback: function () {
michael@0 2669 LightWeightThemeWebInstaller._install(data);
michael@0 2670 }
michael@0 2671 }];
michael@0 2672
michael@0 2673 NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id);
michael@0 2674 },
michael@0 2675
michael@0 2676 _install: function (newLWTheme) {
michael@0 2677 this._manager.currentTheme = newLWTheme;
michael@0 2678 },
michael@0 2679
michael@0 2680 _previewWindow: null,
michael@0 2681 _preview: function (event) {
michael@0 2682 if (!this._isAllowed(event.target))
michael@0 2683 return;
michael@0 2684 let data = this._getThemeFromNode(event.target);
michael@0 2685 if (!data)
michael@0 2686 return;
michael@0 2687 this._resetPreview();
michael@0 2688
michael@0 2689 this._previewWindow = event.target.ownerDocument.defaultView;
michael@0 2690 this._previewWindow.addEventListener("pagehide", this, true);
michael@0 2691 BrowserApp.deck.addEventListener("TabSelect", this, false);
michael@0 2692 this._manager.previewTheme(data);
michael@0 2693 },
michael@0 2694
michael@0 2695 _resetPreview: function (event) {
michael@0 2696 if (!this._previewWindow ||
michael@0 2697 event && !this._isAllowed(event.target))
michael@0 2698 return;
michael@0 2699
michael@0 2700 this._previewWindow.removeEventListener("pagehide", this, true);
michael@0 2701 this._previewWindow = null;
michael@0 2702 BrowserApp.deck.removeEventListener("TabSelect", this, false);
michael@0 2703
michael@0 2704 this._manager.resetPreview();
michael@0 2705 },
michael@0 2706
michael@0 2707 _isAllowed: function (node) {
michael@0 2708 let pm = Services.perms;
michael@0 2709
michael@0 2710 let uri = node.ownerDocument.documentURIObject;
michael@0 2711 return pm.testPermission(uri, "install") == pm.ALLOW_ACTION;
michael@0 2712 },
michael@0 2713
michael@0 2714 _getThemeFromNode: function (node) {
michael@0 2715 return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI);
michael@0 2716 }
michael@0 2717 };
michael@0 2718
michael@0 2719 var DesktopUserAgent = {
michael@0 2720 DESKTOP_UA: null,
michael@0 2721
michael@0 2722 init: function ua_init() {
michael@0 2723 Services.obs.addObserver(this, "DesktopMode:Change", false);
michael@0 2724 UserAgentOverrides.addComplexOverride(this.onRequest.bind(this));
michael@0 2725
michael@0 2726 // See https://developer.mozilla.org/en/Gecko_user_agent_string_reference
michael@0 2727 this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"]
michael@0 2728 .getService(Ci.nsIHttpProtocolHandler).userAgent
michael@0 2729 .replace(/Android; [a-zA-Z]+/, "X11; Linux x86_64")
michael@0 2730 .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101");
michael@0 2731 },
michael@0 2732
michael@0 2733 uninit: function ua_uninit() {
michael@0 2734 Services.obs.removeObserver(this, "DesktopMode:Change");
michael@0 2735 },
michael@0 2736
michael@0 2737 onRequest: function(channel, defaultUA) {
michael@0 2738 let channelWindow = this._getWindowForRequest(channel);
michael@0 2739 let tab = BrowserApp.getTabForWindow(channelWindow);
michael@0 2740 if (tab == null)
michael@0 2741 return null;
michael@0 2742
michael@0 2743 return this.getUserAgentForTab(tab);
michael@0 2744 },
michael@0 2745
michael@0 2746 getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) {
michael@0 2747 let tab = BrowserApp.getTabForWindow(aWindow.top);
michael@0 2748 if (tab)
michael@0 2749 return this.getUserAgentForTab(tab);
michael@0 2750
michael@0 2751 return null;
michael@0 2752 },
michael@0 2753
michael@0 2754 getUserAgentForTab: function ua_getUserAgentForTab(aTab) {
michael@0 2755 // Send desktop UA if "Request Desktop Site" is enabled.
michael@0 2756 if (aTab.desktopMode)
michael@0 2757 return this.DESKTOP_UA;
michael@0 2758
michael@0 2759 return null;
michael@0 2760 },
michael@0 2761
michael@0 2762 _getRequestLoadContext: function ua_getRequestLoadContext(aRequest) {
michael@0 2763 if (aRequest && aRequest.notificationCallbacks) {
michael@0 2764 try {
michael@0 2765 return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext);
michael@0 2766 } catch (ex) { }
michael@0 2767 }
michael@0 2768
michael@0 2769 if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) {
michael@0 2770 try {
michael@0 2771 return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext);
michael@0 2772 } catch (ex) { }
michael@0 2773 }
michael@0 2774
michael@0 2775 return null;
michael@0 2776 },
michael@0 2777
michael@0 2778 _getWindowForRequest: function ua_getWindowForRequest(aRequest) {
michael@0 2779 let loadContext = this._getRequestLoadContext(aRequest);
michael@0 2780 if (loadContext) {
michael@0 2781 try {
michael@0 2782 return loadContext.associatedWindow;
michael@0 2783 } catch (e) {
michael@0 2784 // loadContext.associatedWindow can throw when there's no window
michael@0 2785 }
michael@0 2786 }
michael@0 2787 return null;
michael@0 2788 },
michael@0 2789
michael@0 2790 observe: function ua_observe(aSubject, aTopic, aData) {
michael@0 2791 if (aTopic === "DesktopMode:Change") {
michael@0 2792 let args = JSON.parse(aData);
michael@0 2793 let tab = BrowserApp.getTabForId(args.tabId);
michael@0 2794 if (tab != null)
michael@0 2795 tab.reloadWithMode(args.desktopMode);
michael@0 2796 }
michael@0 2797 }
michael@0 2798 };
michael@0 2799
michael@0 2800
michael@0 2801 function nsBrowserAccess() {
michael@0 2802 }
michael@0 2803
michael@0 2804 nsBrowserAccess.prototype = {
michael@0 2805 QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]),
michael@0 2806
michael@0 2807 _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aContext) {
michael@0 2808 let isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
michael@0 2809 if (isExternal && aURI && aURI.schemeIs("chrome"))
michael@0 2810 return null;
michael@0 2811
michael@0 2812 let loadflags = isExternal ?
michael@0 2813 Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
michael@0 2814 Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
michael@0 2815 if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
michael@0 2816 switch (aContext) {
michael@0 2817 case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL:
michael@0 2818 aWhere = Services.prefs.getIntPref("browser.link.open_external");
michael@0 2819 break;
michael@0 2820 default: // OPEN_NEW or an illegal value
michael@0 2821 aWhere = Services.prefs.getIntPref("browser.link.open_newwindow");
michael@0 2822 }
michael@0 2823 }
michael@0 2824
michael@0 2825 Services.io.offline = false;
michael@0 2826
michael@0 2827 let referrer;
michael@0 2828 if (aOpener) {
michael@0 2829 try {
michael@0 2830 let location = aOpener.location;
michael@0 2831 referrer = Services.io.newURI(location, null, null);
michael@0 2832 } catch(e) { }
michael@0 2833 }
michael@0 2834
michael@0 2835 let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
michael@0 2836 let pinned = false;
michael@0 2837
michael@0 2838 if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) {
michael@0 2839 pinned = true;
michael@0 2840 let spec = aURI.spec;
michael@0 2841 let tabs = BrowserApp.tabs;
michael@0 2842 for (let i = 0; i < tabs.length; i++) {
michael@0 2843 let appOrigin = ss.getTabValue(tabs[i], "appOrigin");
michael@0 2844 if (appOrigin == spec) {
michael@0 2845 let tab = tabs[i];
michael@0 2846 BrowserApp.selectTab(tab);
michael@0 2847 return tab.browser;
michael@0 2848 }
michael@0 2849 }
michael@0 2850 }
michael@0 2851
michael@0 2852 let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW ||
michael@0 2853 aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB ||
michael@0 2854 aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB);
michael@0 2855 let isPrivate = false;
michael@0 2856
michael@0 2857 if (newTab) {
michael@0 2858 let parentId = -1;
michael@0 2859 if (!isExternal && aOpener) {
michael@0 2860 let parent = BrowserApp.getTabForWindow(aOpener.top);
michael@0 2861 if (parent) {
michael@0 2862 parentId = parent.id;
michael@0 2863 isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow);
michael@0 2864 }
michael@0 2865 }
michael@0 2866
michael@0 2867 // BrowserApp.addTab calls loadURIWithFlags with the appropriate params
michael@0 2868 let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags,
michael@0 2869 referrerURI: referrer,
michael@0 2870 external: isExternal,
michael@0 2871 parentId: parentId,
michael@0 2872 selected: true,
michael@0 2873 isPrivate: isPrivate,
michael@0 2874 pinned: pinned });
michael@0 2875
michael@0 2876 return tab.browser;
michael@0 2877 }
michael@0 2878
michael@0 2879 // OPEN_CURRENTWINDOW and illegal values
michael@0 2880 let browser = BrowserApp.selectedBrowser;
michael@0 2881 if (aURI && browser)
michael@0 2882 browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null);
michael@0 2883
michael@0 2884 return browser;
michael@0 2885 },
michael@0 2886
michael@0 2887 openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) {
michael@0 2888 let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
michael@0 2889 return browser ? browser.contentWindow : null;
michael@0 2890 },
michael@0 2891
michael@0 2892 openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) {
michael@0 2893 let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
michael@0 2894 return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
michael@0 2895 },
michael@0 2896
michael@0 2897 isTabContentWindow: function(aWindow) {
michael@0 2898 return BrowserApp.getBrowserForWindow(aWindow) != null;
michael@0 2899 },
michael@0 2900
michael@0 2901 get contentWindow() {
michael@0 2902 return BrowserApp.selectedBrowser.contentWindow;
michael@0 2903 }
michael@0 2904 };
michael@0 2905
michael@0 2906
michael@0 2907 // track the last known screen size so that new tabs
michael@0 2908 // get created with the right size rather than being 1x1
michael@0 2909 let gScreenWidth = 1;
michael@0 2910 let gScreenHeight = 1;
michael@0 2911 let gReflowPending = null;
michael@0 2912
michael@0 2913 // The margins that should be applied to the viewport for fixed position
michael@0 2914 // children. This is used to avoid browser chrome permanently obscuring
michael@0 2915 // fixed position content, and also to make sure window-sized pages take
michael@0 2916 // into account said browser chrome.
michael@0 2917 let gViewportMargins = { top: 0, right: 0, bottom: 0, left: 0};
michael@0 2918
michael@0 2919 function Tab(aURL, aParams) {
michael@0 2920 this.browser = null;
michael@0 2921 this.id = 0;
michael@0 2922 this.lastTouchedAt = Date.now();
michael@0 2923 this._zoom = 1.0;
michael@0 2924 this._drawZoom = 1.0;
michael@0 2925 this._restoreZoom = false;
michael@0 2926 this._fixedMarginLeft = 0;
michael@0 2927 this._fixedMarginTop = 0;
michael@0 2928 this._fixedMarginRight = 0;
michael@0 2929 this._fixedMarginBottom = 0;
michael@0 2930 this._readerEnabled = false;
michael@0 2931 this._readerActive = false;
michael@0 2932 this.userScrollPos = { x: 0, y: 0 };
michael@0 2933 this.viewportExcludesHorizontalMargins = true;
michael@0 2934 this.viewportExcludesVerticalMargins = true;
michael@0 2935 this.viewportMeasureCallback = null;
michael@0 2936 this.lastPageSizeAfterViewportRemeasure = { width: 0, height: 0 };
michael@0 2937 this.contentDocumentIsDisplayed = true;
michael@0 2938 this.pluginDoorhangerTimeout = null;
michael@0 2939 this.shouldShowPluginDoorhanger = true;
michael@0 2940 this.clickToPlayPluginsActivated = false;
michael@0 2941 this.desktopMode = false;
michael@0 2942 this.originalURI = null;
michael@0 2943 this.savedArticle = null;
michael@0 2944 this.hasTouchListener = false;
michael@0 2945 this.browserWidth = 0;
michael@0 2946 this.browserHeight = 0;
michael@0 2947
michael@0 2948 this.create(aURL, aParams);
michael@0 2949 }
michael@0 2950
michael@0 2951 Tab.prototype = {
michael@0 2952 create: function(aURL, aParams) {
michael@0 2953 if (this.browser)
michael@0 2954 return;
michael@0 2955
michael@0 2956 aParams = aParams || {};
michael@0 2957
michael@0 2958 this.browser = document.createElement("browser");
michael@0 2959 this.browser.setAttribute("type", "content-targetable");
michael@0 2960 this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight);
michael@0 2961
michael@0 2962 // Make sure the previously selected panel remains selected. The selected panel of a deck is
michael@0 2963 // not stable when panels are added.
michael@0 2964 let selectedPanel = BrowserApp.deck.selectedPanel;
michael@0 2965 BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null);
michael@0 2966 BrowserApp.deck.selectedPanel = selectedPanel;
michael@0 2967
michael@0 2968 if (BrowserApp.manifestUrl) {
michael@0 2969 let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService);
michael@0 2970 let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl);
michael@0 2971 if (manifest) {
michael@0 2972 let app = manifest.QueryInterface(Ci.mozIApplication);
michael@0 2973 this.browser.docShell.setIsApp(app.localId);
michael@0 2974 }
michael@0 2975 }
michael@0 2976
michael@0 2977 // Must be called after appendChild so the docshell has been created.
michael@0 2978 this.setActive(false);
michael@0 2979
michael@0 2980 let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate;
michael@0 2981 if (isPrivate) {
michael@0 2982 this.browser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing = true;
michael@0 2983 }
michael@0 2984
michael@0 2985 this.browser.stop();
michael@0 2986
michael@0 2987 let frameLoader = this.browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
michael@0 2988 frameLoader.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL;
michael@0 2989
michael@0 2990 // only set tab uri if uri is valid
michael@0 2991 let uri = null;
michael@0 2992 let title = aParams.title || aURL;
michael@0 2993 try {
michael@0 2994 uri = Services.io.newURI(aURL, null, null).spec;
michael@0 2995 } catch (e) {}
michael@0 2996
michael@0 2997 // When the tab is stubbed from Java, there's a window between the stub
michael@0 2998 // creation and the tab creation in Gecko where the stub could be removed
michael@0 2999 // or the selected tab can change (which is easiest to hit during startup).
michael@0 3000 // To prevent these races, we need to differentiate between tab stubs from
michael@0 3001 // Java and new tabs from Gecko.
michael@0 3002 let stub = false;
michael@0 3003
michael@0 3004 if (!aParams.zombifying) {
michael@0 3005 if ("tabID" in aParams) {
michael@0 3006 this.id = aParams.tabID;
michael@0 3007 stub = true;
michael@0 3008 } else {
michael@0 3009 let jni = new JNI();
michael@0 3010 let cls = jni.findClass("org/mozilla/gecko/Tabs");
michael@0 3011 let method = jni.getStaticMethodID(cls, "getNextTabId", "()I");
michael@0 3012 this.id = jni.callStaticIntMethod(cls, method);
michael@0 3013 jni.close();
michael@0 3014 }
michael@0 3015
michael@0 3016 this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false;
michael@0 3017
michael@0 3018 let message = {
michael@0 3019 type: "Tab:Added",
michael@0 3020 tabID: this.id,
michael@0 3021 uri: uri,
michael@0 3022 parentId: ("parentId" in aParams) ? aParams.parentId : -1,
michael@0 3023 external: ("external" in aParams) ? aParams.external : false,
michael@0 3024 selected: ("selected" in aParams) ? aParams.selected : true,
michael@0 3025 title: title,
michael@0 3026 delayLoad: aParams.delayLoad || false,
michael@0 3027 desktopMode: this.desktopMode,
michael@0 3028 isPrivate: isPrivate,
michael@0 3029 stub: stub
michael@0 3030 };
michael@0 3031 sendMessageToJava(message);
michael@0 3032
michael@0 3033 this.overscrollController = new OverscrollController(this);
michael@0 3034 }
michael@0 3035
michael@0 3036 this.browser.contentWindow.controllers.insertControllerAt(0, this.overscrollController);
michael@0 3037
michael@0 3038 let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL |
michael@0 3039 Ci.nsIWebProgress.NOTIFY_LOCATION |
michael@0 3040 Ci.nsIWebProgress.NOTIFY_SECURITY;
michael@0 3041 this.browser.addProgressListener(this, flags);
michael@0 3042 this.browser.sessionHistory.addSHistoryListener(this);
michael@0 3043
michael@0 3044 this.browser.addEventListener("DOMContentLoaded", this, true);
michael@0 3045 this.browser.addEventListener("DOMFormHasPassword", this, true);
michael@0 3046 this.browser.addEventListener("DOMLinkAdded", this, true);
michael@0 3047 this.browser.addEventListener("DOMTitleChanged", this, true);
michael@0 3048 this.browser.addEventListener("DOMWindowClose", this, true);
michael@0 3049 this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
michael@0 3050 this.browser.addEventListener("DOMAutoComplete", this, true);
michael@0 3051 this.browser.addEventListener("blur", this, true);
michael@0 3052 this.browser.addEventListener("scroll", this, true);
michael@0 3053 this.browser.addEventListener("MozScrolledAreaChanged", this, true);
michael@0 3054 this.browser.addEventListener("pageshow", this, true);
michael@0 3055 this.browser.addEventListener("MozApplicationManifest", this, true);
michael@0 3056
michael@0 3057 // Note that the XBL binding is untrusted
michael@0 3058 this.browser.addEventListener("PluginBindingAttached", this, true, true);
michael@0 3059 this.browser.addEventListener("VideoBindingAttached", this, true, true);
michael@0 3060 this.browser.addEventListener("VideoBindingCast", this, true, true);
michael@0 3061
michael@0 3062 Services.obs.addObserver(this, "before-first-paint", false);
michael@0 3063 Services.obs.addObserver(this, "after-viewport-change", false);
michael@0 3064 Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false);
michael@0 3065
michael@0 3066 if (aParams.delayLoad) {
michael@0 3067 // If this is a zombie tab, attach restore data so the tab will be
michael@0 3068 // restored when selected
michael@0 3069 this.browser.__SS_data = {
michael@0 3070 entries: [{
michael@0 3071 url: aURL,
michael@0 3072 title: title
michael@0 3073 }],
michael@0 3074 index: 1
michael@0 3075 };
michael@0 3076 this.browser.__SS_restore = true;
michael@0 3077 } else {
michael@0 3078 let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
michael@0 3079 let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
michael@0 3080 let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
michael@0 3081 let charset = "charset" in aParams ? aParams.charset : null;
michael@0 3082
michael@0 3083 // The search term the user entered to load the current URL
michael@0 3084 this.userSearch = "userSearch" in aParams ? aParams.userSearch : "";
michael@0 3085
michael@0 3086 try {
michael@0 3087 this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData);
michael@0 3088 } catch(e) {
michael@0 3089 let message = {
michael@0 3090 type: "Content:LoadError",
michael@0 3091 tabID: this.id
michael@0 3092 };
michael@0 3093 sendMessageToJava(message);
michael@0 3094 dump("Handled load error: " + e);
michael@0 3095 }
michael@0 3096 }
michael@0 3097 },
michael@0 3098
michael@0 3099 /**
michael@0 3100 * Retrieves the font size in twips for a given element.
michael@0 3101 */
michael@0 3102 getInflatedFontSizeFor: function(aElement) {
michael@0 3103 // GetComputedStyle should always give us CSS pixels for a font size.
michael@0 3104 let fontSizeStr = this.window.getComputedStyle(aElement)['fontSize'];
michael@0 3105 let fontSize = fontSizeStr.slice(0, -2);
michael@0 3106 return aElement.fontSizeInflation * fontSize;
michael@0 3107 },
michael@0 3108
michael@0 3109 /**
michael@0 3110 * This returns the zoom necessary to match the font size of an element to
michael@0 3111 * the minimum font size specified by the browser.zoom.reflowOnZoom.minFontSizeTwips
michael@0 3112 * preference.
michael@0 3113 */
michael@0 3114 getZoomToMinFontSize: function(aElement) {
michael@0 3115 // We only use the font.size.inflation.minTwips preference because this is
michael@0 3116 // the only one that is controlled by the user-interface in the 'Settings'
michael@0 3117 // menu. Thus, if font.size.inflation.emPerLine is changed, this does not
michael@0 3118 // effect reflow-on-zoom.
michael@0 3119 let minFontSize = convertFromTwipsToPx(Services.prefs.getIntPref("font.size.inflation.minTwips"));
michael@0 3120 return minFontSize / this.getInflatedFontSizeFor(aElement);
michael@0 3121 },
michael@0 3122
michael@0 3123 clearReflowOnZoomPendingActions: function() {
michael@0 3124 // Reflow was completed, so now re-enable painting.
michael@0 3125 let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
michael@0 3126 let docShell = webNav.QueryInterface(Ci.nsIDocShell);
michael@0 3127 let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
michael@0 3128 docViewer.resumePainting();
michael@0 3129
michael@0 3130 BrowserApp.selectedTab._mReflozPositioned = false;
michael@0 3131 },
michael@0 3132
michael@0 3133 /**
michael@0 3134 * Reflow on zoom consists of a few different sub-operations:
michael@0 3135 *
michael@0 3136 * 1. When a double-tap event is seen, we verify that the correct preferences
michael@0 3137 * are enabled and perform the pre-position handling calculation. We also
michael@0 3138 * signal that reflow-on-zoom should be performed at this time, and pause
michael@0 3139 * painting.
michael@0 3140 * 2. During the next call to setViewport(), which is in the Tab prototype,
michael@0 3141 * we detect that a call to changeMaxLineBoxWidth should be performed. If
michael@0 3142 * we're zooming out, then the max line box width should be reset at this
michael@0 3143 * time. Otherwise, we call performReflowOnZoom.
michael@0 3144 * 2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to
michael@0 3145 * doChangeMaxLineBoxWidth, based on a timeout specified in preferences.
michael@0 3146 * 3. doChangeMaxLineBoxWidth changes the line box width (which also
michael@0 3147 * schedules a reflow event), and then calls ZoomHelper.zoomInAndSnapToRange.
michael@0 3148 * 4. ZoomHelper.zoomInAndSnapToRange performs the positioning of reflow-on-zoom
michael@0 3149 * and then re-enables painting.
michael@0 3150 *
michael@0 3151 * Some of the events happen synchronously, while others happen asynchronously.
michael@0 3152 * The following is a rough sketch of the progression of events:
michael@0 3153 *
michael@0 3154 * double tap event seen -> onDoubleTap() -> ... asynchronous ...
michael@0 3155 * -> setViewport() -> performReflowOnZoom() -> ... asynchronous ...
michael@0 3156 * -> doChangeMaxLineBoxWidth() -> ZoomHelper.zoomInAndSnapToRange()
michael@0 3157 * -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change')
michael@0 3158 * -> resumePainting()
michael@0 3159 */
michael@0 3160 performReflowOnZoom: function(aViewport) {
michael@0 3161 let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom;
michael@0 3162
michael@0 3163 let viewportWidth = gScreenWidth / zoom;
michael@0 3164 let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
michael@0 3165
michael@0 3166 if (gReflowPending) {
michael@0 3167 clearTimeout(gReflowPending);
michael@0 3168 }
michael@0 3169
michael@0 3170 // We add in a bit of fudge just so that the end characters
michael@0 3171 // don't accidentally get clipped. 15px is an arbitrary choice.
michael@0 3172 gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
michael@0 3173 reflozTimeout,
michael@0 3174 viewportWidth - 15);
michael@0 3175 },
michael@0 3176
michael@0 3177 /**
michael@0 3178 * Reloads the tab with the desktop mode setting.
michael@0 3179 */
michael@0 3180 reloadWithMode: function (aDesktopMode) {
michael@0 3181 // Set desktop mode for tab and send change to Java
michael@0 3182 if (this.desktopMode != aDesktopMode) {
michael@0 3183 this.desktopMode = aDesktopMode;
michael@0 3184 sendMessageToJava({
michael@0 3185 type: "DesktopMode:Changed",
michael@0 3186 desktopMode: aDesktopMode,
michael@0 3187 tabID: this.id
michael@0 3188 });
michael@0 3189 }
michael@0 3190
michael@0 3191 // Only reload the page for http/https schemes
michael@0 3192 let currentURI = this.browser.currentURI;
michael@0 3193 if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https"))
michael@0 3194 return;
michael@0 3195
michael@0 3196 let url = currentURI.spec;
michael@0 3197 let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE |
michael@0 3198 Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY;
michael@0 3199 if (this.originalURI && !this.originalURI.equals(currentURI)) {
michael@0 3200 // We were redirected; reload the original URL
michael@0 3201 url = this.originalURI.spec;
michael@0 3202 }
michael@0 3203
michael@0 3204 this.browser.docShell.loadURI(url, flags, null, null, null);
michael@0 3205 },
michael@0 3206
michael@0 3207 destroy: function() {
michael@0 3208 if (!this.browser)
michael@0 3209 return;
michael@0 3210
michael@0 3211 this.browser.contentWindow.controllers.removeController(this.overscrollController);
michael@0 3212
michael@0 3213 this.browser.removeProgressListener(this);
michael@0 3214 this.browser.sessionHistory.removeSHistoryListener(this);
michael@0 3215
michael@0 3216 this.browser.removeEventListener("DOMContentLoaded", this, true);
michael@0 3217 this.browser.removeEventListener("DOMFormHasPassword", this, true);
michael@0 3218 this.browser.removeEventListener("DOMLinkAdded", this, true);
michael@0 3219 this.browser.removeEventListener("DOMTitleChanged", this, true);
michael@0 3220 this.browser.removeEventListener("DOMWindowClose", this, true);
michael@0 3221 this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
michael@0 3222 this.browser.removeEventListener("DOMAutoComplete", this, true);
michael@0 3223 this.browser.removeEventListener("blur", this, true);
michael@0 3224 this.browser.removeEventListener("scroll", this, true);
michael@0 3225 this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
michael@0 3226 this.browser.removeEventListener("pageshow", this, true);
michael@0 3227 this.browser.removeEventListener("MozApplicationManifest", this, true);
michael@0 3228
michael@0 3229 this.browser.removeEventListener("PluginBindingAttached", this, true, true);
michael@0 3230 this.browser.removeEventListener("VideoBindingAttached", this, true, true);
michael@0 3231 this.browser.removeEventListener("VideoBindingCast", this, true, true);
michael@0 3232
michael@0 3233 Services.obs.removeObserver(this, "before-first-paint");
michael@0 3234 Services.obs.removeObserver(this, "after-viewport-change");
michael@0 3235 Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this);
michael@0 3236
michael@0 3237 // Make sure the previously selected panel remains selected. The selected panel of a deck is
michael@0 3238 // not stable when panels are removed.
michael@0 3239 let selectedPanel = BrowserApp.deck.selectedPanel;
michael@0 3240 BrowserApp.deck.removeChild(this.browser);
michael@0 3241 BrowserApp.deck.selectedPanel = selectedPanel;
michael@0 3242
michael@0 3243 this.browser = null;
michael@0 3244 this.savedArticle = null;
michael@0 3245 },
michael@0 3246
michael@0 3247 // This should be called to update the browser when the tab gets selected/unselected
michael@0 3248 setActive: function setActive(aActive) {
michael@0 3249 if (!this.browser || !this.browser.docShell)
michael@0 3250 return;
michael@0 3251
michael@0 3252 this.lastTouchedAt = Date.now();
michael@0 3253
michael@0 3254 if (aActive) {
michael@0 3255 this.browser.setAttribute("type", "content-primary");
michael@0 3256 this.browser.focus();
michael@0 3257 this.browser.docShellIsActive = true;
michael@0 3258 Reader.updatePageAction(this);
michael@0 3259 ExternalApps.updatePageAction(this.browser.currentURI);
michael@0 3260 } else {
michael@0 3261 this.browser.setAttribute("type", "content-targetable");
michael@0 3262 this.browser.docShellIsActive = false;
michael@0 3263 }
michael@0 3264 },
michael@0 3265
michael@0 3266 getActive: function getActive() {
michael@0 3267 return this.browser.docShellIsActive;
michael@0 3268 },
michael@0 3269
michael@0 3270 setDisplayPort: function(aDisplayPort) {
michael@0 3271 let zoom = this._zoom;
michael@0 3272 let resolution = aDisplayPort.resolution;
michael@0 3273 if (zoom <= 0 || resolution <= 0)
michael@0 3274 return;
michael@0 3275
michael@0 3276 // "zoom" is the user-visible zoom of the "this" tab
michael@0 3277 // "resolution" is the zoom at which we wish gecko to render "this" tab at
michael@0 3278 // these two may be different if we are, for example, trying to render a
michael@0 3279 // large area of the page at low resolution because the user is panning real
michael@0 3280 // fast.
michael@0 3281 // The gecko scroll position is in CSS pixels. The display port rect
michael@0 3282 // values (aDisplayPort), however, are in CSS pixels multiplied by the desired
michael@0 3283 // rendering resolution. Therefore care must be taken when doing math with
michael@0 3284 // these sets of values, to ensure that they are normalized to the same coordinate
michael@0 3285 // space first.
michael@0 3286
michael@0 3287 let element = this.browser.contentDocument.documentElement;
michael@0 3288 if (!element)
michael@0 3289 return;
michael@0 3290
michael@0 3291 // we should never be drawing background tabs at resolutions other than the user-
michael@0 3292 // visible zoom. for foreground tabs, however, if we are drawing at some other
michael@0 3293 // resolution, we need to set the resolution as specified.
michael@0 3294 let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 3295 if (BrowserApp.selectedTab == this) {
michael@0 3296 if (resolution != this._drawZoom) {
michael@0 3297 this._drawZoom = resolution;
michael@0 3298 cwu.setResolution(resolution / window.devicePixelRatio, resolution / window.devicePixelRatio);
michael@0 3299 }
michael@0 3300 } else if (!fuzzyEquals(resolution, zoom)) {
michael@0 3301 dump("Warning: setDisplayPort resolution did not match zoom for background tab! (" + resolution + " != " + zoom + ")");
michael@0 3302 }
michael@0 3303
michael@0 3304 // Finally, we set the display port, taking care to convert everything into the CSS-pixel
michael@0 3305 // coordinate space, because that is what the function accepts. Also we have to fudge the
michael@0 3306 // displayport somewhat to make sure it gets through all the conversions gecko will do on it
michael@0 3307 // without deforming too much. See https://bugzilla.mozilla.org/show_bug.cgi?id=737510#c10
michael@0 3308 // for details on what these operations are.
michael@0 3309 let geckoScrollX = this.browser.contentWindow.scrollX;
michael@0 3310 let geckoScrollY = this.browser.contentWindow.scrollY;
michael@0 3311 aDisplayPort = this._dirtiestHackEverToWorkAroundGeckoRounding(aDisplayPort, geckoScrollX, geckoScrollY);
michael@0 3312
michael@0 3313 let displayPort = {
michael@0 3314 x: (aDisplayPort.left / resolution) - geckoScrollX,
michael@0 3315 y: (aDisplayPort.top / resolution) - geckoScrollY,
michael@0 3316 width: (aDisplayPort.right - aDisplayPort.left) / resolution,
michael@0 3317 height: (aDisplayPort.bottom - aDisplayPort.top) / resolution
michael@0 3318 };
michael@0 3319
michael@0 3320 if (this._oldDisplayPort == null ||
michael@0 3321 !fuzzyEquals(displayPort.x, this._oldDisplayPort.x) ||
michael@0 3322 !fuzzyEquals(displayPort.y, this._oldDisplayPort.y) ||
michael@0 3323 !fuzzyEquals(displayPort.width, this._oldDisplayPort.width) ||
michael@0 3324 !fuzzyEquals(displayPort.height, this._oldDisplayPort.height)) {
michael@0 3325 if (BrowserApp.gUseLowPrecision) {
michael@0 3326 // Set the display-port to be 4x the size of the critical display-port,
michael@0 3327 // on each dimension, giving us a 0.25x lower precision buffer around the
michael@0 3328 // critical display-port. Spare area is *not* redistributed to the other
michael@0 3329 // axis, as display-list building and invalidation cost scales with the
michael@0 3330 // size of the display-port.
michael@0 3331 let pageRect = cwu.getRootBounds();
michael@0 3332 let pageXMost = pageRect.right - geckoScrollX;
michael@0 3333 let pageYMost = pageRect.bottom - geckoScrollY;
michael@0 3334
michael@0 3335 let dpW = Math.min(pageRect.right - pageRect.left, displayPort.width * 4);
michael@0 3336 let dpH = Math.min(pageRect.bottom - pageRect.top, displayPort.height * 4);
michael@0 3337
michael@0 3338 let dpX = Math.min(Math.max(displayPort.x - displayPort.width * 1.5,
michael@0 3339 pageRect.left - geckoScrollX), pageXMost - dpW);
michael@0 3340 let dpY = Math.min(Math.max(displayPort.y - displayPort.height * 1.5,
michael@0 3341 pageRect.top - geckoScrollY), pageYMost - dpH);
michael@0 3342 cwu.setDisplayPortForElement(dpX, dpY, dpW, dpH, element, 0);
michael@0 3343 cwu.setCriticalDisplayPortForElement(displayPort.x, displayPort.y,
michael@0 3344 displayPort.width, displayPort.height,
michael@0 3345 element);
michael@0 3346 } else {
michael@0 3347 cwu.setDisplayPortForElement(displayPort.x, displayPort.y,
michael@0 3348 displayPort.width, displayPort.height,
michael@0 3349 element, 0);
michael@0 3350 }
michael@0 3351 }
michael@0 3352
michael@0 3353 this._oldDisplayPort = displayPort;
michael@0 3354 },
michael@0 3355
michael@0 3356 /*
michael@0 3357 * Yes, this is ugly. But it's currently the safest way to account for the rounding errors that occur
michael@0 3358 * when we pump the displayport coordinates through gecko and they pop out in the compositor.
michael@0 3359 *
michael@0 3360 * In general, the values are converted from page-relative device pixels to viewport-relative app units,
michael@0 3361 * and then back to page-relative device pixels (now as ints). The first half of this is only slightly
michael@0 3362 * lossy, but it's enough to throw off the numbers a little. Because of this, when gecko calls
michael@0 3363 * ScaleToOutsidePixels to generate the final rect, the rect may get expanded more than it should,
michael@0 3364 * ending up a pixel larger than it started off. This is undesirable in general, but specifically
michael@0 3365 * bad for tiling, because it means we means we end up painting one line of pixels from a tile,
michael@0 3366 * causing an otherwise unnecessary upload of the whole tile.
michael@0 3367 *
michael@0 3368 * In order to counteract the rounding error, this code simulates the conversions that will happen
michael@0 3369 * to the display port, and calculates whether or not that final ScaleToOutsidePixels is actually
michael@0 3370 * expanding the rect more than it should. If so, it determines how much rounding error was introduced
michael@0 3371 * up until that point, and adjusts the original values to compensate for that rounding error.
michael@0 3372 */
michael@0 3373 _dirtiestHackEverToWorkAroundGeckoRounding: function(aDisplayPort, aGeckoScrollX, aGeckoScrollY) {
michael@0 3374 const APP_UNITS_PER_CSS_PIXEL = 60.0;
michael@0 3375 const EXTRA_FUDGE = 0.04;
michael@0 3376
michael@0 3377 let resolution = aDisplayPort.resolution;
michael@0 3378
michael@0 3379 // Some helper functions that simulate conversion processes in gecko
michael@0 3380
michael@0 3381 function cssPixelsToAppUnits(aVal) {
michael@0 3382 return Math.floor((aVal * APP_UNITS_PER_CSS_PIXEL) + 0.5);
michael@0 3383 }
michael@0 3384
michael@0 3385 function appUnitsToDevicePixels(aVal) {
michael@0 3386 return aVal / APP_UNITS_PER_CSS_PIXEL * resolution;
michael@0 3387 }
michael@0 3388
michael@0 3389 function devicePixelsToAppUnits(aVal) {
michael@0 3390 return cssPixelsToAppUnits(aVal / resolution);
michael@0 3391 }
michael@0 3392
michael@0 3393 // Stash our original (desired) displayport width and height away, we need it
michael@0 3394 // later and we might modify the displayport in between.
michael@0 3395 let originalWidth = aDisplayPort.right - aDisplayPort.left;
michael@0 3396 let originalHeight = aDisplayPort.bottom - aDisplayPort.top;
michael@0 3397
michael@0 3398 // This is the first conversion the displayport goes through, going from page-relative
michael@0 3399 // device pixels to viewport-relative app units.
michael@0 3400 let appUnitDisplayPort = {
michael@0 3401 x: cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX),
michael@0 3402 y: cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY),
michael@0 3403 w: cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution),
michael@0 3404 h: cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution)
michael@0 3405 };
michael@0 3406
michael@0 3407 // This is the translation gecko applies when converting back from viewport-relative
michael@0 3408 // device pixels to page-relative device pixels.
michael@0 3409 let geckoTransformX = -Math.floor((-aGeckoScrollX * resolution) + 0.5);
michael@0 3410 let geckoTransformY = -Math.floor((-aGeckoScrollY * resolution) + 0.5);
michael@0 3411
michael@0 3412 // The final "left" value as calculated in gecko is:
michael@0 3413 // left = geckoTransformX + Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x))
michael@0 3414 // In a perfect world, this value would be identical to aDisplayPort.left, which is what
michael@0 3415 // we started with. However, this may not be the case if the value being floored has accumulated
michael@0 3416 // enough error to drop below what it should be.
michael@0 3417 // For example, assume geckoTransformX is 0, and aDisplayPort.left is 4, but
michael@0 3418 // appUnitsToDevicePixels(appUnitsToDevicePixels.x) comes out as 3.9 because of rounding error.
michael@0 3419 // That's bad, because the -0.1 error has caused it to floor to 3 instead of 4. (If it had errored
michael@0 3420 // the other way and come out as 4.1, there's no problem). In this example, we need to increase the
michael@0 3421 // "left" value by some amount so that the 3.9 actually comes out as >= 4, and it gets floored into
michael@0 3422 // the expected value of 4. The delta values calculated below calculate that error amount (e.g. -0.1).
michael@0 3423 let errorLeft = (geckoTransformX + appUnitsToDevicePixels(appUnitDisplayPort.x)) - aDisplayPort.left;
michael@0 3424 let errorTop = (geckoTransformY + appUnitsToDevicePixels(appUnitDisplayPort.y)) - aDisplayPort.top;
michael@0 3425
michael@0 3426 // If the error was negative, that means it will floor incorrectly, so we need to bump up the
michael@0 3427 // original aDisplayPort.left and/or aDisplayPort.top values. The amount we bump it up by is
michael@0 3428 // the error amount (increased by a small fudge factor to ensure it's sufficient), converted
michael@0 3429 // backwards through the conversion process.
michael@0 3430 if (errorLeft < 0) {
michael@0 3431 aDisplayPort.left += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorLeft));
michael@0 3432 // After we modify the left value, we need to re-simulate some values to take that into account
michael@0 3433 appUnitDisplayPort.x = cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX);
michael@0 3434 appUnitDisplayPort.w = cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution);
michael@0 3435 }
michael@0 3436 if (errorTop < 0) {
michael@0 3437 aDisplayPort.top += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorTop));
michael@0 3438 // After we modify the top value, we need to re-simulate some values to take that into account
michael@0 3439 appUnitDisplayPort.y = cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY);
michael@0 3440 appUnitDisplayPort.h = cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution);
michael@0 3441 }
michael@0 3442
michael@0 3443 // At this point, the aDisplayPort.left and aDisplayPort.top values have been corrected to account
michael@0 3444 // for the error in conversion such that they end up where we want them. Now we need to also do the
michael@0 3445 // same for the right/bottom values so that the width/height end up where we want them.
michael@0 3446
michael@0 3447 // This is the final conversion that the displayport goes through before gecko spits it back to
michael@0 3448 // us. Note that the width/height calculates are of the form "ceil(transform(right)) - floor(transform(left))"
michael@0 3449 let scaledOutDevicePixels = {
michael@0 3450 x: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)),
michael@0 3451 y: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y)),
michael@0 3452 w: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)),
michael@0 3453 h: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y))
michael@0 3454 };
michael@0 3455
michael@0 3456 // The final "width" value as calculated in gecko is scaledOutDevicePixels.w.
michael@0 3457 // In a perfect world, this would equal originalWidth. However, things are not perfect, and as before,
michael@0 3458 // we need to calculate how much rounding error has been introduced. In this case the rounding error is causing
michael@0 3459 // the Math.ceil call above to ceiling to the wrong final value. For example, 4 gets converted 4.1 and gets
michael@0 3460 // ceiling'd to 5; in this case the error is 0.1.
michael@0 3461 let errorRight = (appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w) - scaledOutDevicePixels.x) - originalWidth;
michael@0 3462 let errorBottom = (appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h) - scaledOutDevicePixels.y) - originalHeight;
michael@0 3463
michael@0 3464 // If the error was positive, that means it will ceiling incorrectly, so we need to bump down the
michael@0 3465 // original aDisplayPort.right and/or aDisplayPort.bottom. Again, we back-convert the error amount
michael@0 3466 // with a small fudge factor to figure out how much to adjust the original values.
michael@0 3467 if (errorRight > 0) aDisplayPort.right -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorRight + EXTRA_FUDGE));
michael@0 3468 if (errorBottom > 0) aDisplayPort.bottom -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorBottom + EXTRA_FUDGE));
michael@0 3469
michael@0 3470 // Et voila!
michael@0 3471 return aDisplayPort;
michael@0 3472 },
michael@0 3473
michael@0 3474 setScrollClampingSize: function(zoom) {
michael@0 3475 let viewportWidth = gScreenWidth / zoom;
michael@0 3476 let viewportHeight = gScreenHeight / zoom;
michael@0 3477 let screenWidth = gScreenWidth;
michael@0 3478 let screenHeight = gScreenHeight;
michael@0 3479
michael@0 3480 // Shrink the viewport appropriately if the margins are excluded
michael@0 3481 if (this.viewportExcludesVerticalMargins) {
michael@0 3482 screenHeight = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
michael@0 3483 viewportHeight = screenHeight / zoom;
michael@0 3484 }
michael@0 3485 if (this.viewportExcludesHorizontalMargins) {
michael@0 3486 screenWidth = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
michael@0 3487 viewportWidth = screenWidth / zoom;
michael@0 3488 }
michael@0 3489
michael@0 3490 // Make sure the aspect ratio of the screen is maintained when setting
michael@0 3491 // the clamping scroll-port size.
michael@0 3492 let factor = Math.min(viewportWidth / screenWidth,
michael@0 3493 viewportHeight / screenHeight);
michael@0 3494 let scrollPortWidth = screenWidth * factor;
michael@0 3495 let scrollPortHeight = screenHeight * factor;
michael@0 3496
michael@0 3497 let win = this.browser.contentWindow;
michael@0 3498 win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).
michael@0 3499 setScrollPositionClampingScrollPortSize(scrollPortWidth, scrollPortHeight);
michael@0 3500 },
michael@0 3501
michael@0 3502 setViewport: function(aViewport) {
michael@0 3503 // Transform coordinates based on zoom
michael@0 3504 let x = aViewport.x / aViewport.zoom;
michael@0 3505 let y = aViewport.y / aViewport.zoom;
michael@0 3506
michael@0 3507 this.setScrollClampingSize(aViewport.zoom);
michael@0 3508
michael@0 3509 // Adjust the max line box width to be no more than the viewport width, but
michael@0 3510 // only if the reflow-on-zoom preference is enabled.
michael@0 3511 let isZooming = !fuzzyEquals(aViewport.zoom, this._zoom);
michael@0 3512
michael@0 3513 let docViewer = null;
michael@0 3514
michael@0 3515 if (isZooming &&
michael@0 3516 BrowserEventHandler.mReflozPref &&
michael@0 3517 BrowserApp.selectedTab._mReflozPoint &&
michael@0 3518 BrowserApp.selectedTab.probablyNeedRefloz) {
michael@0 3519 let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
michael@0 3520 let docShell = webNav.QueryInterface(Ci.nsIDocShell);
michael@0 3521 docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
michael@0 3522 docViewer.pausePainting();
michael@0 3523
michael@0 3524 BrowserApp.selectedTab.performReflowOnZoom(aViewport);
michael@0 3525 BrowserApp.selectedTab.probablyNeedRefloz = false;
michael@0 3526 }
michael@0 3527
michael@0 3528 let win = this.browser.contentWindow;
michael@0 3529 win.scrollTo(x, y);
michael@0 3530 this.saveSessionZoom(aViewport.zoom);
michael@0 3531
michael@0 3532 this.userScrollPos.x = win.scrollX;
michael@0 3533 this.userScrollPos.y = win.scrollY;
michael@0 3534 this.setResolution(aViewport.zoom, false);
michael@0 3535
michael@0 3536 if (aViewport.displayPort)
michael@0 3537 this.setDisplayPort(aViewport.displayPort);
michael@0 3538
michael@0 3539 // Store fixed margins for later retrieval in getViewport.
michael@0 3540 this._fixedMarginLeft = aViewport.fixedMarginLeft;
michael@0 3541 this._fixedMarginTop = aViewport.fixedMarginTop;
michael@0 3542 this._fixedMarginRight = aViewport.fixedMarginRight;
michael@0 3543 this._fixedMarginBottom = aViewport.fixedMarginBottom;
michael@0 3544
michael@0 3545 let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 3546 dwi.setContentDocumentFixedPositionMargins(
michael@0 3547 aViewport.fixedMarginTop / aViewport.zoom,
michael@0 3548 aViewport.fixedMarginRight / aViewport.zoom,
michael@0 3549 aViewport.fixedMarginBottom / aViewport.zoom,
michael@0 3550 aViewport.fixedMarginLeft / aViewport.zoom);
michael@0 3551
michael@0 3552 Services.obs.notifyObservers(null, "after-viewport-change", "");
michael@0 3553 if (docViewer) {
michael@0 3554 docViewer.resumePainting();
michael@0 3555 }
michael@0 3556 },
michael@0 3557
michael@0 3558 setResolution: function(aZoom, aForce) {
michael@0 3559 // Set zoom level
michael@0 3560 if (aForce || !fuzzyEquals(aZoom, this._zoom)) {
michael@0 3561 this._zoom = aZoom;
michael@0 3562 if (BrowserApp.selectedTab == this) {
michael@0 3563 let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 3564 this._drawZoom = aZoom;
michael@0 3565 cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio);
michael@0 3566 }
michael@0 3567 }
michael@0 3568 },
michael@0 3569
michael@0 3570 getPageSize: function(aDocument, aDefaultWidth, aDefaultHeight) {
michael@0 3571 let body = aDocument.body || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight };
michael@0 3572 let html = aDocument.documentElement || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight };
michael@0 3573 return [Math.max(body.scrollWidth, html.scrollWidth),
michael@0 3574 Math.max(body.scrollHeight, html.scrollHeight)];
michael@0 3575 },
michael@0 3576
michael@0 3577 getViewport: function() {
michael@0 3578 let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
michael@0 3579 let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
michael@0 3580 let zoom = this.restoredSessionZoom() || this._zoom;
michael@0 3581
michael@0 3582 let viewport = {
michael@0 3583 width: screenW,
michael@0 3584 height: screenH,
michael@0 3585 cssWidth: screenW / zoom,
michael@0 3586 cssHeight: screenH / zoom,
michael@0 3587 pageLeft: 0,
michael@0 3588 pageTop: 0,
michael@0 3589 pageRight: screenW,
michael@0 3590 pageBottom: screenH,
michael@0 3591 // We make up matching css page dimensions
michael@0 3592 cssPageLeft: 0,
michael@0 3593 cssPageTop: 0,
michael@0 3594 cssPageRight: screenW / zoom,
michael@0 3595 cssPageBottom: screenH / zoom,
michael@0 3596 fixedMarginLeft: this._fixedMarginLeft,
michael@0 3597 fixedMarginTop: this._fixedMarginTop,
michael@0 3598 fixedMarginRight: this._fixedMarginRight,
michael@0 3599 fixedMarginBottom: this._fixedMarginBottom,
michael@0 3600 zoom: zoom,
michael@0 3601 };
michael@0 3602
michael@0 3603 // Set the viewport offset to current scroll offset
michael@0 3604 viewport.cssX = this.browser.contentWindow.scrollX || 0;
michael@0 3605 viewport.cssY = this.browser.contentWindow.scrollY || 0;
michael@0 3606
michael@0 3607 // Transform coordinates based on zoom
michael@0 3608 viewport.x = Math.round(viewport.cssX * viewport.zoom);
michael@0 3609 viewport.y = Math.round(viewport.cssY * viewport.zoom);
michael@0 3610
michael@0 3611 let doc = this.browser.contentDocument;
michael@0 3612 if (doc != null) {
michael@0 3613 let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 3614 let cssPageRect = cwu.getRootBounds();
michael@0 3615
michael@0 3616 /*
michael@0 3617 * Avoid sending page sizes of less than screen size before we hit DOMContentLoaded, because
michael@0 3618 * this causes the page size to jump around wildly during page load. After the page is loaded,
michael@0 3619 * send updates regardless of page size; we'll zoom to fit the content as needed.
michael@0 3620 *
michael@0 3621 * In the check below, we floor the viewport size because there might be slight rounding errors
michael@0 3622 * introduced in the CSS page size due to the conversion to and from app units in Gecko. The
michael@0 3623 * error should be no more than one app unit so doing the floor is overkill, but safe in the
michael@0 3624 * sense that the extra page size updates that get sent as a result will be mostly harmless.
michael@0 3625 */
michael@0 3626 let pageLargerThanScreen = (cssPageRect.width >= Math.floor(viewport.cssWidth))
michael@0 3627 && (cssPageRect.height >= Math.floor(viewport.cssHeight));
michael@0 3628 if (doc.readyState === 'complete' || pageLargerThanScreen) {
michael@0 3629 viewport.cssPageLeft = cssPageRect.left;
michael@0 3630 viewport.cssPageTop = cssPageRect.top;
michael@0 3631 viewport.cssPageRight = cssPageRect.right;
michael@0 3632 viewport.cssPageBottom = cssPageRect.bottom;
michael@0 3633 /* Transform the page width and height based on the zoom factor. */
michael@0 3634 viewport.pageLeft = (viewport.cssPageLeft * viewport.zoom);
michael@0 3635 viewport.pageTop = (viewport.cssPageTop * viewport.zoom);
michael@0 3636 viewport.pageRight = (viewport.cssPageRight * viewport.zoom);
michael@0 3637 viewport.pageBottom = (viewport.cssPageBottom * viewport.zoom);
michael@0 3638 }
michael@0 3639 }
michael@0 3640
michael@0 3641 return viewport;
michael@0 3642 },
michael@0 3643
michael@0 3644 sendViewportUpdate: function(aPageSizeUpdate) {
michael@0 3645 let viewport = this.getViewport();
michael@0 3646 let displayPort = Services.androidBridge.getDisplayPort(aPageSizeUpdate, BrowserApp.isBrowserContentDocumentDisplayed(), this.id, viewport);
michael@0 3647 if (displayPort != null)
michael@0 3648 this.setDisplayPort(displayPort);
michael@0 3649 },
michael@0 3650
michael@0 3651 updateViewportForPageSize: function() {
michael@0 3652 let hasHorizontalMargins = gViewportMargins.left != 0 || gViewportMargins.right != 0;
michael@0 3653 let hasVerticalMargins = gViewportMargins.top != 0 || gViewportMargins.bottom != 0;
michael@0 3654
michael@0 3655 if (!hasHorizontalMargins && !hasVerticalMargins) {
michael@0 3656 // If there are no margins, then we don't need to do any remeasuring
michael@0 3657 return;
michael@0 3658 }
michael@0 3659
michael@0 3660 // If the page size has changed so that it might or might not fit on the
michael@0 3661 // screen with the margins included, run updateViewportSize to resize the
michael@0 3662 // browser accordingly.
michael@0 3663 // A page will receive the smaller viewport when its page size fits
michael@0 3664 // within the screen size, so remeasure when the page size remains within
michael@0 3665 // the threshold of screen + margins, in case it's sizing itself relative
michael@0 3666 // to the viewport.
michael@0 3667 let viewport = this.getViewport();
michael@0 3668 let pageWidth = viewport.pageRight - viewport.pageLeft;
michael@0 3669 let pageHeight = viewport.pageBottom - viewport.pageTop;
michael@0 3670 let remeasureNeeded = false;
michael@0 3671
michael@0 3672 if (hasHorizontalMargins) {
michael@0 3673 let viewportShouldExcludeHorizontalMargins = (pageWidth <= gScreenWidth - 0.5);
michael@0 3674 if (viewportShouldExcludeHorizontalMargins != this.viewportExcludesHorizontalMargins) {
michael@0 3675 remeasureNeeded = true;
michael@0 3676 }
michael@0 3677 }
michael@0 3678 if (hasVerticalMargins) {
michael@0 3679 let viewportShouldExcludeVerticalMargins = (pageHeight <= gScreenHeight - 0.5);
michael@0 3680 if (viewportShouldExcludeVerticalMargins != this.viewportExcludesVerticalMargins) {
michael@0 3681 remeasureNeeded = true;
michael@0 3682 }
michael@0 3683 }
michael@0 3684
michael@0 3685 if (remeasureNeeded) {
michael@0 3686 if (!this.viewportMeasureCallback) {
michael@0 3687 this.viewportMeasureCallback = setTimeout(function() {
michael@0 3688 this.viewportMeasureCallback = null;
michael@0 3689
michael@0 3690 // Re-fetch the viewport as it may have changed between setting the timeout
michael@0 3691 // and running this callback
michael@0 3692 let viewport = this.getViewport();
michael@0 3693 let pageWidth = viewport.pageRight - viewport.pageLeft;
michael@0 3694 let pageHeight = viewport.pageBottom - viewport.pageTop;
michael@0 3695
michael@0 3696 if (Math.abs(pageWidth - this.lastPageSizeAfterViewportRemeasure.width) >= 0.5 ||
michael@0 3697 Math.abs(pageHeight - this.lastPageSizeAfterViewportRemeasure.height) >= 0.5) {
michael@0 3698 this.updateViewportSize(gScreenWidth);
michael@0 3699 }
michael@0 3700 }.bind(this), kViewportRemeasureThrottle);
michael@0 3701 }
michael@0 3702 } else if (this.viewportMeasureCallback) {
michael@0 3703 // If the page changed size twice since we last measured the viewport and
michael@0 3704 // the latest size change reveals we don't need to remeasure, cancel any
michael@0 3705 // pending remeasure.
michael@0 3706 clearTimeout(this.viewportMeasureCallback);
michael@0 3707 this.viewportMeasureCallback = null;
michael@0 3708 }
michael@0 3709 },
michael@0 3710
michael@0 3711 handleEvent: function(aEvent) {
michael@0 3712 switch (aEvent.type) {
michael@0 3713 case "DOMContentLoaded": {
michael@0 3714 let target = aEvent.originalTarget;
michael@0 3715
michael@0 3716 // ignore on frames and other documents
michael@0 3717 if (target != this.browser.contentDocument)
michael@0 3718 return;
michael@0 3719
michael@0 3720 // Sample the background color of the page and pass it along. (This is used to draw the
michael@0 3721 // checkerboard.) Right now we don't detect changes in the background color after this
michael@0 3722 // event fires; it's not clear that doing so is worth the effort.
michael@0 3723 var backgroundColor = null;
michael@0 3724 try {
michael@0 3725 let { contentDocument, contentWindow } = this.browser;
michael@0 3726 let computedStyle = contentWindow.getComputedStyle(contentDocument.body);
michael@0 3727 backgroundColor = computedStyle.backgroundColor;
michael@0 3728 } catch (e) {
michael@0 3729 // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds.
michael@0 3730 }
michael@0 3731
michael@0 3732 let docURI = target.documentURI;
michael@0 3733 let errorType = "";
michael@0 3734 if (docURI.startsWith("about:certerror"))
michael@0 3735 errorType = "certerror";
michael@0 3736 else if (docURI.startsWith("about:blocked"))
michael@0 3737 errorType = "blocked"
michael@0 3738 else if (docURI.startsWith("about:neterror"))
michael@0 3739 errorType = "neterror";
michael@0 3740
michael@0 3741 sendMessageToJava({
michael@0 3742 type: "DOMContentLoaded",
michael@0 3743 tabID: this.id,
michael@0 3744 bgColor: backgroundColor,
michael@0 3745 errorType: errorType
michael@0 3746 });
michael@0 3747
michael@0 3748 // Attach a listener to watch for "click" events bubbling up from error
michael@0 3749 // pages and other similar page. This lets us fix bugs like 401575 which
michael@0 3750 // require error page UI to do privileged things, without letting error
michael@0 3751 // pages have any privilege themselves.
michael@0 3752 if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) {
michael@0 3753 this.browser.addEventListener("click", ErrorPageEventHandler, true);
michael@0 3754 let listener = function() {
michael@0 3755 this.browser.removeEventListener("click", ErrorPageEventHandler, true);
michael@0 3756 this.browser.removeEventListener("pagehide", listener, true);
michael@0 3757 }.bind(this);
michael@0 3758
michael@0 3759 this.browser.addEventListener("pagehide", listener, true);
michael@0 3760 }
michael@0 3761
michael@0 3762 if (docURI.startsWith("about:reader")) {
michael@0 3763 // During browser restart / recovery, duplicate "DOMContentLoaded" messages are received here
michael@0 3764 // For the visible tab ... where more than one tab is being reloaded, the inital "DOMContentLoaded"
michael@0 3765 // Message can be received before the document body is available ... so we avoid instantiating an
michael@0 3766 // AboutReader object, expecting that an eventual valid message will follow.
michael@0 3767 let contentDocument = this.browser.contentDocument;
michael@0 3768 if (contentDocument.body) {
michael@0 3769 new AboutReader(contentDocument, this.browser.contentWindow);
michael@0 3770 }
michael@0 3771 }
michael@0 3772
michael@0 3773 break;
michael@0 3774 }
michael@0 3775
michael@0 3776 case "DOMFormHasPassword": {
michael@0 3777 LoginManagerContent.onFormPassword(aEvent);
michael@0 3778 break;
michael@0 3779 }
michael@0 3780
michael@0 3781 case "DOMLinkAdded": {
michael@0 3782 let target = aEvent.originalTarget;
michael@0 3783 if (!target.href || target.disabled)
michael@0 3784 return;
michael@0 3785
michael@0 3786 // Ignore on frames and other documents
michael@0 3787 if (target.ownerDocument != this.browser.contentDocument)
michael@0 3788 return;
michael@0 3789
michael@0 3790 // Sanitize the rel string
michael@0 3791 let list = [];
michael@0 3792 if (target.rel) {
michael@0 3793 list = target.rel.toLowerCase().split(/\s+/);
michael@0 3794 let hash = {};
michael@0 3795 list.forEach(function(value) { hash[value] = true; });
michael@0 3796 list = [];
michael@0 3797 for (let rel in hash)
michael@0 3798 list.push("[" + rel + "]");
michael@0 3799 }
michael@0 3800
michael@0 3801 if (list.indexOf("[icon]") != -1) {
michael@0 3802 // We want to get the largest icon size possible for our UI.
michael@0 3803 let maxSize = 0;
michael@0 3804
michael@0 3805 // We use the sizes attribute if available
michael@0 3806 // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
michael@0 3807 if (target.hasAttribute("sizes")) {
michael@0 3808 let sizes = target.getAttribute("sizes").toLowerCase();
michael@0 3809
michael@0 3810 if (sizes == "any") {
michael@0 3811 // Since Java expects an integer, use -1 to represent icons with sizes="any"
michael@0 3812 maxSize = -1;
michael@0 3813 } else {
michael@0 3814 let tokens = sizes.split(" ");
michael@0 3815 tokens.forEach(function(token) {
michael@0 3816 // TODO: check for invalid tokens
michael@0 3817 let [w, h] = token.split("x");
michael@0 3818 maxSize = Math.max(maxSize, Math.max(w, h));
michael@0 3819 });
michael@0 3820 }
michael@0 3821 }
michael@0 3822
michael@0 3823 let json = {
michael@0 3824 type: "Link:Favicon",
michael@0 3825 tabID: this.id,
michael@0 3826 href: resolveGeckoURI(target.href),
michael@0 3827 charset: target.ownerDocument.characterSet,
michael@0 3828 title: target.title,
michael@0 3829 rel: list.join(" "),
michael@0 3830 size: maxSize
michael@0 3831 };
michael@0 3832 sendMessageToJava(json);
michael@0 3833 } else if (list.indexOf("[alternate]") != -1) {
michael@0 3834 let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, "");
michael@0 3835 let isFeed = (type == "application/rss+xml" || type == "application/atom+xml");
michael@0 3836
michael@0 3837 if (!isFeed)
michael@0 3838 return;
michael@0 3839
michael@0 3840 try {
michael@0 3841 // urlSecurityCeck will throw if things are not OK
michael@0 3842 ContentAreaUtils.urlSecurityCheck(target.href, target.ownerDocument.nodePrincipal, Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
michael@0 3843
michael@0 3844 if (!this.browser.feeds)
michael@0 3845 this.browser.feeds = [];
michael@0 3846 this.browser.feeds.push({ href: target.href, title: target.title, type: type });
michael@0 3847
michael@0 3848 let json = {
michael@0 3849 type: "Link:Feed",
michael@0 3850 tabID: this.id
michael@0 3851 };
michael@0 3852 sendMessageToJava(json);
michael@0 3853 } catch (e) {}
michael@0 3854 } else if (list.indexOf("[search]" != -1)) {
michael@0 3855 let type = target.type && target.type.toLowerCase();
michael@0 3856
michael@0 3857 // Replace all starting or trailing spaces or spaces before "*;" globally w/ "".
michael@0 3858 type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
michael@0 3859
michael@0 3860 // Check that type matches opensearch.
michael@0 3861 let isOpenSearch = (type == "application/opensearchdescription+xml");
michael@0 3862 if (isOpenSearch && target.title && /^(?:https?|ftp):/i.test(target.href)) {
michael@0 3863 let visibleEngines = Services.search.getVisibleEngines();
michael@0 3864 // NOTE: Engines are currently identified by name, but this can be changed
michael@0 3865 // when Engines are identified by URL (see bug 335102).
michael@0 3866 if (visibleEngines.some(function(e) {
michael@0 3867 return e.name == target.title;
michael@0 3868 })) {
michael@0 3869 // This engine is already present, do nothing.
michael@0 3870 return;
michael@0 3871 }
michael@0 3872
michael@0 3873 if (this.browser.engines) {
michael@0 3874 // This engine has already been handled, do nothing.
michael@0 3875 if (this.browser.engines.some(function(e) {
michael@0 3876 return e.url == target.href;
michael@0 3877 })) {
michael@0 3878 return;
michael@0 3879 }
michael@0 3880 } else {
michael@0 3881 this.browser.engines = [];
michael@0 3882 }
michael@0 3883
michael@0 3884 // Get favicon.
michael@0 3885 let iconURL = target.ownerDocument.documentURIObject.prePath + "/favicon.ico";
michael@0 3886
michael@0 3887 let newEngine = {
michael@0 3888 title: target.title,
michael@0 3889 url: target.href,
michael@0 3890 iconURL: iconURL
michael@0 3891 };
michael@0 3892
michael@0 3893 this.browser.engines.push(newEngine);
michael@0 3894
michael@0 3895 // Don't send a message to display engines if we've already handled an engine.
michael@0 3896 if (this.browser.engines.length > 1)
michael@0 3897 return;
michael@0 3898
michael@0 3899 // Broadcast message that this tab contains search engines that should be visible.
michael@0 3900 let newEngineMessage = {
michael@0 3901 type: "Link:OpenSearch",
michael@0 3902 tabID: this.id,
michael@0 3903 visible: true
michael@0 3904 };
michael@0 3905
michael@0 3906 sendMessageToJava(newEngineMessage);
michael@0 3907 }
michael@0 3908 }
michael@0 3909 break;
michael@0 3910 }
michael@0 3911
michael@0 3912 case "DOMTitleChanged": {
michael@0 3913 if (!aEvent.isTrusted)
michael@0 3914 return;
michael@0 3915
michael@0 3916 // ignore on frames and other documents
michael@0 3917 if (aEvent.originalTarget != this.browser.contentDocument)
michael@0 3918 return;
michael@0 3919
michael@0 3920 sendMessageToJava({
michael@0 3921 type: "DOMTitleChanged",
michael@0 3922 tabID: this.id,
michael@0 3923 title: aEvent.target.title.substring(0, 255)
michael@0 3924 });
michael@0 3925 break;
michael@0 3926 }
michael@0 3927
michael@0 3928 case "DOMWindowClose": {
michael@0 3929 if (!aEvent.isTrusted)
michael@0 3930 return;
michael@0 3931
michael@0 3932 // Find the relevant tab, and close it from Java
michael@0 3933 if (this.browser.contentWindow == aEvent.target) {
michael@0 3934 aEvent.preventDefault();
michael@0 3935
michael@0 3936 sendMessageToJava({
michael@0 3937 type: "Tab:Close",
michael@0 3938 tabID: this.id
michael@0 3939 });
michael@0 3940 }
michael@0 3941 break;
michael@0 3942 }
michael@0 3943
michael@0 3944 case "DOMWillOpenModalDialog": {
michael@0 3945 if (!aEvent.isTrusted)
michael@0 3946 return;
michael@0 3947
michael@0 3948 // We're about to open a modal dialog, make sure the opening
michael@0 3949 // tab is brought to the front.
michael@0 3950 let tab = BrowserApp.getTabForWindow(aEvent.target.top);
michael@0 3951 BrowserApp.selectTab(tab);
michael@0 3952 break;
michael@0 3953 }
michael@0 3954
michael@0 3955 case "DOMAutoComplete":
michael@0 3956 case "blur": {
michael@0 3957 LoginManagerContent.onUsernameInput(aEvent);
michael@0 3958 break;
michael@0 3959 }
michael@0 3960
michael@0 3961 case "scroll": {
michael@0 3962 let win = this.browser.contentWindow;
michael@0 3963 if (this.userScrollPos.x != win.scrollX || this.userScrollPos.y != win.scrollY) {
michael@0 3964 this.sendViewportUpdate();
michael@0 3965 }
michael@0 3966 break;
michael@0 3967 }
michael@0 3968
michael@0 3969 case "MozScrolledAreaChanged": {
michael@0 3970 // This event is only fired for root scroll frames, and only when the
michael@0 3971 // scrolled area has actually changed, so no need to check for that.
michael@0 3972 // Just make sure it's the event for the correct root scroll frame.
michael@0 3973 if (aEvent.originalTarget != this.browser.contentDocument)
michael@0 3974 return;
michael@0 3975
michael@0 3976 this.sendViewportUpdate(true);
michael@0 3977 this.updateViewportForPageSize();
michael@0 3978 break;
michael@0 3979 }
michael@0 3980
michael@0 3981 case "PluginBindingAttached": {
michael@0 3982 PluginHelper.handlePluginBindingAttached(this, aEvent);
michael@0 3983 break;
michael@0 3984 }
michael@0 3985
michael@0 3986 case "VideoBindingAttached": {
michael@0 3987 CastingApps.handleVideoBindingAttached(this, aEvent);
michael@0 3988 break;
michael@0 3989 }
michael@0 3990
michael@0 3991 case "VideoBindingCast": {
michael@0 3992 CastingApps.handleVideoBindingCast(this, aEvent);
michael@0 3993 break;
michael@0 3994 }
michael@0 3995
michael@0 3996 case "MozApplicationManifest": {
michael@0 3997 OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView);
michael@0 3998 break;
michael@0 3999 }
michael@0 4000
michael@0 4001 case "pageshow": {
michael@0 4002 // only send pageshow for the top-level document
michael@0 4003 if (aEvent.originalTarget.defaultView != this.browser.contentWindow)
michael@0 4004 return;
michael@0 4005
michael@0 4006 sendMessageToJava({
michael@0 4007 type: "Content:PageShow",
michael@0 4008 tabID: this.id
michael@0 4009 });
michael@0 4010
michael@0 4011 if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) {
michael@0 4012 if (!this._linkifier)
michael@0 4013 this._linkifier = new Linkifier();
michael@0 4014 this._linkifier.linkifyNumbers(this.browser.contentWindow.document);
michael@0 4015 }
michael@0 4016
michael@0 4017 // Update page actions for helper apps.
michael@0 4018 let uri = this.browser.currentURI;
michael@0 4019 if (BrowserApp.selectedTab == this) {
michael@0 4020 if (ExternalApps.shouldCheckUri(uri)) {
michael@0 4021 ExternalApps.updatePageAction(uri);
michael@0 4022 } else {
michael@0 4023 ExternalApps.clearPageAction();
michael@0 4024 }
michael@0 4025 }
michael@0 4026
michael@0 4027 if (!Reader.isEnabledForParseOnLoad)
michael@0 4028 return;
michael@0 4029
michael@0 4030 // Once document is fully loaded, parse it
michael@0 4031 Reader.parseDocumentFromTab(this.id, function (article) {
michael@0 4032 // Do nothing if there's no article or the page in this tab has
michael@0 4033 // changed
michael@0 4034 let tabURL = uri.specIgnoringRef;
michael@0 4035 if (article == null || (article.url != tabURL)) {
michael@0 4036 // Don't clear the article for about:reader pages since we want to
michael@0 4037 // use the article from the previous page
michael@0 4038 if (!tabURL.startsWith("about:reader")) {
michael@0 4039 this.savedArticle = null;
michael@0 4040 this.readerEnabled = false;
michael@0 4041 this.readerActive = false;
michael@0 4042 } else {
michael@0 4043 this.readerActive = true;
michael@0 4044 }
michael@0 4045 return;
michael@0 4046 }
michael@0 4047
michael@0 4048 this.savedArticle = article;
michael@0 4049
michael@0 4050 sendMessageToJava({
michael@0 4051 type: "Content:ReaderEnabled",
michael@0 4052 tabID: this.id
michael@0 4053 });
michael@0 4054
michael@0 4055 if(this.readerActive)
michael@0 4056 this.readerActive = false;
michael@0 4057
michael@0 4058 if(!this.readerEnabled)
michael@0 4059 this.readerEnabled = true;
michael@0 4060 }.bind(this));
michael@0 4061 }
michael@0 4062 }
michael@0 4063 },
michael@0 4064
michael@0 4065 onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
michael@0 4066 let contentWin = aWebProgress.DOMWindow;
michael@0 4067 if (contentWin != contentWin.top)
michael@0 4068 return;
michael@0 4069
michael@0 4070 // Filter optimization: Only really send NETWORK state changes to Java listener
michael@0 4071 if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
michael@0 4072 if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) {
michael@0 4073 // We may receive a document stop event while a document is still loading
michael@0 4074 // (such as when doing URI fixup). Don't notify Java UI in these cases.
michael@0 4075 return;
michael@0 4076 }
michael@0 4077
michael@0 4078 // Clear page-specific opensearch engines and feeds for a new request.
michael@0 4079 if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) {
michael@0 4080 this.browser.engines = null;
michael@0 4081 this.browser.feeds = null;
michael@0 4082 }
michael@0 4083
michael@0 4084 // true if the page loaded successfully (i.e., no 404s or other errors)
michael@0 4085 let success = false;
michael@0 4086 let uri = "";
michael@0 4087 try {
michael@0 4088 // Remember original URI for UA changes on redirected pages
michael@0 4089 this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI;
michael@0 4090
michael@0 4091 if (this.originalURI != null)
michael@0 4092 uri = this.originalURI.spec;
michael@0 4093 } catch (e) { }
michael@0 4094 try {
michael@0 4095 success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded;
michael@0 4096 } catch (e) {
michael@0 4097 // If the request does not handle the nsIHttpChannel interface, use nsIRequest's success
michael@0 4098 // status. Used for local files. See bug 948849.
michael@0 4099 success = aRequest.status == 0;
michael@0 4100 }
michael@0 4101
michael@0 4102 // Check to see if we restoring the content from a previous presentation (session)
michael@0 4103 // since there should be no real network activity
michael@0 4104 let restoring = (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) > 0;
michael@0 4105
michael@0 4106 let message = {
michael@0 4107 type: "Content:StateChange",
michael@0 4108 tabID: this.id,
michael@0 4109 uri: uri,
michael@0 4110 state: aStateFlags,
michael@0 4111 restoring: restoring,
michael@0 4112 success: success
michael@0 4113 };
michael@0 4114 sendMessageToJava(message);
michael@0 4115 }
michael@0 4116 },
michael@0 4117
michael@0 4118 onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) {
michael@0 4119 let contentWin = aWebProgress.DOMWindow;
michael@0 4120
michael@0 4121 // Browser webapps may load content inside iframes that can not reach across the app/frame boundary
michael@0 4122 // i.e. even though the page is loaded in an iframe window.top != webapp
michael@0 4123 // Make cure this window is a top level tab before moving on.
michael@0 4124 if (BrowserApp.getBrowserForWindow(contentWin) == null)
michael@0 4125 return;
michael@0 4126
michael@0 4127 this._hostChanged = true;
michael@0 4128
michael@0 4129 let fixedURI = aLocationURI;
michael@0 4130 try {
michael@0 4131 fixedURI = URIFixup.createExposableURI(aLocationURI);
michael@0 4132 } catch (ex) { }
michael@0 4133
michael@0 4134 let contentType = contentWin.document.contentType;
michael@0 4135
michael@0 4136 // If fixedURI matches browser.lastURI, we assume this isn't a real location
michael@0 4137 // change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883.
michael@0 4138 // Note that we have to ensure fixedURI is not the same as aLocationURI so we
michael@0 4139 // don't false-positive page reloads as spurious additions.
michael@0 4140 let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 ||
michael@0 4141 ((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI));
michael@0 4142 this.browser.lastURI = fixedURI;
michael@0 4143
michael@0 4144 // Reset state of click-to-play plugin notifications.
michael@0 4145 clearTimeout(this.pluginDoorhangerTimeout);
michael@0 4146 this.pluginDoorhangerTimeout = null;
michael@0 4147 this.shouldShowPluginDoorhanger = true;
michael@0 4148 this.clickToPlayPluginsActivated = false;
michael@0 4149 // Borrowed from desktop Firefox: http://mxr.mozilla.org/mozilla-central/source/browser/base/content/urlbarBindings.xml#174
michael@0 4150 let documentURI = contentWin.document.documentURIObject.spec
michael@0 4151 let matchedURL = documentURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
michael@0 4152 let baseDomain = "";
michael@0 4153 if (matchedURL) {
michael@0 4154 var domain = "";
michael@0 4155 [, , domain] = matchedURL;
michael@0 4156
michael@0 4157 try {
michael@0 4158 baseDomain = Services.eTLD.getBaseDomainFromHost(domain);
michael@0 4159 if (!domain.endsWith(baseDomain)) {
michael@0 4160 // getBaseDomainFromHost converts its resultant to ACE.
michael@0 4161 let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
michael@0 4162 baseDomain = IDNService.convertACEtoUTF8(baseDomain);
michael@0 4163 }
michael@0 4164 } catch (e) {}
michael@0 4165 }
michael@0 4166
michael@0 4167 // Update the page actions URI for helper apps.
michael@0 4168 if (BrowserApp.selectedTab == this) {
michael@0 4169 ExternalApps.updatePageActionUri(fixedURI);
michael@0 4170 }
michael@0 4171
michael@0 4172 let message = {
michael@0 4173 type: "Content:LocationChange",
michael@0 4174 tabID: this.id,
michael@0 4175 uri: fixedURI.spec,
michael@0 4176 userSearch: this.userSearch || "",
michael@0 4177 baseDomain: baseDomain,
michael@0 4178 contentType: (contentType ? contentType : ""),
michael@0 4179 sameDocument: sameDocument
michael@0 4180 };
michael@0 4181
michael@0 4182 sendMessageToJava(message);
michael@0 4183
michael@0 4184 // The search term is only valid for this location change event, so reset it here.
michael@0 4185 this.userSearch = "";
michael@0 4186
michael@0 4187 if (!sameDocument) {
michael@0 4188 // XXX This code assumes that this is the earliest hook we have at which
michael@0 4189 // browser.contentDocument is changed to the new document we're loading
michael@0 4190 this.contentDocumentIsDisplayed = false;
michael@0 4191 this.hasTouchListener = false;
michael@0 4192 } else {
michael@0 4193 this.sendViewportUpdate();
michael@0 4194 }
michael@0 4195 },
michael@0 4196
michael@0 4197 // Properties used to cache security state used to update the UI
michael@0 4198 _state: null,
michael@0 4199 _hostChanged: false, // onLocationChange will flip this bit
michael@0 4200
michael@0 4201 onSecurityChange: function(aWebProgress, aRequest, aState) {
michael@0 4202 // Don't need to do anything if the data we use to update the UI hasn't changed
michael@0 4203 if (this._state == aState && !this._hostChanged)
michael@0 4204 return;
michael@0 4205
michael@0 4206 this._state = aState;
michael@0 4207 this._hostChanged = false;
michael@0 4208
michael@0 4209 let identity = IdentityHandler.checkIdentity(aState, this.browser);
michael@0 4210
michael@0 4211 let message = {
michael@0 4212 type: "Content:SecurityChange",
michael@0 4213 tabID: this.id,
michael@0 4214 identity: identity
michael@0 4215 };
michael@0 4216
michael@0 4217 sendMessageToJava(message);
michael@0 4218 },
michael@0 4219
michael@0 4220 onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) {
michael@0 4221 },
michael@0 4222
michael@0 4223 onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) {
michael@0 4224 },
michael@0 4225
michael@0 4226 _sendHistoryEvent: function(aMessage, aParams) {
michael@0 4227 let message = {
michael@0 4228 type: "SessionHistory:" + aMessage,
michael@0 4229 tabID: this.id,
michael@0 4230 };
michael@0 4231
michael@0 4232 // Restore zoom only when moving in session history, not for new page loads.
michael@0 4233 this._restoreZoom = aMessage != "New";
michael@0 4234
michael@0 4235 if (aParams) {
michael@0 4236 if ("url" in aParams)
michael@0 4237 message.url = aParams.url;
michael@0 4238 if ("index" in aParams)
michael@0 4239 message.index = aParams.index;
michael@0 4240 if ("numEntries" in aParams)
michael@0 4241 message.numEntries = aParams.numEntries;
michael@0 4242 }
michael@0 4243
michael@0 4244 sendMessageToJava(message);
michael@0 4245 },
michael@0 4246
michael@0 4247 _getGeckoZoom: function() {
michael@0 4248 let res = {x: {}, y: {}};
michael@0 4249 let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 4250 cwu.getResolution(res.x, res.y);
michael@0 4251 let zoom = res.x.value * window.devicePixelRatio;
michael@0 4252 return zoom;
michael@0 4253 },
michael@0 4254
michael@0 4255 saveSessionZoom: function(aZoom) {
michael@0 4256 let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 4257 cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio);
michael@0 4258 },
michael@0 4259
michael@0 4260 restoredSessionZoom: function() {
michael@0 4261 let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 4262
michael@0 4263 if (this._restoreZoom && cwu.isResolutionSet) {
michael@0 4264 return this._getGeckoZoom();
michael@0 4265 }
michael@0 4266 return null;
michael@0 4267 },
michael@0 4268
michael@0 4269 OnHistoryNewEntry: function(aUri) {
michael@0 4270 this._sendHistoryEvent("New", { url: aUri.spec });
michael@0 4271 },
michael@0 4272
michael@0 4273 OnHistoryGoBack: function(aUri) {
michael@0 4274 this._sendHistoryEvent("Back");
michael@0 4275 return true;
michael@0 4276 },
michael@0 4277
michael@0 4278 OnHistoryGoForward: function(aUri) {
michael@0 4279 this._sendHistoryEvent("Forward");
michael@0 4280 return true;
michael@0 4281 },
michael@0 4282
michael@0 4283 OnHistoryReload: function(aUri, aFlags) {
michael@0 4284 // we don't do anything with this, so don't propagate it
michael@0 4285 // for now anyway
michael@0 4286 return true;
michael@0 4287 },
michael@0 4288
michael@0 4289 OnHistoryGotoIndex: function(aIndex, aUri) {
michael@0 4290 this._sendHistoryEvent("Goto", { index: aIndex });
michael@0 4291 return true;
michael@0 4292 },
michael@0 4293
michael@0 4294 OnHistoryPurge: function(aNumEntries) {
michael@0 4295 this._sendHistoryEvent("Purge", { numEntries: aNumEntries });
michael@0 4296 return true;
michael@0 4297 },
michael@0 4298
michael@0 4299 OnHistoryReplaceEntry: function(aIndex) {
michael@0 4300 // we don't do anything with this, so don't propogate it
michael@0 4301 // for now anyway.
michael@0 4302 },
michael@0 4303
michael@0 4304 get metadata() {
michael@0 4305 return ViewportHandler.getMetadataForDocument(this.browser.contentDocument);
michael@0 4306 },
michael@0 4307
michael@0 4308 /** Update viewport when the metadata changes. */
michael@0 4309 updateViewportMetadata: function updateViewportMetadata(aMetadata, aInitialLoad) {
michael@0 4310 if (Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) {
michael@0 4311 aMetadata.allowZoom = true;
michael@0 4312 aMetadata.allowDoubleTapZoom = true;
michael@0 4313 aMetadata.minZoom = aMetadata.maxZoom = NaN;
michael@0 4314 }
michael@0 4315
michael@0 4316 let scaleRatio = window.devicePixelRatio;
michael@0 4317
michael@0 4318 if (aMetadata.defaultZoom > 0)
michael@0 4319 aMetadata.defaultZoom *= scaleRatio;
michael@0 4320 if (aMetadata.minZoom > 0)
michael@0 4321 aMetadata.minZoom *= scaleRatio;
michael@0 4322 if (aMetadata.maxZoom > 0)
michael@0 4323 aMetadata.maxZoom *= scaleRatio;
michael@0 4324
michael@0 4325 aMetadata.isRTL = this.browser.contentDocument.documentElement.dir == "rtl";
michael@0 4326
michael@0 4327 ViewportHandler.setMetadataForDocument(this.browser.contentDocument, aMetadata);
michael@0 4328 this.sendViewportMetadata();
michael@0 4329
michael@0 4330 this.updateViewportSize(gScreenWidth, aInitialLoad);
michael@0 4331 },
michael@0 4332
michael@0 4333 /** Update viewport when the metadata or the window size changes. */
michael@0 4334 updateViewportSize: function updateViewportSize(aOldScreenWidth, aInitialLoad) {
michael@0 4335 // When this function gets called on window resize, we must execute
michael@0 4336 // this.sendViewportUpdate() so that refreshDisplayPort is called.
michael@0 4337 // Ensure that when making changes to this function that code path
michael@0 4338 // is not accidentally removed (the call to sendViewportUpdate() is
michael@0 4339 // at the very end).
michael@0 4340
michael@0 4341 if (this.viewportMeasureCallback) {
michael@0 4342 clearTimeout(this.viewportMeasureCallback);
michael@0 4343 this.viewportMeasureCallback = null;
michael@0 4344 }
michael@0 4345
michael@0 4346 let browser = this.browser;
michael@0 4347 if (!browser)
michael@0 4348 return;
michael@0 4349
michael@0 4350 let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right;
michael@0 4351 let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom;
michael@0 4352 let viewportW, viewportH;
michael@0 4353
michael@0 4354 let metadata = this.metadata;
michael@0 4355 if (metadata.autoSize) {
michael@0 4356 viewportW = screenW / window.devicePixelRatio;
michael@0 4357 viewportH = screenH / window.devicePixelRatio;
michael@0 4358 } else {
michael@0 4359 viewportW = metadata.width;
michael@0 4360 viewportH = metadata.height;
michael@0 4361
michael@0 4362 // If (scale * width) < device-width, increase the width (bug 561413).
michael@0 4363 let maxInitialZoom = metadata.defaultZoom || metadata.maxZoom;
michael@0 4364 if (maxInitialZoom && viewportW) {
michael@0 4365 viewportW = Math.max(viewportW, screenW / maxInitialZoom);
michael@0 4366 }
michael@0 4367
michael@0 4368 let validW = viewportW > 0;
michael@0 4369 let validH = viewportH > 0;
michael@0 4370
michael@0 4371 if (!validW)
michael@0 4372 viewportW = validH ? (viewportH * (screenW / screenH)) : BrowserApp.defaultBrowserWidth;
michael@0 4373 if (!validH)
michael@0 4374 viewportH = viewportW * (screenH / screenW);
michael@0 4375 }
michael@0 4376
michael@0 4377 // Make sure the viewport height is not shorter than the window when
michael@0 4378 // the page is zoomed out to show its full width. Note that before
michael@0 4379 // we set the viewport width, the "full width" of the page isn't properly
michael@0 4380 // defined, so that's why we have to call setBrowserSize twice - once
michael@0 4381 // to set the width, and the second time to figure out the height based
michael@0 4382 // on the layout at that width.
michael@0 4383 let oldBrowserWidth = this.browserWidth;
michael@0 4384 this.setBrowserSize(viewportW, viewportH);
michael@0 4385
michael@0 4386 // This change to the zoom accounts for all types of changes I can conceive:
michael@0 4387 // 1. screen size changes, CSS viewport does not (pages with no meta viewport
michael@0 4388 // or a fixed size viewport)
michael@0 4389 // 2. screen size changes, CSS viewport also does (pages with a device-width
michael@0 4390 // viewport)
michael@0 4391 // 3. screen size remains constant, but CSS viewport changes (meta viewport
michael@0 4392 // tag is added or removed)
michael@0 4393 // 4. neither screen size nor CSS viewport changes
michael@0 4394 //
michael@0 4395 // In all of these cases, we maintain how much actual content is visible
michael@0 4396 // within the screen width. Note that "actual content" may be different
michael@0 4397 // with respect to CSS pixels because of the CSS viewport size changing.
michael@0 4398 let zoom = this.restoredSessionZoom() || metadata.defaultZoom;
michael@0 4399 if (!zoom || !aInitialLoad) {
michael@0 4400 let zoomScale = (screenW * oldBrowserWidth) / (aOldScreenWidth * viewportW);
michael@0 4401 zoom = this.clampZoom(this._zoom * zoomScale);
michael@0 4402 }
michael@0 4403 this.setResolution(zoom, false);
michael@0 4404 this.setScrollClampingSize(zoom);
michael@0 4405
michael@0 4406 // if this page has not been painted yet, then this must be getting run
michael@0 4407 // because a meta-viewport element was added (via the DOMMetaAdded handler).
michael@0 4408 // in this case, we should not do anything that forces a reflow (see bug 759678)
michael@0 4409 // such as requesting the page size or sending a viewport update. this code
michael@0 4410 // will get run again in the before-first-paint handler and that point we
michael@0 4411 // will run though all of it. the reason we even bother executing up to this
michael@0 4412 // point on the DOMMetaAdded handler is so that scripts that use window.innerWidth
michael@0 4413 // before they are painted have a correct value (bug 771575).
michael@0 4414 if (!this.contentDocumentIsDisplayed) {
michael@0 4415 return;
michael@0 4416 }
michael@0 4417
michael@0 4418 this.viewportExcludesHorizontalMargins = true;
michael@0 4419 this.viewportExcludesVerticalMargins = true;
michael@0 4420 let minScale = 1.0;
michael@0 4421 if (this.browser.contentDocument) {
michael@0 4422 // this may get run during a Viewport:Change message while the document
michael@0 4423 // has not yet loaded, so need to guard against a null document.
michael@0 4424 let [pageWidth, pageHeight] = this.getPageSize(this.browser.contentDocument, viewportW, viewportH);
michael@0 4425
michael@0 4426 // In the situation the page size equals or exceeds the screen size,
michael@0 4427 // lengthen the viewport on the corresponding axis to include the margins.
michael@0 4428 // The '- 0.5' is to account for rounding errors.
michael@0 4429 if (pageWidth * this._zoom > gScreenWidth - 0.5) {
michael@0 4430 screenW = gScreenWidth;
michael@0 4431 this.viewportExcludesHorizontalMargins = false;
michael@0 4432 }
michael@0 4433 if (pageHeight * this._zoom > gScreenHeight - 0.5) {
michael@0 4434 screenH = gScreenHeight;
michael@0 4435 this.viewportExcludesVerticalMargins = false;
michael@0 4436 }
michael@0 4437
michael@0 4438 minScale = screenW / pageWidth;
michael@0 4439 }
michael@0 4440 minScale = this.clampZoom(minScale);
michael@0 4441 viewportH = Math.max(viewportH, screenH / minScale);
michael@0 4442
michael@0 4443 // In general we want to keep calls to setBrowserSize and setScrollClampingSize
michael@0 4444 // together because setBrowserSize could mark the viewport size as dirty, creating
michael@0 4445 // a pending resize event for content. If that resize gets dispatched (which happens
michael@0 4446 // on the next reflow) without setScrollClampingSize having being called, then
michael@0 4447 // content might be exposed to incorrect innerWidth/innerHeight values.
michael@0 4448 this.setBrowserSize(viewportW, viewportH);
michael@0 4449 this.setScrollClampingSize(zoom);
michael@0 4450
michael@0 4451 // Avoid having the scroll position jump around after device rotation.
michael@0 4452 let win = this.browser.contentWindow;
michael@0 4453 this.userScrollPos.x = win.scrollX;
michael@0 4454 this.userScrollPos.y = win.scrollY;
michael@0 4455
michael@0 4456 this.sendViewportUpdate();
michael@0 4457
michael@0 4458 if (metadata.allowZoom && !Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) {
michael@0 4459 // If the CSS viewport is narrower than the screen (i.e. width <= device-width)
michael@0 4460 // then we disable double-tap-to-zoom behaviour.
michael@0 4461 var oldAllowDoubleTapZoom = metadata.allowDoubleTapZoom;
michael@0 4462 var newAllowDoubleTapZoom = (!metadata.isSpecified) || (viewportW > screenW / window.devicePixelRatio);
michael@0 4463 if (oldAllowDoubleTapZoom !== newAllowDoubleTapZoom) {
michael@0 4464 metadata.allowDoubleTapZoom = newAllowDoubleTapZoom;
michael@0 4465 this.sendViewportMetadata();
michael@0 4466 }
michael@0 4467 }
michael@0 4468
michael@0 4469 // Store the page size that was used to calculate the viewport so that we
michael@0 4470 // can verify it's changed when we consider remeasuring in updateViewportForPageSize
michael@0 4471 let viewport = this.getViewport();
michael@0 4472 this.lastPageSizeAfterViewportRemeasure = {
michael@0 4473 width: viewport.pageRight - viewport.pageLeft,
michael@0 4474 height: viewport.pageBottom - viewport.pageTop
michael@0 4475 };
michael@0 4476 },
michael@0 4477
michael@0 4478 sendViewportMetadata: function sendViewportMetadata() {
michael@0 4479 let metadata = this.metadata;
michael@0 4480 sendMessageToJava({
michael@0 4481 type: "Tab:ViewportMetadata",
michael@0 4482 allowZoom: metadata.allowZoom,
michael@0 4483 allowDoubleTapZoom: metadata.allowDoubleTapZoom,
michael@0 4484 defaultZoom: metadata.defaultZoom || window.devicePixelRatio,
michael@0 4485 minZoom: metadata.minZoom || 0,
michael@0 4486 maxZoom: metadata.maxZoom || 0,
michael@0 4487 isRTL: metadata.isRTL,
michael@0 4488 tabID: this.id
michael@0 4489 });
michael@0 4490 },
michael@0 4491
michael@0 4492 setBrowserSize: function(aWidth, aHeight) {
michael@0 4493 if (fuzzyEquals(this.browserWidth, aWidth) && fuzzyEquals(this.browserHeight, aHeight)) {
michael@0 4494 return;
michael@0 4495 }
michael@0 4496
michael@0 4497 this.browserWidth = aWidth;
michael@0 4498 this.browserHeight = aHeight;
michael@0 4499
michael@0 4500 if (!this.browser.contentWindow)
michael@0 4501 return;
michael@0 4502 let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 4503 cwu.setCSSViewport(aWidth, aHeight);
michael@0 4504 },
michael@0 4505
michael@0 4506 /** Takes a scale and restricts it based on this tab's zoom limits. */
michael@0 4507 clampZoom: function clampZoom(aZoom) {
michael@0 4508 let zoom = ViewportHandler.clamp(aZoom, kViewportMinScale, kViewportMaxScale);
michael@0 4509
michael@0 4510 let md = this.metadata;
michael@0 4511 if (!md.allowZoom)
michael@0 4512 return md.defaultZoom || zoom;
michael@0 4513
michael@0 4514 if (md && md.minZoom)
michael@0 4515 zoom = Math.max(zoom, md.minZoom);
michael@0 4516 if (md && md.maxZoom)
michael@0 4517 zoom = Math.min(zoom, md.maxZoom);
michael@0 4518 return zoom;
michael@0 4519 },
michael@0 4520
michael@0 4521 observe: function(aSubject, aTopic, aData) {
michael@0 4522 switch (aTopic) {
michael@0 4523 case "before-first-paint":
michael@0 4524 // Is it on the top level?
michael@0 4525 let contentDocument = aSubject;
michael@0 4526 if (contentDocument == this.browser.contentDocument) {
michael@0 4527 if (BrowserApp.selectedTab == this) {
michael@0 4528 BrowserApp.contentDocumentChanged();
michael@0 4529 }
michael@0 4530 this.contentDocumentIsDisplayed = true;
michael@0 4531
michael@0 4532 // reset CSS viewport and zoom to default on new page, and then calculate
michael@0 4533 // them properly using the actual metadata from the page. note that the
michael@0 4534 // updateMetadata call takes into account the existing CSS viewport size
michael@0 4535 // and zoom when calculating the new ones, so we need to reset these
michael@0 4536 // things here before calling updateMetadata.
michael@0 4537 this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight);
michael@0 4538 let zoom = this.restoredSessionZoom() || gScreenWidth / this.browserWidth;
michael@0 4539 this.setResolution(zoom, true);
michael@0 4540 ViewportHandler.updateMetadata(this, true);
michael@0 4541
michael@0 4542 // Note that if we draw without a display-port, things can go wrong. By the
michael@0 4543 // time we execute this, it's almost certain a display-port has been set via
michael@0 4544 // the MozScrolledAreaChanged event. If that didn't happen, the updateMetadata
michael@0 4545 // call above does so at the end of the updateViewportSize function. As long
michael@0 4546 // as that is happening, we don't need to do it again here.
michael@0 4547
michael@0 4548 if (!this.restoredSessionZoom() && contentDocument.mozSyntheticDocument) {
michael@0 4549 // for images, scale to fit width. this needs to happen *after* the call
michael@0 4550 // to updateMetadata above, because that call sets the CSS viewport which
michael@0 4551 // will affect the page size (i.e. contentDocument.body.scroll*) that we
michael@0 4552 // use in this calculation. also we call sendViewportUpdate after changing
michael@0 4553 // the resolution so that the display port gets recalculated appropriately.
michael@0 4554 let fitZoom = Math.min(gScreenWidth / contentDocument.body.scrollWidth,
michael@0 4555 gScreenHeight / contentDocument.body.scrollHeight);
michael@0 4556 this.setResolution(fitZoom, false);
michael@0 4557 this.sendViewportUpdate();
michael@0 4558 }
michael@0 4559 }
michael@0 4560
michael@0 4561 // If the reflow-text-on-page-load pref is enabled, and reflow-on-zoom
michael@0 4562 // is enabled, and our defaultZoom level is set, then we need to get
michael@0 4563 // the default zoom and reflow the text according to the defaultZoom
michael@0 4564 // level.
michael@0 4565 let rzEnabled = BrowserEventHandler.mReflozPref;
michael@0 4566 let rzPl = Services.prefs.getBoolPref("browser.zoom.reflowZoom.reflowTextOnPageLoad");
michael@0 4567
michael@0 4568 if (rzEnabled && rzPl) {
michael@0 4569 // Retrieve the viewport width and adjust the max line box width
michael@0 4570 // accordingly.
michael@0 4571 let vp = BrowserApp.selectedTab.getViewport();
michael@0 4572 BrowserApp.selectedTab.performReflowOnZoom(vp);
michael@0 4573 }
michael@0 4574 break;
michael@0 4575 case "after-viewport-change":
michael@0 4576 if (BrowserApp.selectedTab._mReflozPositioned) {
michael@0 4577 BrowserApp.selectedTab.clearReflowOnZoomPendingActions();
michael@0 4578 }
michael@0 4579 break;
michael@0 4580 case "nsPref:changed":
michael@0 4581 if (aData == "browser.ui.zoom.force-user-scalable")
michael@0 4582 ViewportHandler.updateMetadata(this, false);
michael@0 4583 break;
michael@0 4584 }
michael@0 4585 },
michael@0 4586
michael@0 4587 set readerEnabled(isReaderEnabled) {
michael@0 4588 this._readerEnabled = isReaderEnabled;
michael@0 4589 if (this.getActive())
michael@0 4590 Reader.updatePageAction(this);
michael@0 4591 },
michael@0 4592
michael@0 4593 get readerEnabled() {
michael@0 4594 return this._readerEnabled;
michael@0 4595 },
michael@0 4596
michael@0 4597 set readerActive(isReaderActive) {
michael@0 4598 this._readerActive = isReaderActive;
michael@0 4599 if (this.getActive())
michael@0 4600 Reader.updatePageAction(this);
michael@0 4601 },
michael@0 4602
michael@0 4603 get readerActive() {
michael@0 4604 return this._readerActive;
michael@0 4605 },
michael@0 4606
michael@0 4607 // nsIBrowserTab
michael@0 4608 get window() {
michael@0 4609 if (!this.browser)
michael@0 4610 return null;
michael@0 4611 return this.browser.contentWindow;
michael@0 4612 },
michael@0 4613
michael@0 4614 get scale() {
michael@0 4615 return this._zoom;
michael@0 4616 },
michael@0 4617
michael@0 4618 QueryInterface: XPCOMUtils.generateQI([
michael@0 4619 Ci.nsIWebProgressListener,
michael@0 4620 Ci.nsISHistoryListener,
michael@0 4621 Ci.nsIObserver,
michael@0 4622 Ci.nsISupportsWeakReference,
michael@0 4623 Ci.nsIBrowserTab
michael@0 4624 ])
michael@0 4625 };
michael@0 4626
michael@0 4627 var BrowserEventHandler = {
michael@0 4628 init: function init() {
michael@0 4629 Services.obs.addObserver(this, "Gesture:SingleTap", false);
michael@0 4630 Services.obs.addObserver(this, "Gesture:CancelTouch", false);
michael@0 4631 Services.obs.addObserver(this, "Gesture:DoubleTap", false);
michael@0 4632 Services.obs.addObserver(this, "Gesture:Scroll", false);
michael@0 4633 Services.obs.addObserver(this, "dom-touch-listener-added", false);
michael@0 4634
michael@0 4635 BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false);
michael@0 4636 BrowserApp.deck.addEventListener("touchstart", this, true);
michael@0 4637 BrowserApp.deck.addEventListener("click", InputWidgetHelper, true);
michael@0 4638 BrowserApp.deck.addEventListener("click", SelectHelper, true);
michael@0 4639
michael@0 4640 SpatialNavigation.init(BrowserApp.deck, null);
michael@0 4641
michael@0 4642 document.addEventListener("MozMagnifyGesture", this, true);
michael@0 4643
michael@0 4644 Services.prefs.addObserver("browser.zoom.reflowOnZoom", this, false);
michael@0 4645 this.updateReflozPref();
michael@0 4646 },
michael@0 4647
michael@0 4648 resetMaxLineBoxWidth: function() {
michael@0 4649 BrowserApp.selectedTab.probablyNeedRefloz = false;
michael@0 4650
michael@0 4651 if (gReflowPending) {
michael@0 4652 clearTimeout(gReflowPending);
michael@0 4653 }
michael@0 4654
michael@0 4655 let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout");
michael@0 4656 gReflowPending = setTimeout(doChangeMaxLineBoxWidth,
michael@0 4657 reflozTimeout, 0);
michael@0 4658 },
michael@0 4659
michael@0 4660 updateReflozPref: function() {
michael@0 4661 this.mReflozPref = Services.prefs.getBoolPref("browser.zoom.reflowOnZoom");
michael@0 4662 },
michael@0 4663
michael@0 4664 handleEvent: function(aEvent) {
michael@0 4665 switch (aEvent.type) {
michael@0 4666 case 'touchstart':
michael@0 4667 this._handleTouchStart(aEvent);
michael@0 4668 break;
michael@0 4669 case 'MozMagnifyGesture':
michael@0 4670 this.observe(this, aEvent.type,
michael@0 4671 JSON.stringify({x: aEvent.screenX, y: aEvent.screenY,
michael@0 4672 zoomDelta: aEvent.delta}));
michael@0 4673 break;
michael@0 4674 }
michael@0 4675 },
michael@0 4676
michael@0 4677 _handleTouchStart: function(aEvent) {
michael@0 4678 if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.touches.length > 1 || aEvent.defaultPrevented)
michael@0 4679 return;
michael@0 4680
michael@0 4681 let closest = aEvent.target;
michael@0 4682
michael@0 4683 if (closest) {
michael@0 4684 // If we've pressed a scrollable element, let Java know that we may
michael@0 4685 // want to override the scroll behaviour (for document sub-frames)
michael@0 4686 this._scrollableElement = this._findScrollableElement(closest, true);
michael@0 4687 this._firstScrollEvent = true;
michael@0 4688
michael@0 4689 if (this._scrollableElement != null) {
michael@0 4690 // Discard if it's the top-level scrollable, we let Java handle this
michael@0 4691 // The top-level scrollable is the body in quirks mode and the html element
michael@0 4692 // in standards mode
michael@0 4693 let doc = BrowserApp.selectedBrowser.contentDocument;
michael@0 4694 let rootScrollable = (doc.compatMode === "BackCompat" ? doc.body : doc.documentElement);
michael@0 4695 if (this._scrollableElement != rootScrollable) {
michael@0 4696 sendMessageToJava({ type: "Panning:Override" });
michael@0 4697 }
michael@0 4698 }
michael@0 4699 }
michael@0 4700
michael@0 4701 if (!ElementTouchHelper.isElementClickable(closest, null, false))
michael@0 4702 closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX,
michael@0 4703 aEvent.changedTouches[0].screenY);
michael@0 4704 if (!closest)
michael@0 4705 closest = aEvent.target;
michael@0 4706
michael@0 4707 if (closest) {
michael@0 4708 let uri = this._getLinkURI(closest);
michael@0 4709 if (uri) {
michael@0 4710 try {
michael@0 4711 Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
michael@0 4712 } catch (e) {}
michael@0 4713 }
michael@0 4714 this._doTapHighlight(closest);
michael@0 4715 }
michael@0 4716 },
michael@0 4717
michael@0 4718 _getLinkURI: function(aElement) {
michael@0 4719 if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
michael@0 4720 ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) ||
michael@0 4721 (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) {
michael@0 4722 try {
michael@0 4723 return Services.io.newURI(aElement.href, null, null);
michael@0 4724 } catch (e) {}
michael@0 4725 }
michael@0 4726 return null;
michael@0 4727 },
michael@0 4728
michael@0 4729 observe: function(aSubject, aTopic, aData) {
michael@0 4730 if (aTopic == "dom-touch-listener-added") {
michael@0 4731 let tab = BrowserApp.getTabForWindow(aSubject.top);
michael@0 4732 if (!tab || tab.hasTouchListener)
michael@0 4733 return;
michael@0 4734
michael@0 4735 tab.hasTouchListener = true;
michael@0 4736 sendMessageToJava({
michael@0 4737 type: "Tab:HasTouchListener",
michael@0 4738 tabID: tab.id
michael@0 4739 });
michael@0 4740 return;
michael@0 4741 } else if (aTopic == "nsPref:changed") {
michael@0 4742 if (aData == "browser.zoom.reflowOnZoom") {
michael@0 4743 this.updateReflozPref();
michael@0 4744 }
michael@0 4745 return;
michael@0 4746 }
michael@0 4747
michael@0 4748 // the remaining events are all dependent on the browser content document being the
michael@0 4749 // same as the browser displayed document. if they are not the same, we should ignore
michael@0 4750 // the event.
michael@0 4751 if (BrowserApp.isBrowserContentDocumentDisplayed()) {
michael@0 4752 this.handleUserEvent(aTopic, aData);
michael@0 4753 }
michael@0 4754 },
michael@0 4755
michael@0 4756 handleUserEvent: function(aTopic, aData) {
michael@0 4757 switch (aTopic) {
michael@0 4758
michael@0 4759 case "Gesture:Scroll": {
michael@0 4760 // If we've lost our scrollable element, return. Don't cancel the
michael@0 4761 // override, as we probably don't want Java to handle panning until the
michael@0 4762 // user releases their finger.
michael@0 4763 if (this._scrollableElement == null)
michael@0 4764 return;
michael@0 4765
michael@0 4766 // If this is the first scroll event and we can't scroll in the direction
michael@0 4767 // the user wanted, and neither can any non-root sub-frame, cancel the
michael@0 4768 // override so that Java can handle panning the main document.
michael@0 4769 let data = JSON.parse(aData);
michael@0 4770
michael@0 4771 // round the scroll amounts because they come in as floats and might be
michael@0 4772 // subject to minor rounding errors because of zoom values. I've seen values
michael@0 4773 // like 0.99 come in here and get truncated to 0; this avoids that problem.
michael@0 4774 let zoom = BrowserApp.selectedTab._zoom;
michael@0 4775 let x = Math.round(data.x / zoom);
michael@0 4776 let y = Math.round(data.y / zoom);
michael@0 4777
michael@0 4778 if (this._firstScrollEvent) {
michael@0 4779 while (this._scrollableElement != null &&
michael@0 4780 !this._elementCanScroll(this._scrollableElement, x, y))
michael@0 4781 this._scrollableElement = this._findScrollableElement(this._scrollableElement, false);
michael@0 4782
michael@0 4783 let doc = BrowserApp.selectedBrowser.contentDocument;
michael@0 4784 if (this._scrollableElement == null ||
michael@0 4785 this._scrollableElement == doc.documentElement) {
michael@0 4786 sendMessageToJava({ type: "Panning:CancelOverride" });
michael@0 4787 return;
michael@0 4788 }
michael@0 4789
michael@0 4790 this._firstScrollEvent = false;
michael@0 4791 }
michael@0 4792
michael@0 4793 // Scroll the scrollable element
michael@0 4794 if (this._elementCanScroll(this._scrollableElement, x, y)) {
michael@0 4795 this._scrollElementBy(this._scrollableElement, x, y);
michael@0 4796 sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: true });
michael@0 4797 SelectionHandler.subdocumentScrolled(this._scrollableElement);
michael@0 4798 } else {
michael@0 4799 sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: false });
michael@0 4800 }
michael@0 4801
michael@0 4802 break;
michael@0 4803 }
michael@0 4804
michael@0 4805 case "Gesture:CancelTouch":
michael@0 4806 this._cancelTapHighlight();
michael@0 4807 break;
michael@0 4808
michael@0 4809 case "Gesture:SingleTap": {
michael@0 4810 let element = this._highlightElement;
michael@0 4811 if (element) {
michael@0 4812 try {
michael@0 4813 let data = JSON.parse(aData);
michael@0 4814 let [x, y] = [data.x, data.y];
michael@0 4815 if (ElementTouchHelper.isElementClickable(element)) {
michael@0 4816 [x, y] = this._moveClickPoint(element, x, y);
michael@0 4817 }
michael@0 4818
michael@0 4819 // Was the element already focused before it was clicked?
michael@0 4820 let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser));
michael@0 4821
michael@0 4822 this._sendMouseEvent("mousemove", element, x, y);
michael@0 4823 this._sendMouseEvent("mousedown", element, x, y);
michael@0 4824 this._sendMouseEvent("mouseup", element, x, y);
michael@0 4825
michael@0 4826 // If the element was previously focused, show the caret attached to it.
michael@0 4827 if (isFocused)
michael@0 4828 SelectionHandler.attachCaret(element);
michael@0 4829
michael@0 4830 // scrollToFocusedInput does its own checks to find out if an element should be zoomed into
michael@0 4831 BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser);
michael@0 4832 } catch(e) {
michael@0 4833 Cu.reportError(e);
michael@0 4834 }
michael@0 4835 }
michael@0 4836 this._cancelTapHighlight();
michael@0 4837 break;
michael@0 4838 }
michael@0 4839
michael@0 4840 case"Gesture:DoubleTap":
michael@0 4841 this._cancelTapHighlight();
michael@0 4842 this.onDoubleTap(aData);
michael@0 4843 break;
michael@0 4844
michael@0 4845 case "MozMagnifyGesture":
michael@0 4846 this.onPinchFinish(aData);
michael@0 4847 break;
michael@0 4848
michael@0 4849 default:
michael@0 4850 dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"');
michael@0 4851 break;
michael@0 4852 }
michael@0 4853 },
michael@0 4854
michael@0 4855 onDoubleTap: function(aData) {
michael@0 4856 let data = JSON.parse(aData);
michael@0 4857 let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y);
michael@0 4858
michael@0 4859 // We only want to do this if reflow-on-zoom is enabled, we don't already
michael@0 4860 // have a reflow-on-zoom event pending, and the element upon which the user
michael@0 4861 // double-tapped isn't of a type we want to avoid reflow-on-zoom.
michael@0 4862 if (BrowserEventHandler.mReflozPref &&
michael@0 4863 !BrowserApp.selectedTab._mReflozPoint &&
michael@0 4864 !this._shouldSuppressReflowOnZoom(element)) {
michael@0 4865
michael@0 4866 // See comment above performReflowOnZoom() for a detailed description of
michael@0 4867 // the events happening in the reflow-on-zoom operation.
michael@0 4868 let data = JSON.parse(aData);
michael@0 4869 let zoomPointX = data.x;
michael@0 4870 let zoomPointY = data.y;
michael@0 4871
michael@0 4872 BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY,
michael@0 4873 range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) };
michael@0 4874
michael@0 4875 // Before we perform a reflow on zoom, let's disable painting.
michael@0 4876 let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation);
michael@0 4877 let docShell = webNav.QueryInterface(Ci.nsIDocShell);
michael@0 4878 let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
michael@0 4879 docViewer.pausePainting();
michael@0 4880
michael@0 4881 BrowserApp.selectedTab.probablyNeedRefloz = true;
michael@0 4882 }
michael@0 4883
michael@0 4884 if (!element) {
michael@0 4885 ZoomHelper.zoomOut();
michael@0 4886 return;
michael@0 4887 }
michael@0 4888
michael@0 4889 while (element && !this._shouldZoomToElement(element))
michael@0 4890 element = element.parentNode;
michael@0 4891
michael@0 4892 if (!element) {
michael@0 4893 ZoomHelper.zoomOut();
michael@0 4894 } else {
michael@0 4895 ZoomHelper.zoomToElement(element, data.y);
michael@0 4896 }
michael@0 4897 },
michael@0 4898
michael@0 4899 /**
michael@0 4900 * Determine if reflow-on-zoom functionality should be suppressed, given a
michael@0 4901 * particular element. Double-tapping on the following elements suppresses
michael@0 4902 * reflow-on-zoom:
michael@0 4903 *
michael@0 4904 * <video>, <object>, <embed>, <applet>, <canvas>, <img>, <media>, <pre>
michael@0 4905 */
michael@0 4906 _shouldSuppressReflowOnZoom: function(aElement) {
michael@0 4907 if (aElement instanceof Ci.nsIDOMHTMLVideoElement ||
michael@0 4908 aElement instanceof Ci.nsIDOMHTMLObjectElement ||
michael@0 4909 aElement instanceof Ci.nsIDOMHTMLEmbedElement ||
michael@0 4910 aElement instanceof Ci.nsIDOMHTMLAppletElement ||
michael@0 4911 aElement instanceof Ci.nsIDOMHTMLCanvasElement ||
michael@0 4912 aElement instanceof Ci.nsIDOMHTMLImageElement ||
michael@0 4913 aElement instanceof Ci.nsIDOMHTMLMediaElement ||
michael@0 4914 aElement instanceof Ci.nsIDOMHTMLPreElement) {
michael@0 4915 return true;
michael@0 4916 }
michael@0 4917
michael@0 4918 return false;
michael@0 4919 },
michael@0 4920
michael@0 4921 onPinchFinish: function(aData) {
michael@0 4922 let data = {};
michael@0 4923 try {
michael@0 4924 data = JSON.parse(aData);
michael@0 4925 } catch(ex) {
michael@0 4926 console.log(ex);
michael@0 4927 return;
michael@0 4928 }
michael@0 4929
michael@0 4930 if (BrowserEventHandler.mReflozPref &&
michael@0 4931 data.zoomDelta < 0.0) {
michael@0 4932 BrowserEventHandler.resetMaxLineBoxWidth();
michael@0 4933 }
michael@0 4934 },
michael@0 4935
michael@0 4936 _shouldZoomToElement: function(aElement) {
michael@0 4937 let win = aElement.ownerDocument.defaultView;
michael@0 4938 if (win.getComputedStyle(aElement, null).display == "inline")
michael@0 4939 return false;
michael@0 4940 if (aElement instanceof Ci.nsIDOMHTMLLIElement)
michael@0 4941 return false;
michael@0 4942 if (aElement instanceof Ci.nsIDOMHTMLQuoteElement)
michael@0 4943 return false;
michael@0 4944 return true;
michael@0 4945 },
michael@0 4946
michael@0 4947 _firstScrollEvent: false,
michael@0 4948
michael@0 4949 _scrollableElement: null,
michael@0 4950
michael@0 4951 _highlightElement: null,
michael@0 4952
michael@0 4953 _doTapHighlight: function _doTapHighlight(aElement) {
michael@0 4954 DOMUtils.setContentState(aElement, kStateActive);
michael@0 4955 this._highlightElement = aElement;
michael@0 4956 },
michael@0 4957
michael@0 4958 _cancelTapHighlight: function _cancelTapHighlight() {
michael@0 4959 if (!this._highlightElement)
michael@0 4960 return;
michael@0 4961
michael@0 4962 // If the active element is in a sub-frame, we need to make that frame's document
michael@0 4963 // active to remove the element's active state.
michael@0 4964 if (this._highlightElement.ownerDocument != BrowserApp.selectedBrowser.contentWindow.document)
michael@0 4965 DOMUtils.setContentState(this._highlightElement.ownerDocument.documentElement, kStateActive);
michael@0 4966
michael@0 4967 DOMUtils.setContentState(BrowserApp.selectedBrowser.contentWindow.document.documentElement, kStateActive);
michael@0 4968 this._highlightElement = null;
michael@0 4969 },
michael@0 4970
michael@0 4971 _updateLastPosition: function(x, y, dx, dy) {
michael@0 4972 this.lastX = x;
michael@0 4973 this.lastY = y;
michael@0 4974 this.lastTime = Date.now();
michael@0 4975
michael@0 4976 this.motionBuffer.push({ dx: dx, dy: dy, time: this.lastTime });
michael@0 4977 },
michael@0 4978
michael@0 4979 _moveClickPoint: function(aElement, aX, aY) {
michael@0 4980 // the element can be out of the aX/aY point because of the touch radius
michael@0 4981 // if outside, we gracefully move the touch point to the edge of the element
michael@0 4982 if (!(aElement instanceof HTMLHtmlElement)) {
michael@0 4983 let isTouchClick = true;
michael@0 4984 let rects = ElementTouchHelper.getContentClientRects(aElement);
michael@0 4985 for (let i = 0; i < rects.length; i++) {
michael@0 4986 let rect = rects[i];
michael@0 4987 let inBounds =
michael@0 4988 (aX > rect.left && aX < (rect.left + rect.width)) &&
michael@0 4989 (aY > rect.top && aY < (rect.top + rect.height));
michael@0 4990 if (inBounds) {
michael@0 4991 isTouchClick = false;
michael@0 4992 break;
michael@0 4993 }
michael@0 4994 }
michael@0 4995
michael@0 4996 if (isTouchClick) {
michael@0 4997 let rect = rects[0];
michael@0 4998 // if either width or height is zero, we don't want to move the click to the edge of the element. See bug 757208
michael@0 4999 if (rect.width != 0 && rect.height != 0) {
michael@0 5000 aX = Math.min(Math.ceil(rect.left + rect.width) - 1, Math.max(Math.ceil(rect.left), aX));
michael@0 5001 aY = Math.min(Math.ceil(rect.top + rect.height) - 1, Math.max(Math.ceil(rect.top), aY));
michael@0 5002 }
michael@0 5003 }
michael@0 5004 }
michael@0 5005 return [aX, aY];
michael@0 5006 },
michael@0 5007
michael@0 5008 _sendMouseEvent: function _sendMouseEvent(aName, aElement, aX, aY) {
michael@0 5009 let window = aElement.ownerDocument.defaultView;
michael@0 5010 try {
michael@0 5011 let cwu = window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 5012 cwu.sendMouseEventToWindow(aName, aX, aY, 0, 1, 0, true);
michael@0 5013 } catch(e) {
michael@0 5014 Cu.reportError(e);
michael@0 5015 }
michael@0 5016 },
michael@0 5017
michael@0 5018 _hasScrollableOverflow: function(elem) {
michael@0 5019 var win = elem.ownerDocument.defaultView;
michael@0 5020 if (!win)
michael@0 5021 return false;
michael@0 5022 var computedStyle = win.getComputedStyle(elem);
michael@0 5023 if (!computedStyle)
michael@0 5024 return false;
michael@0 5025 // We check for overflow:hidden only because all the other cases are scrollable
michael@0 5026 // under various conditions. See https://bugzilla.mozilla.org/show_bug.cgi?id=911574#c24
michael@0 5027 // for some more details.
michael@0 5028 return !(computedStyle.overflowX == 'hidden' && computedStyle.overflowY == 'hidden');
michael@0 5029 },
michael@0 5030
michael@0 5031 _findScrollableElement: function(elem, checkElem) {
michael@0 5032 // Walk the DOM tree until we find a scrollable element
michael@0 5033 let scrollable = false;
michael@0 5034 while (elem) {
michael@0 5035 /* Element is scrollable if its scroll-size exceeds its client size, and:
michael@0 5036 * - It has overflow other than 'hidden', or
michael@0 5037 * - It's a textarea node, or
michael@0 5038 * - It's a text input, or
michael@0 5039 * - It's a select element showing multiple rows
michael@0 5040 */
michael@0 5041 if (checkElem) {
michael@0 5042 if ((elem.scrollTopMax > 0 || elem.scrollLeftMax > 0) &&
michael@0 5043 (this._hasScrollableOverflow(elem) ||
michael@0 5044 elem.mozMatchesSelector("textarea")) ||
michael@0 5045 (elem instanceof HTMLInputElement && elem.mozIsTextField(false)) ||
michael@0 5046 (elem instanceof HTMLSelectElement && (elem.size > 1 || elem.multiple))) {
michael@0 5047 scrollable = true;
michael@0 5048 break;
michael@0 5049 }
michael@0 5050 } else {
michael@0 5051 checkElem = true;
michael@0 5052 }
michael@0 5053
michael@0 5054 // Propagate up iFrames
michael@0 5055 if (!elem.parentNode && elem.documentElement && elem.documentElement.ownerDocument)
michael@0 5056 elem = elem.documentElement.ownerDocument.defaultView.frameElement;
michael@0 5057 else
michael@0 5058 elem = elem.parentNode;
michael@0 5059 }
michael@0 5060
michael@0 5061 if (!scrollable)
michael@0 5062 return null;
michael@0 5063
michael@0 5064 return elem;
michael@0 5065 },
michael@0 5066
michael@0 5067 _scrollElementBy: function(elem, x, y) {
michael@0 5068 elem.scrollTop = elem.scrollTop + y;
michael@0 5069 elem.scrollLeft = elem.scrollLeft + x;
michael@0 5070 },
michael@0 5071
michael@0 5072 _elementCanScroll: function(elem, x, y) {
michael@0 5073 let scrollX = (x < 0 && elem.scrollLeft > 0)
michael@0 5074 || (x > 0 && elem.scrollLeft < elem.scrollLeftMax);
michael@0 5075
michael@0 5076 let scrollY = (y < 0 && elem.scrollTop > 0)
michael@0 5077 || (y > 0 && elem.scrollTop < elem.scrollTopMax);
michael@0 5078
michael@0 5079 return scrollX || scrollY;
michael@0 5080 }
michael@0 5081 };
michael@0 5082
michael@0 5083 const kReferenceDpi = 240; // standard "pixel" size used in some preferences
michael@0 5084
michael@0 5085 const ElementTouchHelper = {
michael@0 5086 /* Return the element at the given coordinates, starting from the given window and
michael@0 5087 drilling down through frames. If no window is provided, the top-level window of
michael@0 5088 the currently selected tab is used. The coordinates provided should be CSS pixels
michael@0 5089 relative to the window's scroll position. */
michael@0 5090 anyElementFromPoint: function(aX, aY, aWindow) {
michael@0 5091 let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow);
michael@0 5092 let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 5093 let elem = cwu.elementFromPoint(aX, aY, true, true);
michael@0 5094
michael@0 5095 while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
michael@0 5096 let rect = elem.getBoundingClientRect();
michael@0 5097 aX -= rect.left;
michael@0 5098 aY -= rect.top;
michael@0 5099 cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 5100 elem = cwu.elementFromPoint(aX, aY, true, true);
michael@0 5101 }
michael@0 5102
michael@0 5103 return elem;
michael@0 5104 },
michael@0 5105
michael@0 5106 /* Return the most appropriate clickable element (if any), starting from the given window
michael@0 5107 and drilling down through iframes as necessary. If no window is provided, the top-level
michael@0 5108 window of the currently selected tab is used. The coordinates provided should be CSS
michael@0 5109 pixels relative to the window's scroll position. The element returned may not actually
michael@0 5110 contain the coordinates passed in because of touch radius and clickability heuristics. */
michael@0 5111 elementFromPoint: function(aX, aY, aWindow) {
michael@0 5112 // browser's elementFromPoint expect browser-relative client coordinates.
michael@0 5113 // subtract browser's scroll values to adjust
michael@0 5114 let win = (aWindow ? aWindow : BrowserApp.selectedBrowser.contentWindow);
michael@0 5115 let cwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 5116 let elem = this.getClosest(cwu, aX, aY);
michael@0 5117
michael@0 5118 // step through layers of IFRAMEs and FRAMES to find innermost element
michael@0 5119 while (elem && (elem instanceof HTMLIFrameElement || elem instanceof HTMLFrameElement)) {
michael@0 5120 // adjust client coordinates' origin to be top left of iframe viewport
michael@0 5121 let rect = elem.getBoundingClientRect();
michael@0 5122 aX -= rect.left;
michael@0 5123 aY -= rect.top;
michael@0 5124 cwu = elem.contentDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 5125 elem = this.getClosest(cwu, aX, aY);
michael@0 5126 }
michael@0 5127
michael@0 5128 return elem;
michael@0 5129 },
michael@0 5130
michael@0 5131 /* Returns the touch radius in content px. */
michael@0 5132 getTouchRadius: function getTouchRadius() {
michael@0 5133 let dpiRatio = ViewportHandler.displayDPI / kReferenceDpi;
michael@0 5134 let zoom = BrowserApp.selectedTab._zoom;
michael@0 5135 return {
michael@0 5136 top: this.radius.top * dpiRatio / zoom,
michael@0 5137 right: this.radius.right * dpiRatio / zoom,
michael@0 5138 bottom: this.radius.bottom * dpiRatio / zoom,
michael@0 5139 left: this.radius.left * dpiRatio / zoom
michael@0 5140 };
michael@0 5141 },
michael@0 5142
michael@0 5143 /* Returns the touch radius in reference pixels. */
michael@0 5144 get radius() {
michael@0 5145 let prefs = Services.prefs;
michael@0 5146 delete this.radius;
michael@0 5147 return this.radius = { "top": prefs.getIntPref("browser.ui.touch.top"),
michael@0 5148 "right": prefs.getIntPref("browser.ui.touch.right"),
michael@0 5149 "bottom": prefs.getIntPref("browser.ui.touch.bottom"),
michael@0 5150 "left": prefs.getIntPref("browser.ui.touch.left")
michael@0 5151 };
michael@0 5152 },
michael@0 5153
michael@0 5154 get weight() {
michael@0 5155 delete this.weight;
michael@0 5156 return this.weight = { "visited": Services.prefs.getIntPref("browser.ui.touch.weight.visited") };
michael@0 5157 },
michael@0 5158
michael@0 5159 /* Retrieve the closest element to a point by looking at borders position */
michael@0 5160 getClosest: function getClosest(aWindowUtils, aX, aY) {
michael@0 5161 let target = aWindowUtils.elementFromPoint(aX, aY,
michael@0 5162 true, /* ignore root scroll frame*/
michael@0 5163 false); /* don't flush layout */
michael@0 5164
michael@0 5165 // if this element is clickable we return quickly. also, if it isn't,
michael@0 5166 // use a cache to speed up future calls to isElementClickable in the
michael@0 5167 // loop below.
michael@0 5168 let unclickableCache = new Array();
michael@0 5169 if (this.isElementClickable(target, unclickableCache, false))
michael@0 5170 return target;
michael@0 5171
michael@0 5172 target = null;
michael@0 5173 let radius = this.getTouchRadius();
michael@0 5174 let nodes = aWindowUtils.nodesFromRect(aX, aY, radius.top, radius.right, radius.bottom, radius.left, true, false);
michael@0 5175
michael@0 5176 let threshold = Number.POSITIVE_INFINITY;
michael@0 5177 for (let i = 0; i < nodes.length; i++) {
michael@0 5178 let current = nodes[i];
michael@0 5179 if (!current.mozMatchesSelector || !this.isElementClickable(current, unclickableCache, true))
michael@0 5180 continue;
michael@0 5181
michael@0 5182 let rect = current.getBoundingClientRect();
michael@0 5183 let distance = this._computeDistanceFromRect(aX, aY, rect);
michael@0 5184
michael@0 5185 // increase a little bit the weight for already visited items
michael@0 5186 if (current && current.mozMatchesSelector("*:visited"))
michael@0 5187 distance *= (this.weight.visited / 100);
michael@0 5188
michael@0 5189 if (distance < threshold) {
michael@0 5190 target = current;
michael@0 5191 threshold = distance;
michael@0 5192 }
michael@0 5193 }
michael@0 5194
michael@0 5195 return target;
michael@0 5196 },
michael@0 5197
michael@0 5198 isElementClickable: function isElementClickable(aElement, aUnclickableCache, aAllowBodyListeners) {
michael@0 5199 const selector = "a,:link,:visited,[role=button],button,input,select,textarea";
michael@0 5200
michael@0 5201 let stopNode = null;
michael@0 5202 if (!aAllowBodyListeners && aElement && aElement.ownerDocument)
michael@0 5203 stopNode = aElement.ownerDocument.body;
michael@0 5204
michael@0 5205 for (let elem = aElement; elem && elem != stopNode; elem = elem.parentNode) {
michael@0 5206 if (aUnclickableCache && aUnclickableCache.indexOf(elem) != -1)
michael@0 5207 continue;
michael@0 5208 if (this._hasMouseListener(elem))
michael@0 5209 return true;
michael@0 5210 if (elem.mozMatchesSelector && elem.mozMatchesSelector(selector))
michael@0 5211 return true;
michael@0 5212 if (elem instanceof HTMLLabelElement && elem.control != null)
michael@0 5213 return true;
michael@0 5214 if (aUnclickableCache)
michael@0 5215 aUnclickableCache.push(elem);
michael@0 5216 }
michael@0 5217 return false;
michael@0 5218 },
michael@0 5219
michael@0 5220 _computeDistanceFromRect: function _computeDistanceFromRect(aX, aY, aRect) {
michael@0 5221 let x = 0, y = 0;
michael@0 5222 let xmost = aRect.left + aRect.width;
michael@0 5223 let ymost = aRect.top + aRect.height;
michael@0 5224
michael@0 5225 // compute horizontal distance from left/right border depending if X is
michael@0 5226 // before/inside/after the element's rectangle
michael@0 5227 if (aRect.left < aX && aX < xmost)
michael@0 5228 x = Math.min(xmost - aX, aX - aRect.left);
michael@0 5229 else if (aX < aRect.left)
michael@0 5230 x = aRect.left - aX;
michael@0 5231 else if (aX > xmost)
michael@0 5232 x = aX - xmost;
michael@0 5233
michael@0 5234 // compute vertical distance from top/bottom border depending if Y is
michael@0 5235 // above/inside/below the element's rectangle
michael@0 5236 if (aRect.top < aY && aY < ymost)
michael@0 5237 y = Math.min(ymost - aY, aY - aRect.top);
michael@0 5238 else if (aY < aRect.top)
michael@0 5239 y = aRect.top - aY;
michael@0 5240 if (aY > ymost)
michael@0 5241 y = aY - ymost;
michael@0 5242
michael@0 5243 return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
michael@0 5244 },
michael@0 5245
michael@0 5246 _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),
michael@0 5247 _clickableEvents: ["mousedown", "mouseup", "click"],
michael@0 5248 _hasMouseListener: function _hasMouseListener(aElement) {
michael@0 5249 let els = this._els;
michael@0 5250 let listeners = els.getListenerInfoFor(aElement, {});
michael@0 5251 for (let i = 0; i < listeners.length; i++) {
michael@0 5252 if (this._clickableEvents.indexOf(listeners[i].type) != -1)
michael@0 5253 return true;
michael@0 5254 }
michael@0 5255 return false;
michael@0 5256 },
michael@0 5257
michael@0 5258 getContentClientRects: function(aElement) {
michael@0 5259 let offset = { x: 0, y: 0 };
michael@0 5260
michael@0 5261 let nativeRects = aElement.getClientRects();
michael@0 5262 // step out of iframes and frames, offsetting scroll values
michael@0 5263 for (let frame = aElement.ownerDocument.defaultView; frame.frameElement; frame = frame.parent) {
michael@0 5264 // adjust client coordinates' origin to be top left of iframe viewport
michael@0 5265 let rect = frame.frameElement.getBoundingClientRect();
michael@0 5266 let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
michael@0 5267 let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
michael@0 5268 offset.x += rect.left + parseInt(left);
michael@0 5269 offset.y += rect.top + parseInt(top);
michael@0 5270 }
michael@0 5271
michael@0 5272 let result = [];
michael@0 5273 for (let i = nativeRects.length - 1; i >= 0; i--) {
michael@0 5274 let r = nativeRects[i];
michael@0 5275 result.push({ left: r.left + offset.x,
michael@0 5276 top: r.top + offset.y,
michael@0 5277 width: r.width,
michael@0 5278 height: r.height
michael@0 5279 });
michael@0 5280 }
michael@0 5281 return result;
michael@0 5282 },
michael@0 5283
michael@0 5284 getBoundingContentRect: function(aElement) {
michael@0 5285 if (!aElement)
michael@0 5286 return {x: 0, y: 0, w: 0, h: 0};
michael@0 5287
michael@0 5288 let document = aElement.ownerDocument;
michael@0 5289 while (document.defaultView.frameElement)
michael@0 5290 document = document.defaultView.frameElement.ownerDocument;
michael@0 5291
michael@0 5292 let cwu = document.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 5293 let scrollX = {}, scrollY = {};
michael@0 5294 cwu.getScrollXY(false, scrollX, scrollY);
michael@0 5295
michael@0 5296 let r = aElement.getBoundingClientRect();
michael@0 5297
michael@0 5298 // step out of iframes and frames, offsetting scroll values
michael@0 5299 for (let frame = aElement.ownerDocument.defaultView; frame.frameElement && frame != content; frame = frame.parent) {
michael@0 5300 // adjust client coordinates' origin to be top left of iframe viewport
michael@0 5301 let rect = frame.frameElement.getBoundingClientRect();
michael@0 5302 let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
michael@0 5303 let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
michael@0 5304 scrollX.value += rect.left + parseInt(left);
michael@0 5305 scrollY.value += rect.top + parseInt(top);
michael@0 5306 }
michael@0 5307
michael@0 5308 return {x: r.left + scrollX.value,
michael@0 5309 y: r.top + scrollY.value,
michael@0 5310 w: r.width,
michael@0 5311 h: r.height };
michael@0 5312 }
michael@0 5313 };
michael@0 5314
michael@0 5315 var ErrorPageEventHandler = {
michael@0 5316 handleEvent: function(aEvent) {
michael@0 5317 switch (aEvent.type) {
michael@0 5318 case "click": {
michael@0 5319 // Don't trust synthetic events
michael@0 5320 if (!aEvent.isTrusted)
michael@0 5321 return;
michael@0 5322
michael@0 5323 let target = aEvent.originalTarget;
michael@0 5324 let errorDoc = target.ownerDocument;
michael@0 5325
michael@0 5326 // If the event came from an ssl error page, it is probably either the "Add
michael@0 5327 // Exception…" or "Get me out of here!" button
michael@0 5328 if (errorDoc.documentURI.startsWith("about:certerror?e=nssBadCert")) {
michael@0 5329 let perm = errorDoc.getElementById("permanentExceptionButton");
michael@0 5330 let temp = errorDoc.getElementById("temporaryExceptionButton");
michael@0 5331 if (target == temp || target == perm) {
michael@0 5332 // Handle setting an cert exception and reloading the page
michael@0 5333 try {
michael@0 5334 // Add a new SSL exception for this URL
michael@0 5335 let uri = Services.io.newURI(errorDoc.location.href, null, null);
michael@0 5336 let sslExceptions = new SSLExceptions();
michael@0 5337
michael@0 5338 if (target == perm)
michael@0 5339 sslExceptions.addPermanentException(uri, errorDoc.defaultView);
michael@0 5340 else
michael@0 5341 sslExceptions.addTemporaryException(uri, errorDoc.defaultView);
michael@0 5342 } catch (e) {
michael@0 5343 dump("Failed to set cert exception: " + e + "\n");
michael@0 5344 }
michael@0 5345 errorDoc.location.reload();
michael@0 5346 } else if (target == errorDoc.getElementById("getMeOutOfHereButton")) {
michael@0 5347 errorDoc.location = "about:home";
michael@0 5348 }
michael@0 5349 } else if (errorDoc.documentURI.startsWith("about:blocked")) {
michael@0 5350 // The event came from a button on a malware/phishing block page
michael@0 5351 // First check whether it's malware or phishing, so that we can
michael@0 5352 // use the right strings/links
michael@0 5353 let isMalware = errorDoc.documentURI.contains("e=malwareBlocked");
michael@0 5354 let bucketName = isMalware ? "WARNING_MALWARE_PAGE_" : "WARNING_PHISHING_PAGE_";
michael@0 5355 let nsISecTel = Ci.nsISecurityUITelemetry;
michael@0 5356 let isIframe = (errorDoc.defaultView.parent === errorDoc.defaultView);
michael@0 5357 bucketName += isIframe ? "TOP_" : "FRAME_";
michael@0 5358
michael@0 5359 let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
michael@0 5360
michael@0 5361 if (target == errorDoc.getElementById("getMeOutButton")) {
michael@0 5362 Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "GET_ME_OUT_OF_HERE"]);
michael@0 5363 errorDoc.location = "about:home";
michael@0 5364 } else if (target == errorDoc.getElementById("reportButton")) {
michael@0 5365 // We log even if malware/phishing info URL couldn't be found:
michael@0 5366 // the measurement is for how many users clicked the WHY BLOCKED button
michael@0 5367 Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "WHY_BLOCKED"]);
michael@0 5368
michael@0 5369 // This is the "Why is this site blocked" button. For malware,
michael@0 5370 // we can fetch a site-specific report, for phishing, we redirect
michael@0 5371 // to the generic page describing phishing protection.
michael@0 5372 if (isMalware) {
michael@0 5373 // Get the stop badware "why is this blocked" report url, append the current url, and go there.
michael@0 5374 try {
michael@0 5375 let reportURL = formatter.formatURLPref("browser.safebrowsing.malware.reportURL");
michael@0 5376 reportURL += errorDoc.location.href;
michael@0 5377 BrowserApp.selectedBrowser.loadURI(reportURL);
michael@0 5378 } catch (e) {
michael@0 5379 Cu.reportError("Couldn't get malware report URL: " + e);
michael@0 5380 }
michael@0 5381 } else {
michael@0 5382 // It's a phishing site, just link to the generic information page
michael@0 5383 let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
michael@0 5384 BrowserApp.selectedBrowser.loadURI(url + "phishing-malware");
michael@0 5385 }
michael@0 5386 } else if (target == errorDoc.getElementById("ignoreWarningButton")) {
michael@0 5387 Telemetry.addData("SECURITY_UI", nsISecTel[bucketName + "IGNORE_WARNING"]);
michael@0 5388
michael@0 5389 // Allow users to override and continue through to the site,
michael@0 5390 let webNav = BrowserApp.selectedBrowser.docShell.QueryInterface(Ci.nsIWebNavigation);
michael@0 5391 let location = BrowserApp.selectedBrowser.contentWindow.location;
michael@0 5392 webNav.loadURI(location, Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER, null, null, null);
michael@0 5393
michael@0 5394 // ....but add a notify bar as a reminder, so that they don't lose
michael@0 5395 // track after, e.g., tab switching.
michael@0 5396 NativeWindow.doorhanger.show(Strings.browser.GetStringFromName("safeBrowsingDoorhanger"), "safebrowsing-warning", [], BrowserApp.selectedTab.id);
michael@0 5397 }
michael@0 5398 }
michael@0 5399 break;
michael@0 5400 }
michael@0 5401 }
michael@0 5402 }
michael@0 5403 };
michael@0 5404
michael@0 5405 var FormAssistant = {
michael@0 5406 QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormSubmitObserver]),
michael@0 5407
michael@0 5408 // Used to keep track of the element that corresponds to the current
michael@0 5409 // autocomplete suggestions
michael@0 5410 _currentInputElement: null,
michael@0 5411
michael@0 5412 _isBlocklisted: false,
michael@0 5413
michael@0 5414 // Keep track of whether or not an invalid form has been submitted
michael@0 5415 _invalidSubmit: false,
michael@0 5416
michael@0 5417 init: function() {
michael@0 5418 Services.obs.addObserver(this, "FormAssist:AutoComplete", false);
michael@0 5419 Services.obs.addObserver(this, "FormAssist:Blocklisted", false);
michael@0 5420 Services.obs.addObserver(this, "FormAssist:Hidden", false);
michael@0 5421 Services.obs.addObserver(this, "invalidformsubmit", false);
michael@0 5422 Services.obs.addObserver(this, "PanZoom:StateChange", false);
michael@0 5423
michael@0 5424 // We need to use a capturing listener for focus events
michael@0 5425 BrowserApp.deck.addEventListener("focus", this, true);
michael@0 5426 BrowserApp.deck.addEventListener("click", this, true);
michael@0 5427 BrowserApp.deck.addEventListener("input", this, false);
michael@0 5428 BrowserApp.deck.addEventListener("pageshow", this, false);
michael@0 5429 },
michael@0 5430
michael@0 5431 uninit: function() {
michael@0 5432 Services.obs.removeObserver(this, "FormAssist:AutoComplete");
michael@0 5433 Services.obs.removeObserver(this, "FormAssist:Blocklisted");
michael@0 5434 Services.obs.removeObserver(this, "FormAssist:Hidden");
michael@0 5435 Services.obs.removeObserver(this, "invalidformsubmit");
michael@0 5436 Services.obs.removeObserver(this, "PanZoom:StateChange");
michael@0 5437
michael@0 5438 BrowserApp.deck.removeEventListener("focus", this);
michael@0 5439 BrowserApp.deck.removeEventListener("click", this);
michael@0 5440 BrowserApp.deck.removeEventListener("input", this);
michael@0 5441 BrowserApp.deck.removeEventListener("pageshow", this);
michael@0 5442 },
michael@0 5443
michael@0 5444 observe: function(aSubject, aTopic, aData) {
michael@0 5445 switch (aTopic) {
michael@0 5446 case "PanZoom:StateChange":
michael@0 5447 // If the user is just touching the screen and we haven't entered a pan or zoom state yet do nothing
michael@0 5448 if (aData == "TOUCHING" || aData == "WAITING_LISTENERS")
michael@0 5449 break;
michael@0 5450 if (aData == "NOTHING") {
michael@0 5451 // only look for input elements, not contentEditable or multiline text areas
michael@0 5452 let focused = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true);
michael@0 5453 if (!focused)
michael@0 5454 break;
michael@0 5455
michael@0 5456 if (this._showValidationMessage(focused))
michael@0 5457 break;
michael@0 5458 this._showAutoCompleteSuggestions(focused, function () {});
michael@0 5459 } else {
michael@0 5460 // temporarily hide the form assist popup while we're panning or zooming the page
michael@0 5461 this._hideFormAssistPopup();
michael@0 5462 }
michael@0 5463 break;
michael@0 5464 case "FormAssist:AutoComplete":
michael@0 5465 if (!this._currentInputElement)
michael@0 5466 break;
michael@0 5467
michael@0 5468 let editableElement = this._currentInputElement.QueryInterface(Ci.nsIDOMNSEditableElement);
michael@0 5469
michael@0 5470 // If we have an active composition string, commit it before sending
michael@0 5471 // the autocomplete event with the text that will replace it.
michael@0 5472 try {
michael@0 5473 let imeEditor = editableElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
michael@0 5474 if (imeEditor.composing)
michael@0 5475 imeEditor.forceCompositionEnd();
michael@0 5476 } catch (e) {}
michael@0 5477
michael@0 5478 editableElement.setUserInput(aData);
michael@0 5479
michael@0 5480 let event = this._currentInputElement.ownerDocument.createEvent("Events");
michael@0 5481 event.initEvent("DOMAutoComplete", true, true);
michael@0 5482 this._currentInputElement.dispatchEvent(event);
michael@0 5483 break;
michael@0 5484
michael@0 5485 case "FormAssist:Blocklisted":
michael@0 5486 this._isBlocklisted = (aData == "true");
michael@0 5487 break;
michael@0 5488
michael@0 5489 case "FormAssist:Hidden":
michael@0 5490 this._currentInputElement = null;
michael@0 5491 break;
michael@0 5492 }
michael@0 5493 },
michael@0 5494
michael@0 5495 notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
michael@0 5496 if (!aInvalidElements.length)
michael@0 5497 return;
michael@0 5498
michael@0 5499 // Ignore this notificaiton if the current tab doesn't contain the invalid form
michael@0 5500 if (BrowserApp.selectedBrowser.contentDocument !=
michael@0 5501 aFormElement.ownerDocument.defaultView.top.document)
michael@0 5502 return;
michael@0 5503
michael@0 5504 this._invalidSubmit = true;
michael@0 5505
michael@0 5506 // Our focus listener will show the element's validation message
michael@0 5507 let currentElement = aInvalidElements.queryElementAt(0, Ci.nsISupports);
michael@0 5508 currentElement.focus();
michael@0 5509 },
michael@0 5510
michael@0 5511 handleEvent: function(aEvent) {
michael@0 5512 switch (aEvent.type) {
michael@0 5513 case "focus":
michael@0 5514 let currentElement = aEvent.target;
michael@0 5515
michael@0 5516 // Only show a validation message on focus.
michael@0 5517 this._showValidationMessage(currentElement);
michael@0 5518 break;
michael@0 5519
michael@0 5520 case "click":
michael@0 5521 currentElement = aEvent.target;
michael@0 5522
michael@0 5523 // Prioritize a form validation message over autocomplete suggestions
michael@0 5524 // when the element is first focused (a form validation message will
michael@0 5525 // only be available if an invalid form was submitted)
michael@0 5526 if (this._showValidationMessage(currentElement))
michael@0 5527 break;
michael@0 5528
michael@0 5529 let checkResultsClick = hasResults => {
michael@0 5530 if (!hasResults) {
michael@0 5531 this._hideFormAssistPopup();
michael@0 5532 }
michael@0 5533 };
michael@0 5534
michael@0 5535 this._showAutoCompleteSuggestions(currentElement, checkResultsClick);
michael@0 5536 break;
michael@0 5537
michael@0 5538 case "input":
michael@0 5539 currentElement = aEvent.target;
michael@0 5540
michael@0 5541 // Since we can only show one popup at a time, prioritze autocomplete
michael@0 5542 // suggestions over a form validation message
michael@0 5543 let checkResultsInput = hasResults => {
michael@0 5544 if (hasResults)
michael@0 5545 return;
michael@0 5546
michael@0 5547 if (this._showValidationMessage(currentElement))
michael@0 5548 return;
michael@0 5549
michael@0 5550 // If we're not showing autocomplete suggestions, hide the form assist popup
michael@0 5551 this._hideFormAssistPopup();
michael@0 5552 };
michael@0 5553
michael@0 5554 this._showAutoCompleteSuggestions(currentElement, checkResultsInput);
michael@0 5555 break;
michael@0 5556
michael@0 5557 // Reset invalid submit state on each pageshow
michael@0 5558 case "pageshow":
michael@0 5559 if (!this._invalidSubmit)
michael@0 5560 return;
michael@0 5561
michael@0 5562 let selectedBrowser = BrowserApp.selectedBrowser;
michael@0 5563 if (selectedBrowser) {
michael@0 5564 let selectedDocument = selectedBrowser.contentDocument;
michael@0 5565 let target = aEvent.originalTarget;
michael@0 5566 if (target == selectedDocument || target.ownerDocument == selectedDocument)
michael@0 5567 this._invalidSubmit = false;
michael@0 5568 }
michael@0 5569 }
michael@0 5570 },
michael@0 5571
michael@0 5572 // We only want to show autocomplete suggestions for certain elements
michael@0 5573 _isAutoComplete: function _isAutoComplete(aElement) {
michael@0 5574 if (!(aElement instanceof HTMLInputElement) || aElement.readOnly ||
michael@0 5575 (aElement.getAttribute("type") == "password") ||
michael@0 5576 (aElement.hasAttribute("autocomplete") &&
michael@0 5577 aElement.getAttribute("autocomplete").toLowerCase() == "off"))
michael@0 5578 return false;
michael@0 5579
michael@0 5580 return true;
michael@0 5581 },
michael@0 5582
michael@0 5583 // Retrieves autocomplete suggestions for an element from the form autocomplete service.
michael@0 5584 // aCallback(array_of_suggestions) is called when results are available.
michael@0 5585 _getAutoCompleteSuggestions: function _getAutoCompleteSuggestions(aSearchString, aElement, aCallback) {
michael@0 5586 // Cache the form autocomplete service for future use
michael@0 5587 if (!this._formAutoCompleteService)
michael@0 5588 this._formAutoCompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"].
michael@0 5589 getService(Ci.nsIFormAutoComplete);
michael@0 5590
michael@0 5591 let resultsAvailable = function (results) {
michael@0 5592 let suggestions = [];
michael@0 5593 for (let i = 0; i < results.matchCount; i++) {
michael@0 5594 let value = results.getValueAt(i);
michael@0 5595
michael@0 5596 // Do not show the value if it is the current one in the input field
michael@0 5597 if (value == aSearchString)
michael@0 5598 continue;
michael@0 5599
michael@0 5600 // Supply a label and value, since they can differ for datalist suggestions
michael@0 5601 suggestions.push({ label: value, value: value });
michael@0 5602 }
michael@0 5603 aCallback(suggestions);
michael@0 5604 };
michael@0 5605
michael@0 5606 this._formAutoCompleteService.autoCompleteSearchAsync(aElement.name || aElement.id,
michael@0 5607 aSearchString, aElement, null,
michael@0 5608 resultsAvailable);
michael@0 5609 },
michael@0 5610
michael@0 5611 /**
michael@0 5612 * (Copied from mobile/xul/chrome/content/forms.js)
michael@0 5613 * This function is similar to getListSuggestions from
michael@0 5614 * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
michael@0 5615 * used by the autocomplete.xml binding which is not in used in fennec
michael@0 5616 */
michael@0 5617 _getListSuggestions: function _getListSuggestions(aElement) {
michael@0 5618 if (!(aElement instanceof HTMLInputElement) || !aElement.list)
michael@0 5619 return [];
michael@0 5620
michael@0 5621 let suggestions = [];
michael@0 5622 let filter = !aElement.hasAttribute("mozNoFilter");
michael@0 5623 let lowerFieldValue = aElement.value.toLowerCase();
michael@0 5624
michael@0 5625 let options = aElement.list.options;
michael@0 5626 let length = options.length;
michael@0 5627 for (let i = 0; i < length; i++) {
michael@0 5628 let item = options.item(i);
michael@0 5629
michael@0 5630 let label = item.value;
michael@0 5631 if (item.label)
michael@0 5632 label = item.label;
michael@0 5633 else if (item.text)
michael@0 5634 label = item.text;
michael@0 5635
michael@0 5636 if (filter && !(label.toLowerCase().contains(lowerFieldValue)) )
michael@0 5637 continue;
michael@0 5638 suggestions.push({ label: label, value: item.value });
michael@0 5639 }
michael@0 5640
michael@0 5641 return suggestions;
michael@0 5642 },
michael@0 5643
michael@0 5644 // Retrieves autocomplete suggestions for an element from the form autocomplete service
michael@0 5645 // and sends the suggestions to the Java UI, along with element position data. As
michael@0 5646 // autocomplete queries are asynchronous, calls aCallback when done with a true
michael@0 5647 // argument if results were found and false if no results were found.
michael@0 5648 _showAutoCompleteSuggestions: function _showAutoCompleteSuggestions(aElement, aCallback) {
michael@0 5649 if (!this._isAutoComplete(aElement)) {
michael@0 5650 aCallback(false);
michael@0 5651 return;
michael@0 5652 }
michael@0 5653
michael@0 5654 // Don't display the form auto-complete popup after the user starts typing
michael@0 5655 // to avoid confusing somes IME. See bug 758820 and bug 632744.
michael@0 5656 if (this._isBlocklisted && aElement.value.length > 0) {
michael@0 5657 aCallback(false);
michael@0 5658 return;
michael@0 5659 }
michael@0 5660
michael@0 5661 let resultsAvailable = autoCompleteSuggestions => {
michael@0 5662 // On desktop, we show datalist suggestions below autocomplete suggestions,
michael@0 5663 // without duplicates removed.
michael@0 5664 let listSuggestions = this._getListSuggestions(aElement);
michael@0 5665 let suggestions = autoCompleteSuggestions.concat(listSuggestions);
michael@0 5666
michael@0 5667 // Return false if there are no suggestions to show
michael@0 5668 if (!suggestions.length) {
michael@0 5669 aCallback(false);
michael@0 5670 return;
michael@0 5671 }
michael@0 5672
michael@0 5673 sendMessageToJava({
michael@0 5674 type: "FormAssist:AutoComplete",
michael@0 5675 suggestions: suggestions,
michael@0 5676 rect: ElementTouchHelper.getBoundingContentRect(aElement)
michael@0 5677 });
michael@0 5678
michael@0 5679 // Keep track of input element so we can fill it in if the user
michael@0 5680 // selects an autocomplete suggestion
michael@0 5681 this._currentInputElement = aElement;
michael@0 5682 aCallback(true);
michael@0 5683 };
michael@0 5684
michael@0 5685 this._getAutoCompleteSuggestions(aElement.value, aElement, resultsAvailable);
michael@0 5686 },
michael@0 5687
michael@0 5688 // Only show a validation message if the user submitted an invalid form,
michael@0 5689 // there's a non-empty message string, and the element is the correct type
michael@0 5690 _isValidateable: function _isValidateable(aElement) {
michael@0 5691 if (!this._invalidSubmit ||
michael@0 5692 !aElement.validationMessage ||
michael@0 5693 !(aElement instanceof HTMLInputElement ||
michael@0 5694 aElement instanceof HTMLTextAreaElement ||
michael@0 5695 aElement instanceof HTMLSelectElement ||
michael@0 5696 aElement instanceof HTMLButtonElement))
michael@0 5697 return false;
michael@0 5698
michael@0 5699 return true;
michael@0 5700 },
michael@0 5701
michael@0 5702 // Sends a validation message and position data for an element to the Java UI.
michael@0 5703 // Returns true if there's a validation message to show, false otherwise.
michael@0 5704 _showValidationMessage: function _sendValidationMessage(aElement) {
michael@0 5705 if (!this._isValidateable(aElement))
michael@0 5706 return false;
michael@0 5707
michael@0 5708 sendMessageToJava({
michael@0 5709 type: "FormAssist:ValidationMessage",
michael@0 5710 validationMessage: aElement.validationMessage,
michael@0 5711 rect: ElementTouchHelper.getBoundingContentRect(aElement)
michael@0 5712 });
michael@0 5713
michael@0 5714 return true;
michael@0 5715 },
michael@0 5716
michael@0 5717 _hideFormAssistPopup: function _hideFormAssistPopup() {
michael@0 5718 sendMessageToJava({ type: "FormAssist:Hide" });
michael@0 5719 }
michael@0 5720 };
michael@0 5721
michael@0 5722 /**
michael@0 5723 * An object to watch for Gecko status changes -- add-on installs, pref changes
michael@0 5724 * -- and reflect them back to Java.
michael@0 5725 */
michael@0 5726 let HealthReportStatusListener = {
michael@0 5727 PREF_ACCEPT_LANG: "intl.accept_languages",
michael@0 5728 PREF_BLOCKLIST_ENABLED: "extensions.blocklist.enabled",
michael@0 5729
michael@0 5730 PREF_TELEMETRY_ENABLED:
michael@0 5731 #ifdef MOZ_TELEMETRY_REPORTING
michael@0 5732 "toolkit.telemetry.enabled",
michael@0 5733 #else
michael@0 5734 null,
michael@0 5735 #endif
michael@0 5736
michael@0 5737 init: function () {
michael@0 5738 try {
michael@0 5739 AddonManager.addAddonListener(this);
michael@0 5740 } catch (ex) {
michael@0 5741 console.log("Failed to initialize add-on status listener. FHR cannot report add-on state. " + ex);
michael@0 5742 }
michael@0 5743
michael@0 5744 console.log("Adding HealthReport:RequestSnapshot observer.");
michael@0 5745 Services.obs.addObserver(this, "HealthReport:RequestSnapshot", false);
michael@0 5746 Services.prefs.addObserver(this.PREF_ACCEPT_LANG, this, false);
michael@0 5747 Services.prefs.addObserver(this.PREF_BLOCKLIST_ENABLED, this, false);
michael@0 5748 if (this.PREF_TELEMETRY_ENABLED) {
michael@0 5749 Services.prefs.addObserver(this.PREF_TELEMETRY_ENABLED, this, false);
michael@0 5750 }
michael@0 5751 },
michael@0 5752
michael@0 5753 uninit: function () {
michael@0 5754 Services.obs.removeObserver(this, "HealthReport:RequestSnapshot");
michael@0 5755 Services.prefs.removeObserver(this.PREF_ACCEPT_LANG, this);
michael@0 5756 Services.prefs.removeObserver(this.PREF_BLOCKLIST_ENABLED, this);
michael@0 5757 if (this.PREF_TELEMETRY_ENABLED) {
michael@0 5758 Services.prefs.removeObserver(this.PREF_TELEMETRY_ENABLED, this);
michael@0 5759 }
michael@0 5760
michael@0 5761 AddonManager.removeAddonListener(this);
michael@0 5762 },
michael@0 5763
michael@0 5764 observe: function (aSubject, aTopic, aData) {
michael@0 5765 switch (aTopic) {
michael@0 5766 case "HealthReport:RequestSnapshot":
michael@0 5767 HealthReportStatusListener.sendSnapshotToJava();
michael@0 5768 break;
michael@0 5769 case "nsPref:changed":
michael@0 5770 let response = {
michael@0 5771 type: "Pref:Change",
michael@0 5772 pref: aData,
michael@0 5773 isUserSet: Services.prefs.prefHasUserValue(aData),
michael@0 5774 };
michael@0 5775
michael@0 5776 switch (aData) {
michael@0 5777 case this.PREF_ACCEPT_LANG:
michael@0 5778 response.value = Services.prefs.getCharPref(aData);
michael@0 5779 break;
michael@0 5780 case this.PREF_TELEMETRY_ENABLED:
michael@0 5781 case this.PREF_BLOCKLIST_ENABLED:
michael@0 5782 response.value = Services.prefs.getBoolPref(aData);
michael@0 5783 break;
michael@0 5784 default:
michael@0 5785 console.log("Unexpected pref in HealthReportStatusListener: " + aData);
michael@0 5786 return;
michael@0 5787 }
michael@0 5788
michael@0 5789 sendMessageToJava(response);
michael@0 5790 break;
michael@0 5791 }
michael@0 5792 },
michael@0 5793
michael@0 5794 MILLISECONDS_PER_DAY: 24 * 60 * 60 * 1000,
michael@0 5795
michael@0 5796 COPY_FIELDS: [
michael@0 5797 "blocklistState",
michael@0 5798 "userDisabled",
michael@0 5799 "appDisabled",
michael@0 5800 "version",
michael@0 5801 "type",
michael@0 5802 "scope",
michael@0 5803 "foreignInstall",
michael@0 5804 "hasBinaryComponents",
michael@0 5805 ],
michael@0 5806
michael@0 5807 // Add-on types for which full details are recorded in FHR.
michael@0 5808 // All other types are ignored.
michael@0 5809 FULL_DETAIL_TYPES: [
michael@0 5810 "plugin",
michael@0 5811 "extension",
michael@0 5812 "service",
michael@0 5813 ],
michael@0 5814
michael@0 5815 /**
michael@0 5816 * Return true if the add-on is not of a type for which we report full details.
michael@0 5817 * These add-ons will still make it over to Java, but will be filtered out.
michael@0 5818 */
michael@0 5819 _shouldIgnore: function (aAddon) {
michael@0 5820 return this.FULL_DETAIL_TYPES.indexOf(aAddon.type) == -1;
michael@0 5821 },
michael@0 5822
michael@0 5823 _dateToDays: function (aDate) {
michael@0 5824 return Math.floor(aDate.getTime() / this.MILLISECONDS_PER_DAY);
michael@0 5825 },
michael@0 5826
michael@0 5827 jsonForAddon: function (aAddon) {
michael@0 5828 let o = {};
michael@0 5829 if (aAddon.installDate) {
michael@0 5830 o.installDay = this._dateToDays(aAddon.installDate);
michael@0 5831 }
michael@0 5832 if (aAddon.updateDate) {
michael@0 5833 o.updateDay = this._dateToDays(aAddon.updateDate);
michael@0 5834 }
michael@0 5835
michael@0 5836 for (let field of this.COPY_FIELDS) {
michael@0 5837 o[field] = aAddon[field];
michael@0 5838 }
michael@0 5839
michael@0 5840 return o;
michael@0 5841 },
michael@0 5842
michael@0 5843 notifyJava: function (aAddon, aNeedsRestart, aAction="Addons:Change") {
michael@0 5844 let json = this.jsonForAddon(aAddon);
michael@0 5845 if (this._shouldIgnore(aAddon)) {
michael@0 5846 json.ignore = true;
michael@0 5847 }
michael@0 5848 sendMessageToJava({ type: aAction, id: aAddon.id, json: json });
michael@0 5849 },
michael@0 5850
michael@0 5851 // Add-on listeners.
michael@0 5852 onEnabling: function (aAddon, aNeedsRestart) {
michael@0 5853 this.notifyJava(aAddon, aNeedsRestart);
michael@0 5854 },
michael@0 5855 onDisabling: function (aAddon, aNeedsRestart) {
michael@0 5856 this.notifyJava(aAddon, aNeedsRestart);
michael@0 5857 },
michael@0 5858 onInstalling: function (aAddon, aNeedsRestart) {
michael@0 5859 this.notifyJava(aAddon, aNeedsRestart);
michael@0 5860 },
michael@0 5861 onUninstalling: function (aAddon, aNeedsRestart) {
michael@0 5862 this.notifyJava(aAddon, aNeedsRestart, "Addons:Uninstalling");
michael@0 5863 },
michael@0 5864 onPropertyChanged: function (aAddon, aProperties) {
michael@0 5865 this.notifyJava(aAddon);
michael@0 5866 },
michael@0 5867 onOperationCancelled: function (aAddon) {
michael@0 5868 this.notifyJava(aAddon);
michael@0 5869 },
michael@0 5870
michael@0 5871 sendSnapshotToJava: function () {
michael@0 5872 AddonManager.getAllAddons(function (aAddons) {
michael@0 5873 let jsonA = {};
michael@0 5874 if (aAddons) {
michael@0 5875 for (let i = 0; i < aAddons.length; ++i) {
michael@0 5876 let addon = aAddons[i];
michael@0 5877 try {
michael@0 5878 let addonJSON = HealthReportStatusListener.jsonForAddon(addon);
michael@0 5879 if (HealthReportStatusListener._shouldIgnore(addon)) {
michael@0 5880 addonJSON.ignore = true;
michael@0 5881 }
michael@0 5882 jsonA[addon.id] = addonJSON;
michael@0 5883 } catch (e) {
michael@0 5884 // Just skip this add-on.
michael@0 5885 }
michael@0 5886 }
michael@0 5887 }
michael@0 5888
michael@0 5889 // Now add prefs.
michael@0 5890 let jsonP = {};
michael@0 5891 for (let pref of [this.PREF_BLOCKLIST_ENABLED, this.PREF_TELEMETRY_ENABLED]) {
michael@0 5892 if (!pref) {
michael@0 5893 // This will be the case for PREF_TELEMETRY_ENABLED in developer builds.
michael@0 5894 continue;
michael@0 5895 }
michael@0 5896 jsonP[pref] = {
michael@0 5897 pref: pref,
michael@0 5898 value: Services.prefs.getBoolPref(pref),
michael@0 5899 isUserSet: Services.prefs.prefHasUserValue(pref),
michael@0 5900 };
michael@0 5901 }
michael@0 5902 for (let pref of [this.PREF_ACCEPT_LANG]) {
michael@0 5903 jsonP[pref] = {
michael@0 5904 pref: pref,
michael@0 5905 value: Services.prefs.getCharPref(pref),
michael@0 5906 isUserSet: Services.prefs.prefHasUserValue(pref),
michael@0 5907 };
michael@0 5908 }
michael@0 5909
michael@0 5910 console.log("Sending snapshot message.");
michael@0 5911 sendMessageToJava({
michael@0 5912 type: "HealthReport:Snapshot",
michael@0 5913 json: {
michael@0 5914 addons: jsonA,
michael@0 5915 prefs: jsonP,
michael@0 5916 },
michael@0 5917 });
michael@0 5918 }.bind(this));
michael@0 5919 },
michael@0 5920 };
michael@0 5921
michael@0 5922 var XPInstallObserver = {
michael@0 5923 init: function xpi_init() {
michael@0 5924 Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false);
michael@0 5925 Services.obs.addObserver(XPInstallObserver, "addon-install-started", false);
michael@0 5926
michael@0 5927 AddonManager.addInstallListener(XPInstallObserver);
michael@0 5928 },
michael@0 5929
michael@0 5930 uninit: function xpi_uninit() {
michael@0 5931 Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
michael@0 5932 Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
michael@0 5933
michael@0 5934 AddonManager.removeInstallListener(XPInstallObserver);
michael@0 5935 },
michael@0 5936
michael@0 5937 observe: function xpi_observer(aSubject, aTopic, aData) {
michael@0 5938 switch (aTopic) {
michael@0 5939 case "addon-install-started":
michael@0 5940 NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsDownloading"), "short");
michael@0 5941 break;
michael@0 5942 case "addon-install-blocked":
michael@0 5943 let installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo);
michael@0 5944 let win = installInfo.originatingWindow;
michael@0 5945 let tab = BrowserApp.getTabForWindow(win.top);
michael@0 5946 if (!tab)
michael@0 5947 return;
michael@0 5948
michael@0 5949 let host = null;
michael@0 5950 if (installInfo.originatingURI) {
michael@0 5951 host = installInfo.originatingURI.host;
michael@0 5952 }
michael@0 5953
michael@0 5954 let brandShortName = Strings.brand.GetStringFromName("brandShortName");
michael@0 5955 let notificationName, buttons, message;
michael@0 5956 let strings = Strings.browser;
michael@0 5957 let enabled = true;
michael@0 5958 try {
michael@0 5959 enabled = Services.prefs.getBoolPref("xpinstall.enabled");
michael@0 5960 }
michael@0 5961 catch (e) {}
michael@0 5962
michael@0 5963 if (!enabled) {
michael@0 5964 notificationName = "xpinstall-disabled";
michael@0 5965 if (Services.prefs.prefIsLocked("xpinstall.enabled")) {
michael@0 5966 message = strings.GetStringFromName("xpinstallDisabledMessageLocked");
michael@0 5967 buttons = [];
michael@0 5968 } else {
michael@0 5969 message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2);
michael@0 5970 buttons = [{
michael@0 5971 label: strings.GetStringFromName("xpinstallDisabledButton"),
michael@0 5972 callback: function editPrefs() {
michael@0 5973 Services.prefs.setBoolPref("xpinstall.enabled", true);
michael@0 5974 return false;
michael@0 5975 }
michael@0 5976 }];
michael@0 5977 }
michael@0 5978 } else {
michael@0 5979 notificationName = "xpinstall";
michael@0 5980 if (host) {
michael@0 5981 // We have a host which asked for the install.
michael@0 5982 message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2);
michael@0 5983 } else {
michael@0 5984 // Without a host we address the add-on as the initiator of the install.
michael@0 5985 let addon = null;
michael@0 5986 if (installInfo.installs.length > 0) {
michael@0 5987 addon = installInfo.installs[0].name;
michael@0 5988 }
michael@0 5989 if (addon) {
michael@0 5990 // We have an addon name, show the regular message.
michael@0 5991 message = strings.formatStringFromName("xpinstallPromptWarningLocal", [brandShortName, addon], 2);
michael@0 5992 } else {
michael@0 5993 // We don't have an addon name, show an alternative message.
michael@0 5994 message = strings.formatStringFromName("xpinstallPromptWarningDirect", [brandShortName], 1);
michael@0 5995 }
michael@0 5996 }
michael@0 5997
michael@0 5998 buttons = [{
michael@0 5999 label: strings.GetStringFromName("xpinstallPromptAllowButton"),
michael@0 6000 callback: function() {
michael@0 6001 // Kick off the install
michael@0 6002 installInfo.install();
michael@0 6003 return false;
michael@0 6004 }
michael@0 6005 }];
michael@0 6006 }
michael@0 6007 NativeWindow.doorhanger.show(message, aTopic, buttons, tab.id);
michael@0 6008 break;
michael@0 6009 }
michael@0 6010 },
michael@0 6011
michael@0 6012 onInstallEnded: function(aInstall, aAddon) {
michael@0 6013 let needsRestart = false;
michael@0 6014 if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
michael@0 6015 needsRestart = true;
michael@0 6016 else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
michael@0 6017 needsRestart = true;
michael@0 6018
michael@0 6019 if (needsRestart) {
michael@0 6020 this.showRestartPrompt();
michael@0 6021 } else {
michael@0 6022 // Display completion message for new installs or updates not done Automatically
michael@0 6023 if (!aInstall.existingAddon || !AddonManager.shouldAutoUpdate(aInstall.existingAddon)) {
michael@0 6024 let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart");
michael@0 6025 NativeWindow.toast.show(message, "short");
michael@0 6026 }
michael@0 6027 }
michael@0 6028 },
michael@0 6029
michael@0 6030 onInstallFailed: function(aInstall) {
michael@0 6031 NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsFail"), "short");
michael@0 6032 },
michael@0 6033
michael@0 6034 onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {},
michael@0 6035
michael@0 6036 onDownloadFailed: function(aInstall) {
michael@0 6037 this.onInstallFailed(aInstall);
michael@0 6038 },
michael@0 6039
michael@0 6040 onDownloadCancelled: function(aInstall) {
michael@0 6041 let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host;
michael@0 6042 if (!host)
michael@0 6043 host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host;
michael@0 6044
michael@0 6045 let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError";
michael@0 6046 if (aInstall.error != 0)
michael@0 6047 error += aInstall.error;
michael@0 6048 else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
michael@0 6049 error += "Blocklisted";
michael@0 6050 else if (aInstall.addon && (!aInstall.addon.isCompatible || !aInstall.addon.isPlatformCompatible))
michael@0 6051 error += "Incompatible";
michael@0 6052 else
michael@0 6053 return; // No need to show anything in this case.
michael@0 6054
michael@0 6055 let msg = Strings.browser.GetStringFromName(error);
michael@0 6056 // TODO: formatStringFromName
michael@0 6057 msg = msg.replace("#1", aInstall.name);
michael@0 6058 if (host)
michael@0 6059 msg = msg.replace("#2", host);
michael@0 6060 msg = msg.replace("#3", Strings.brand.GetStringFromName("brandShortName"));
michael@0 6061 msg = msg.replace("#4", Services.appinfo.version);
michael@0 6062
michael@0 6063 NativeWindow.toast.show(msg, "short");
michael@0 6064 },
michael@0 6065
michael@0 6066 showRestartPrompt: function() {
michael@0 6067 let buttons = [{
michael@0 6068 label: Strings.browser.GetStringFromName("notificationRestart.button"),
michael@0 6069 callback: function() {
michael@0 6070 // Notify all windows that an application quit has been requested
michael@0 6071 let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
michael@0 6072 Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
michael@0 6073
michael@0 6074 // If nothing aborted, quit the app
michael@0 6075 if (cancelQuit.data == false) {
michael@0 6076 let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
michael@0 6077 appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
michael@0 6078 }
michael@0 6079 }
michael@0 6080 }];
michael@0 6081
michael@0 6082 let message = Strings.browser.GetStringFromName("notificationRestart.normal");
michael@0 6083 NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 });
michael@0 6084 },
michael@0 6085
michael@0 6086 hideRestartPrompt: function() {
michael@0 6087 NativeWindow.doorhanger.hide("addon-app-restart", BrowserApp.selectedTab.id);
michael@0 6088 }
michael@0 6089 };
michael@0 6090
michael@0 6091 // Blindly copied from Safari documentation for now.
michael@0 6092 const kViewportMinScale = 0;
michael@0 6093 const kViewportMaxScale = 10;
michael@0 6094 const kViewportMinWidth = 200;
michael@0 6095 const kViewportMaxWidth = 10000;
michael@0 6096 const kViewportMinHeight = 223;
michael@0 6097 const kViewportMaxHeight = 10000;
michael@0 6098
michael@0 6099 var ViewportHandler = {
michael@0 6100 // The cached viewport metadata for each document. We tie viewport metadata to each document
michael@0 6101 // instead of to each tab so that we don't have to update it when the document changes. Using an
michael@0 6102 // ES6 weak map lets us avoid leaks.
michael@0 6103 _metadata: new WeakMap(),
michael@0 6104
michael@0 6105 init: function init() {
michael@0 6106 addEventListener("DOMMetaAdded", this, false);
michael@0 6107 Services.obs.addObserver(this, "Window:Resize", false);
michael@0 6108 },
michael@0 6109
michael@0 6110 uninit: function uninit() {
michael@0 6111 removeEventListener("DOMMetaAdded", this, false);
michael@0 6112 Services.obs.removeObserver(this, "Window:Resize");
michael@0 6113 },
michael@0 6114
michael@0 6115 handleEvent: function handleEvent(aEvent) {
michael@0 6116 switch (aEvent.type) {
michael@0 6117 case "DOMMetaAdded":
michael@0 6118 let target = aEvent.originalTarget;
michael@0 6119 if (target.name != "viewport")
michael@0 6120 break;
michael@0 6121 let document = target.ownerDocument;
michael@0 6122 let browser = BrowserApp.getBrowserForDocument(document);
michael@0 6123 let tab = BrowserApp.getTabForBrowser(browser);
michael@0 6124 if (tab)
michael@0 6125 this.updateMetadata(tab, false);
michael@0 6126 break;
michael@0 6127 }
michael@0 6128 },
michael@0 6129
michael@0 6130 observe: function(aSubject, aTopic, aData) {
michael@0 6131 switch (aTopic) {
michael@0 6132 case "Window:Resize":
michael@0 6133 if (window.outerWidth == gScreenWidth && window.outerHeight == gScreenHeight)
michael@0 6134 break;
michael@0 6135 if (window.outerWidth == 0 || window.outerHeight == 0)
michael@0 6136 break;
michael@0 6137
michael@0 6138 let oldScreenWidth = gScreenWidth;
michael@0 6139 gScreenWidth = window.outerWidth * window.devicePixelRatio;
michael@0 6140 gScreenHeight = window.outerHeight * window.devicePixelRatio;
michael@0 6141 let tabs = BrowserApp.tabs;
michael@0 6142 for (let i = 0; i < tabs.length; i++)
michael@0 6143 tabs[i].updateViewportSize(oldScreenWidth);
michael@0 6144 break;
michael@0 6145 }
michael@0 6146 },
michael@0 6147
michael@0 6148 updateMetadata: function updateMetadata(tab, aInitialLoad) {
michael@0 6149 let contentWindow = tab.browser.contentWindow;
michael@0 6150 if (contentWindow.document.documentElement) {
michael@0 6151 let metadata = this.getViewportMetadata(contentWindow);
michael@0 6152 tab.updateViewportMetadata(metadata, aInitialLoad);
michael@0 6153 }
michael@0 6154 },
michael@0 6155
michael@0 6156 /**
michael@0 6157 * Returns the ViewportMetadata object.
michael@0 6158 */
michael@0 6159 getViewportMetadata: function getViewportMetadata(aWindow) {
michael@0 6160 let windowUtils = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 6161
michael@0 6162 // viewport details found here
michael@0 6163 // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
michael@0 6164 // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html
michael@0 6165
michael@0 6166 // Note: These values will be NaN if parseFloat or parseInt doesn't find a number.
michael@0 6167 // Remember that NaN is contagious: Math.max(1, NaN) == Math.min(1, NaN) == NaN.
michael@0 6168 let hasMetaViewport = true;
michael@0 6169 let scale = parseFloat(windowUtils.getDocumentMetadata("viewport-initial-scale"));
michael@0 6170 let minScale = parseFloat(windowUtils.getDocumentMetadata("viewport-minimum-scale"));
michael@0 6171 let maxScale = parseFloat(windowUtils.getDocumentMetadata("viewport-maximum-scale"));
michael@0 6172
michael@0 6173 let widthStr = windowUtils.getDocumentMetadata("viewport-width");
michael@0 6174 let heightStr = windowUtils.getDocumentMetadata("viewport-height");
michael@0 6175 let width = this.clamp(parseInt(widthStr), kViewportMinWidth, kViewportMaxWidth) || 0;
michael@0 6176 let height = this.clamp(parseInt(heightStr), kViewportMinHeight, kViewportMaxHeight) || 0;
michael@0 6177
michael@0 6178 // Allow zoom unless explicity disabled or minScale and maxScale are equal.
michael@0 6179 // WebKit allows 0, "no", or "false" for viewport-user-scalable.
michael@0 6180 // Note: NaN != NaN. Therefore if minScale and maxScale are undefined the clause has no effect.
michael@0 6181 let allowZoomStr = windowUtils.getDocumentMetadata("viewport-user-scalable");
michael@0 6182 let allowZoom = !/^(0|no|false)$/.test(allowZoomStr) && (minScale != maxScale);
michael@0 6183
michael@0 6184 // Double-tap should always be disabled if allowZoom is disabled. So we initialize
michael@0 6185 // allowDoubleTapZoom to the same value as allowZoom and have additional conditions to
michael@0 6186 // disable it in updateViewportSize.
michael@0 6187 let allowDoubleTapZoom = allowZoom;
michael@0 6188
michael@0 6189 let autoSize = true;
michael@0 6190
michael@0 6191 if (isNaN(scale) && isNaN(minScale) && isNaN(maxScale) && allowZoomStr == "" && widthStr == "" && heightStr == "") {
michael@0 6192 // Only check for HandheldFriendly if we don't have a viewport meta tag
michael@0 6193 let handheldFriendly = windowUtils.getDocumentMetadata("HandheldFriendly");
michael@0 6194 if (handheldFriendly == "true") {
michael@0 6195 return new ViewportMetadata({
michael@0 6196 defaultZoom: 1,
michael@0 6197 autoSize: true,
michael@0 6198 allowZoom: true,
michael@0 6199 allowDoubleTapZoom: false
michael@0 6200 });
michael@0 6201 }
michael@0 6202
michael@0 6203 let doctype = aWindow.document.doctype;
michael@0 6204 if (doctype && /(WAP|WML|Mobile)/.test(doctype.publicId)) {
michael@0 6205 return new ViewportMetadata({
michael@0 6206 defaultZoom: 1,
michael@0 6207 autoSize: true,
michael@0 6208 allowZoom: true,
michael@0 6209 allowDoubleTapZoom: false
michael@0 6210 });
michael@0 6211 }
michael@0 6212
michael@0 6213 hasMetaViewport = false;
michael@0 6214 let defaultZoom = Services.prefs.getIntPref("browser.viewport.defaultZoom");
michael@0 6215 if (defaultZoom >= 0) {
michael@0 6216 scale = defaultZoom / 1000;
michael@0 6217 autoSize = false;
michael@0 6218 }
michael@0 6219 }
michael@0 6220
michael@0 6221 scale = this.clamp(scale, kViewportMinScale, kViewportMaxScale);
michael@0 6222 minScale = this.clamp(minScale, kViewportMinScale, kViewportMaxScale);
michael@0 6223 maxScale = this.clamp(maxScale, minScale, kViewportMaxScale);
michael@0 6224
michael@0 6225 if (autoSize) {
michael@0 6226 // If initial scale is 1.0 and width is not set, assume width=device-width
michael@0 6227 autoSize = (widthStr == "device-width" ||
michael@0 6228 (!widthStr && (heightStr == "device-height" || scale == 1.0)));
michael@0 6229 }
michael@0 6230
michael@0 6231 let isRTL = aWindow.document.documentElement.dir == "rtl";
michael@0 6232
michael@0 6233 return new ViewportMetadata({
michael@0 6234 defaultZoom: scale,
michael@0 6235 minZoom: minScale,
michael@0 6236 maxZoom: maxScale,
michael@0 6237 width: width,
michael@0 6238 height: height,
michael@0 6239 autoSize: autoSize,
michael@0 6240 allowZoom: allowZoom,
michael@0 6241 allowDoubleTapZoom: allowDoubleTapZoom,
michael@0 6242 isSpecified: hasMetaViewport,
michael@0 6243 isRTL: isRTL
michael@0 6244 });
michael@0 6245 },
michael@0 6246
michael@0 6247 clamp: function(num, min, max) {
michael@0 6248 return Math.max(min, Math.min(max, num));
michael@0 6249 },
michael@0 6250
michael@0 6251 get displayDPI() {
michael@0 6252 let utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
michael@0 6253 delete this.displayDPI;
michael@0 6254 return this.displayDPI = utils.displayDPI;
michael@0 6255 },
michael@0 6256
michael@0 6257 /**
michael@0 6258 * Returns the viewport metadata for the given document, or the default metrics if no viewport
michael@0 6259 * metadata is available for that document.
michael@0 6260 */
michael@0 6261 getMetadataForDocument: function getMetadataForDocument(aDocument) {
michael@0 6262 let metadata = this._metadata.get(aDocument, new ViewportMetadata());
michael@0 6263 return metadata;
michael@0 6264 },
michael@0 6265
michael@0 6266 /** Updates the saved viewport metadata for the given content document. */
michael@0 6267 setMetadataForDocument: function setMetadataForDocument(aDocument, aMetadata) {
michael@0 6268 if (!aMetadata)
michael@0 6269 this._metadata.delete(aDocument);
michael@0 6270 else
michael@0 6271 this._metadata.set(aDocument, aMetadata);
michael@0 6272 }
michael@0 6273
michael@0 6274 };
michael@0 6275
michael@0 6276 /**
michael@0 6277 * An object which represents the page's preferred viewport properties:
michael@0 6278 * width (int): The CSS viewport width in px.
michael@0 6279 * height (int): The CSS viewport height in px.
michael@0 6280 * defaultZoom (float): The initial scale when the page is loaded.
michael@0 6281 * minZoom (float): The minimum zoom level.
michael@0 6282 * maxZoom (float): The maximum zoom level.
michael@0 6283 * autoSize (boolean): Resize the CSS viewport when the window resizes.
michael@0 6284 * allowZoom (boolean): Let the user zoom in or out.
michael@0 6285 * allowDoubleTapZoom (boolean): Allow double-tap to zoom in.
michael@0 6286 * isSpecified (boolean): Whether the page viewport is specified or not.
michael@0 6287 */
michael@0 6288 function ViewportMetadata(aMetadata = {}) {
michael@0 6289 this.width = ("width" in aMetadata) ? aMetadata.width : 0;
michael@0 6290 this.height = ("height" in aMetadata) ? aMetadata.height : 0;
michael@0 6291 this.defaultZoom = ("defaultZoom" in aMetadata) ? aMetadata.defaultZoom : 0;
michael@0 6292 this.minZoom = ("minZoom" in aMetadata) ? aMetadata.minZoom : 0;
michael@0 6293 this.maxZoom = ("maxZoom" in aMetadata) ? aMetadata.maxZoom : 0;
michael@0 6294 this.autoSize = ("autoSize" in aMetadata) ? aMetadata.autoSize : false;
michael@0 6295 this.allowZoom = ("allowZoom" in aMetadata) ? aMetadata.allowZoom : true;
michael@0 6296 this.allowDoubleTapZoom = ("allowDoubleTapZoom" in aMetadata) ? aMetadata.allowDoubleTapZoom : true;
michael@0 6297 this.isSpecified = ("isSpecified" in aMetadata) ? aMetadata.isSpecified : false;
michael@0 6298 this.isRTL = ("isRTL" in aMetadata) ? aMetadata.isRTL : false;
michael@0 6299 Object.seal(this);
michael@0 6300 }
michael@0 6301
michael@0 6302 ViewportMetadata.prototype = {
michael@0 6303 width: null,
michael@0 6304 height: null,
michael@0 6305 defaultZoom: null,
michael@0 6306 minZoom: null,
michael@0 6307 maxZoom: null,
michael@0 6308 autoSize: null,
michael@0 6309 allowZoom: null,
michael@0 6310 allowDoubleTapZoom: null,
michael@0 6311 isSpecified: null,
michael@0 6312 isRTL: null,
michael@0 6313
michael@0 6314 toString: function() {
michael@0 6315 return "width=" + this.width
michael@0 6316 + "; height=" + this.height
michael@0 6317 + "; defaultZoom=" + this.defaultZoom
michael@0 6318 + "; minZoom=" + this.minZoom
michael@0 6319 + "; maxZoom=" + this.maxZoom
michael@0 6320 + "; autoSize=" + this.autoSize
michael@0 6321 + "; allowZoom=" + this.allowZoom
michael@0 6322 + "; allowDoubleTapZoom=" + this.allowDoubleTapZoom
michael@0 6323 + "; isSpecified=" + this.isSpecified
michael@0 6324 + "; isRTL=" + this.isRTL;
michael@0 6325 }
michael@0 6326 };
michael@0 6327
michael@0 6328
michael@0 6329 /**
michael@0 6330 * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml
michael@0 6331 */
michael@0 6332 var PopupBlockerObserver = {
michael@0 6333 onUpdatePageReport: function onUpdatePageReport(aEvent) {
michael@0 6334 let browser = BrowserApp.selectedBrowser;
michael@0 6335 if (aEvent.originalTarget != browser)
michael@0 6336 return;
michael@0 6337
michael@0 6338 if (!browser.pageReport)
michael@0 6339 return;
michael@0 6340
michael@0 6341 let result = Services.perms.testExactPermission(BrowserApp.selectedBrowser.currentURI, "popup");
michael@0 6342 if (result == Ci.nsIPermissionManager.DENY_ACTION)
michael@0 6343 return;
michael@0 6344
michael@0 6345 // Only show the notification again if we've not already shown it. Since
michael@0 6346 // notifications are per-browser, we don't need to worry about re-adding
michael@0 6347 // it.
michael@0 6348 if (!browser.pageReport.reported) {
michael@0 6349 if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) {
michael@0 6350 let brandShortName = Strings.brand.GetStringFromName("brandShortName");
michael@0 6351 let popupCount = browser.pageReport.length;
michael@0 6352
michael@0 6353 let strings = Strings.browser;
michael@0 6354 let message = PluralForm.get(popupCount, strings.GetStringFromName("popup.message"))
michael@0 6355 .replace("#1", brandShortName)
michael@0 6356 .replace("#2", popupCount);
michael@0 6357
michael@0 6358 let buttons = [
michael@0 6359 {
michael@0 6360 label: strings.GetStringFromName("popup.show"),
michael@0 6361 callback: function(aChecked) {
michael@0 6362 // Set permission before opening popup windows
michael@0 6363 if (aChecked)
michael@0 6364 PopupBlockerObserver.allowPopupsForSite(true);
michael@0 6365
michael@0 6366 PopupBlockerObserver.showPopupsForSite();
michael@0 6367 }
michael@0 6368 },
michael@0 6369 {
michael@0 6370 label: strings.GetStringFromName("popup.dontShow"),
michael@0 6371 callback: function(aChecked) {
michael@0 6372 if (aChecked)
michael@0 6373 PopupBlockerObserver.allowPopupsForSite(false);
michael@0 6374 }
michael@0 6375 }
michael@0 6376 ];
michael@0 6377
michael@0 6378 let options = { checkbox: Strings.browser.GetStringFromName("popup.dontAskAgain") };
michael@0 6379 NativeWindow.doorhanger.show(message, "popup-blocked", buttons, null, options);
michael@0 6380 }
michael@0 6381 // Record the fact that we've reported this blocked popup, so we don't
michael@0 6382 // show it again.
michael@0 6383 browser.pageReport.reported = true;
michael@0 6384 }
michael@0 6385 },
michael@0 6386
michael@0 6387 allowPopupsForSite: function allowPopupsForSite(aAllow) {
michael@0 6388 let currentURI = BrowserApp.selectedBrowser.currentURI;
michael@0 6389 Services.perms.add(currentURI, "popup", aAllow
michael@0 6390 ? Ci.nsIPermissionManager.ALLOW_ACTION
michael@0 6391 : Ci.nsIPermissionManager.DENY_ACTION);
michael@0 6392 dump("Allowing popups for: " + currentURI);
michael@0 6393 },
michael@0 6394
michael@0 6395 showPopupsForSite: function showPopupsForSite() {
michael@0 6396 let uri = BrowserApp.selectedBrowser.currentURI;
michael@0 6397 let pageReport = BrowserApp.selectedBrowser.pageReport;
michael@0 6398 if (pageReport) {
michael@0 6399 for (let i = 0; i < pageReport.length; ++i) {
michael@0 6400 let popupURIspec = pageReport[i].popupWindowURI.spec;
michael@0 6401
michael@0 6402 // Sometimes the popup URI that we get back from the pageReport
michael@0 6403 // isn't useful (for instance, netscape.com's popup URI ends up
michael@0 6404 // being "http://www.netscape.com", which isn't really the URI of
michael@0 6405 // the popup they're trying to show). This isn't going to be
michael@0 6406 // useful to the user, so we won't create a menu item for it.
michael@0 6407 if (popupURIspec == "" || popupURIspec == "about:blank" || popupURIspec == uri.spec)
michael@0 6408 continue;
michael@0 6409
michael@0 6410 let popupFeatures = pageReport[i].popupWindowFeatures;
michael@0 6411 let popupName = pageReport[i].popupWindowName;
michael@0 6412
michael@0 6413 let parent = BrowserApp.selectedTab;
michael@0 6414 let isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow);
michael@0 6415 BrowserApp.addTab(popupURIspec, { parentId: parent.id, isPrivate: isPrivate });
michael@0 6416 }
michael@0 6417 }
michael@0 6418 }
michael@0 6419 };
michael@0 6420
michael@0 6421
michael@0 6422 var IndexedDB = {
michael@0 6423 _permissionsPrompt: "indexedDB-permissions-prompt",
michael@0 6424 _permissionsResponse: "indexedDB-permissions-response",
michael@0 6425
michael@0 6426 _quotaPrompt: "indexedDB-quota-prompt",
michael@0 6427 _quotaResponse: "indexedDB-quota-response",
michael@0 6428 _quotaCancel: "indexedDB-quota-cancel",
michael@0 6429
michael@0 6430 init: function IndexedDB_init() {
michael@0 6431 Services.obs.addObserver(this, this._permissionsPrompt, false);
michael@0 6432 Services.obs.addObserver(this, this._quotaPrompt, false);
michael@0 6433 Services.obs.addObserver(this, this._quotaCancel, false);
michael@0 6434 },
michael@0 6435
michael@0 6436 uninit: function IndexedDB_uninit() {
michael@0 6437 Services.obs.removeObserver(this, this._permissionsPrompt);
michael@0 6438 Services.obs.removeObserver(this, this._quotaPrompt);
michael@0 6439 Services.obs.removeObserver(this, this._quotaCancel);
michael@0 6440 },
michael@0 6441
michael@0 6442 observe: function IndexedDB_observe(subject, topic, data) {
michael@0 6443 if (topic != this._permissionsPrompt &&
michael@0 6444 topic != this._quotaPrompt &&
michael@0 6445 topic != this._quotaCancel) {
michael@0 6446 throw new Error("Unexpected topic!");
michael@0 6447 }
michael@0 6448
michael@0 6449 let requestor = subject.QueryInterface(Ci.nsIInterfaceRequestor);
michael@0 6450
michael@0 6451 let contentWindow = requestor.getInterface(Ci.nsIDOMWindow);
michael@0 6452 let contentDocument = contentWindow.document;
michael@0 6453 let tab = BrowserApp.getTabForWindow(contentWindow);
michael@0 6454 if (!tab)
michael@0 6455 return;
michael@0 6456
michael@0 6457 let host = contentDocument.documentURIObject.asciiHost;
michael@0 6458
michael@0 6459 let strings = Strings.browser;
michael@0 6460
michael@0 6461 let message, responseTopic;
michael@0 6462 if (topic == this._permissionsPrompt) {
michael@0 6463 message = strings.formatStringFromName("offlineApps.ask", [host], 1);
michael@0 6464 responseTopic = this._permissionsResponse;
michael@0 6465 } else if (topic == this._quotaPrompt) {
michael@0 6466 message = strings.formatStringFromName("indexedDBQuota.wantsTo", [ host, data ], 2);
michael@0 6467 responseTopic = this._quotaResponse;
michael@0 6468 } else if (topic == this._quotaCancel) {
michael@0 6469 responseTopic = this._quotaResponse;
michael@0 6470 }
michael@0 6471
michael@0 6472 const firstTimeoutDuration = 300000; // 5 minutes
michael@0 6473
michael@0 6474 let timeoutId;
michael@0 6475
michael@0 6476 let notificationID = responseTopic + host;
michael@0 6477 let observer = requestor.getInterface(Ci.nsIObserver);
michael@0 6478
michael@0 6479 // This will be set to the result of PopupNotifications.show() below, or to
michael@0 6480 // the result of PopupNotifications.getNotification() if this is a
michael@0 6481 // quotaCancel notification.
michael@0 6482 let notification;
michael@0 6483
michael@0 6484 function timeoutNotification() {
michael@0 6485 // Remove the notification.
michael@0 6486 NativeWindow.doorhanger.hide(notificationID, tab.id);
michael@0 6487
michael@0 6488 // Clear all of our timeout stuff. We may be called directly, not just
michael@0 6489 // when the timeout actually elapses.
michael@0 6490 clearTimeout(timeoutId);
michael@0 6491
michael@0 6492 // And tell the page that the popup timed out.
michael@0 6493 observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
michael@0 6494 }
michael@0 6495
michael@0 6496 if (topic == this._quotaCancel) {
michael@0 6497 NativeWindow.doorhanger.hide(notificationID, tab.id);
michael@0 6498 timeoutNotification();
michael@0 6499 observer.observe(null, responseTopic, Ci.nsIPermissionManager.UNKNOWN_ACTION);
michael@0 6500 return;
michael@0 6501 }
michael@0 6502
michael@0 6503 let buttons = [{
michael@0 6504 label: strings.GetStringFromName("offlineApps.allow"),
michael@0 6505 callback: function() {
michael@0 6506 clearTimeout(timeoutId);
michael@0 6507 observer.observe(null, responseTopic, Ci.nsIPermissionManager.ALLOW_ACTION);
michael@0 6508 }
michael@0 6509 },
michael@0 6510 {
michael@0 6511 label: strings.GetStringFromName("offlineApps.dontAllow2"),
michael@0 6512 callback: function(aChecked) {
michael@0 6513 clearTimeout(timeoutId);
michael@0 6514 let action = aChecked ? Ci.nsIPermissionManager.DENY_ACTION : Ci.nsIPermissionManager.UNKNOWN_ACTION;
michael@0 6515 observer.observe(null, responseTopic, action);
michael@0 6516 }
michael@0 6517 }];
michael@0 6518
michael@0 6519 let options = { checkbox: Strings.browser.GetStringFromName("offlineApps.dontAskAgain") };
michael@0 6520 NativeWindow.doorhanger.show(message, notificationID, buttons, tab.id, options);
michael@0 6521
michael@0 6522 // Set the timeoutId after the popup has been created, and use the long
michael@0 6523 // timeout value. If the user doesn't notice the popup after this amount of
michael@0 6524 // time then it is most likely not visible and we want to alert the page.
michael@0 6525 timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
michael@0 6526 }
michael@0 6527 };
michael@0 6528
michael@0 6529 var CharacterEncoding = {
michael@0 6530 _charsets: [],
michael@0 6531
michael@0 6532 init: function init() {
michael@0 6533 Services.obs.addObserver(this, "CharEncoding:Get", false);
michael@0 6534 Services.obs.addObserver(this, "CharEncoding:Set", false);
michael@0 6535 this.sendState();
michael@0 6536 },
michael@0 6537
michael@0 6538 uninit: function uninit() {
michael@0 6539 Services.obs.removeObserver(this, "CharEncoding:Get");
michael@0 6540 Services.obs.removeObserver(this, "CharEncoding:Set");
michael@0 6541 },
michael@0 6542
michael@0 6543 observe: function observe(aSubject, aTopic, aData) {
michael@0 6544 switch (aTopic) {
michael@0 6545 case "CharEncoding:Get":
michael@0 6546 this.getEncoding();
michael@0 6547 break;
michael@0 6548 case "CharEncoding:Set":
michael@0 6549 this.setEncoding(aData);
michael@0 6550 break;
michael@0 6551 }
michael@0 6552 },
michael@0 6553
michael@0 6554 sendState: function sendState() {
michael@0 6555 let showCharEncoding = "false";
michael@0 6556 try {
michael@0 6557 showCharEncoding = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data;
michael@0 6558 } catch (e) { /* Optional */ }
michael@0 6559
michael@0 6560 sendMessageToJava({
michael@0 6561 type: "CharEncoding:State",
michael@0 6562 visible: showCharEncoding
michael@0 6563 });
michael@0 6564 },
michael@0 6565
michael@0 6566 getEncoding: function getEncoding() {
michael@0 6567 function infoToCharset(info) {
michael@0 6568 return { code: info.value, title: info.label };
michael@0 6569 }
michael@0 6570
michael@0 6571 if (!this._charsets.length) {
michael@0 6572 let data = CharsetMenu.getData();
michael@0 6573
michael@0 6574 // In the desktop UI, the pinned charsets are shown above the rest.
michael@0 6575 let pinnedCharsets = data.pinnedCharsets.map(infoToCharset);
michael@0 6576 let otherCharsets = data.otherCharsets.map(infoToCharset)
michael@0 6577
michael@0 6578 this._charsets = pinnedCharsets.concat(otherCharsets);
michael@0 6579 }
michael@0 6580
michael@0 6581 // Look for the index of the selected charset. Default to -1 if the
michael@0 6582 // doc charset isn't found in the list of available charsets.
michael@0 6583 let docCharset = BrowserApp.selectedBrowser.contentDocument.characterSet;
michael@0 6584 let selected = -1;
michael@0 6585 let charsetCount = this._charsets.length;
michael@0 6586
michael@0 6587 for (let i = 0; i < charsetCount; i++) {
michael@0 6588 if (this._charsets[i].code === docCharset) {
michael@0 6589 selected = i;
michael@0 6590 break;
michael@0 6591 }
michael@0 6592 }
michael@0 6593
michael@0 6594 sendMessageToJava({
michael@0 6595 type: "CharEncoding:Data",
michael@0 6596 charsets: this._charsets,
michael@0 6597 selected: selected
michael@0 6598 });
michael@0 6599 },
michael@0 6600
michael@0 6601 setEncoding: function setEncoding(aEncoding) {
michael@0 6602 let browser = BrowserApp.selectedBrowser;
michael@0 6603 browser.docShell.gatherCharsetMenuTelemetry();
michael@0 6604 browser.docShell.charset = aEncoding;
michael@0 6605 browser.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
michael@0 6606 }
michael@0 6607 };
michael@0 6608
michael@0 6609 var IdentityHandler = {
michael@0 6610 // No trusted identity information. No site identity icon is shown.
michael@0 6611 IDENTITY_MODE_UNKNOWN: "unknown",
michael@0 6612
michael@0 6613 // Minimal SSL CA-signed domain verification. Blue lock icon is shown.
michael@0 6614 IDENTITY_MODE_DOMAIN_VERIFIED: "verified",
michael@0 6615
michael@0 6616 // High-quality identity information. Green lock icon is shown.
michael@0 6617 IDENTITY_MODE_IDENTIFIED: "identified",
michael@0 6618
michael@0 6619 // The following mixed content modes are only used if "security.mixed_content.block_active_content"
michael@0 6620 // is enabled. Even though the mixed content state and identitity state are orthogonal,
michael@0 6621 // our Java frontend coalesces them into one indicator.
michael@0 6622
michael@0 6623 // Blocked active mixed content. Shield icon is shown, with a popup option to load content.
michael@0 6624 IDENTITY_MODE_MIXED_CONTENT_BLOCKED: "mixed_content_blocked",
michael@0 6625
michael@0 6626 // Loaded active mixed content. Yellow triangle icon is shown.
michael@0 6627 IDENTITY_MODE_MIXED_CONTENT_LOADED: "mixed_content_loaded",
michael@0 6628
michael@0 6629 // Cache the most recent SSLStatus and Location seen in getIdentityStrings
michael@0 6630 _lastStatus : null,
michael@0 6631 _lastLocation : null,
michael@0 6632
michael@0 6633 /**
michael@0 6634 * Helper to parse out the important parts of _lastStatus (of the SSL cert in
michael@0 6635 * particular) for use in constructing identity UI strings
michael@0 6636 */
michael@0 6637 getIdentityData : function() {
michael@0 6638 let result = {};
michael@0 6639 let status = this._lastStatus.QueryInterface(Components.interfaces.nsISSLStatus);
michael@0 6640 let cert = status.serverCert;
michael@0 6641
michael@0 6642 // Human readable name of Subject
michael@0 6643 result.subjectOrg = cert.organization;
michael@0 6644
michael@0 6645 // SubjectName fields, broken up for individual access
michael@0 6646 if (cert.subjectName) {
michael@0 6647 result.subjectNameFields = {};
michael@0 6648 cert.subjectName.split(",").forEach(function(v) {
michael@0 6649 let field = v.split("=");
michael@0 6650 this[field[0]] = field[1];
michael@0 6651 }, result.subjectNameFields);
michael@0 6652
michael@0 6653 // Call out city, state, and country specifically
michael@0 6654 result.city = result.subjectNameFields.L;
michael@0 6655 result.state = result.subjectNameFields.ST;
michael@0 6656 result.country = result.subjectNameFields.C;
michael@0 6657 }
michael@0 6658
michael@0 6659 // Human readable name of Certificate Authority
michael@0 6660 result.caOrg = cert.issuerOrganization || cert.issuerCommonName;
michael@0 6661 result.cert = cert;
michael@0 6662
michael@0 6663 return result;
michael@0 6664 },
michael@0 6665
michael@0 6666 /**
michael@0 6667 * Determines the identity mode corresponding to the icon we show in the urlbar.
michael@0 6668 */
michael@0 6669 getIdentityMode: function getIdentityMode(aState) {
michael@0 6670 if (aState & Ci.nsIWebProgressListener.STATE_BLOCKED_MIXED_ACTIVE_CONTENT)
michael@0 6671 return this.IDENTITY_MODE_MIXED_CONTENT_BLOCKED;
michael@0 6672
michael@0 6673 // Only show an indicator for loaded mixed content if the pref to block it is enabled
michael@0 6674 if ((aState & Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT) &&
michael@0 6675 Services.prefs.getBoolPref("security.mixed_content.block_active_content"))
michael@0 6676 return this.IDENTITY_MODE_MIXED_CONTENT_LOADED;
michael@0 6677
michael@0 6678 if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
michael@0 6679 return this.IDENTITY_MODE_IDENTIFIED;
michael@0 6680
michael@0 6681 if (aState & Ci.nsIWebProgressListener.STATE_IS_SECURE)
michael@0 6682 return this.IDENTITY_MODE_DOMAIN_VERIFIED;
michael@0 6683
michael@0 6684 return this.IDENTITY_MODE_UNKNOWN;
michael@0 6685 },
michael@0 6686
michael@0 6687 /**
michael@0 6688 * Determine the identity of the page being displayed by examining its SSL cert
michael@0 6689 * (if available). Return the data needed to update the UI.
michael@0 6690 */
michael@0 6691 checkIdentity: function checkIdentity(aState, aBrowser) {
michael@0 6692 this._lastStatus = aBrowser.securityUI
michael@0 6693 .QueryInterface(Components.interfaces.nsISSLStatusProvider)
michael@0 6694 .SSLStatus;
michael@0 6695
michael@0 6696 // Don't pass in the actual location object, since it can cause us to
michael@0 6697 // hold on to the window object too long. Just pass in the fields we
michael@0 6698 // care about. (bug 424829)
michael@0 6699 let locationObj = {};
michael@0 6700 try {
michael@0 6701 let location = aBrowser.contentWindow.location;
michael@0 6702 locationObj.host = location.host;
michael@0 6703 locationObj.hostname = location.hostname;
michael@0 6704 locationObj.port = location.port;
michael@0 6705 } catch (ex) {
michael@0 6706 // Can sometimes throw if the URL being visited has no host/hostname,
michael@0 6707 // e.g. about:blank. The _state for these pages means we won't need these
michael@0 6708 // properties anyways, though.
michael@0 6709 }
michael@0 6710 this._lastLocation = locationObj;
michael@0 6711
michael@0 6712 let mode = this.getIdentityMode(aState);
michael@0 6713 let result = { mode: mode };
michael@0 6714
michael@0 6715 // Don't show identity data for pages with an unknown identity or if any
michael@0 6716 // mixed content is loaded (mixed display content is loaded by default).
michael@0 6717 if (mode == this.IDENTITY_MODE_UNKNOWN ||
michael@0 6718 aState & Ci.nsIWebProgressListener.STATE_IS_BROKEN)
michael@0 6719 return result;
michael@0 6720
michael@0 6721 // Ideally we'd just make this a Java string
michael@0 6722 result.encrypted = Strings.browser.GetStringFromName("identity.encrypted2");
michael@0 6723 result.host = this.getEffectiveHost();
michael@0 6724
michael@0 6725 let iData = this.getIdentityData();
michael@0 6726 result.verifier = Strings.browser.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
michael@0 6727
michael@0 6728 // If the cert is identified, then we can populate the results with credentials
michael@0 6729 if (aState & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL) {
michael@0 6730 result.owner = iData.subjectOrg;
michael@0 6731
michael@0 6732 // Build an appropriate supplemental block out of whatever location data we have
michael@0 6733 let supplemental = "";
michael@0 6734 if (iData.city)
michael@0 6735 supplemental += iData.city + "\n";
michael@0 6736 if (iData.state && iData.country)
michael@0 6737 supplemental += Strings.browser.formatStringFromName("identity.identified.state_and_country", [iData.state, iData.country], 2);
michael@0 6738 else if (iData.state) // State only
michael@0 6739 supplemental += iData.state;
michael@0 6740 else if (iData.country) // Country only
michael@0 6741 supplemental += iData.country;
michael@0 6742 result.supplemental = supplemental;
michael@0 6743
michael@0 6744 return result;
michael@0 6745 }
michael@0 6746
michael@0 6747 // Otherwise, we don't know the cert owner
michael@0 6748 result.owner = Strings.browser.GetStringFromName("identity.ownerUnknown3");
michael@0 6749
michael@0 6750 // Cache the override service the first time we need to check it
michael@0 6751 if (!this._overrideService)
michael@0 6752 this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
michael@0 6753
michael@0 6754 // Check whether this site is a security exception. XPConnect does the right
michael@0 6755 // thing here in terms of converting _lastLocation.port from string to int, but
michael@0 6756 // the overrideService doesn't like undefined ports, so make sure we have
michael@0 6757 // something in the default case (bug 432241).
michael@0 6758 // .hostname can return an empty string in some exceptional cases -
michael@0 6759 // hasMatchingOverride does not handle that, so avoid calling it.
michael@0 6760 // Updating the tooltip value in those cases isn't critical.
michael@0 6761 // FIXME: Fixing bug 646690 would probably makes this check unnecessary
michael@0 6762 if (this._lastLocation.hostname &&
michael@0 6763 this._overrideService.hasMatchingOverride(this._lastLocation.hostname,
michael@0 6764 (this._lastLocation.port || 443),
michael@0 6765 iData.cert, {}, {}))
michael@0 6766 result.verifier = Strings.browser.GetStringFromName("identity.identified.verified_by_you");
michael@0 6767
michael@0 6768 return result;
michael@0 6769 },
michael@0 6770
michael@0 6771 /**
michael@0 6772 * Return the eTLD+1 version of the current hostname
michael@0 6773 */
michael@0 6774 getEffectiveHost: function getEffectiveHost() {
michael@0 6775 if (!this._IDNService)
michael@0 6776 this._IDNService = Cc["@mozilla.org/network/idn-service;1"]
michael@0 6777 .getService(Ci.nsIIDNService);
michael@0 6778 try {
michael@0 6779 let baseDomain = Services.eTLD.getBaseDomainFromHost(this._lastLocation.hostname);
michael@0 6780 return this._IDNService.convertToDisplayIDN(baseDomain, {});
michael@0 6781 } catch (e) {
michael@0 6782 // If something goes wrong (e.g. hostname is an IP address) just fail back
michael@0 6783 // to the full domain.
michael@0 6784 return this._lastLocation.hostname;
michael@0 6785 }
michael@0 6786 }
michael@0 6787 };
michael@0 6788
michael@0 6789 function OverscrollController(aTab) {
michael@0 6790 this.tab = aTab;
michael@0 6791 }
michael@0 6792
michael@0 6793 OverscrollController.prototype = {
michael@0 6794 supportsCommand : function supportsCommand(aCommand) {
michael@0 6795 if (aCommand != "cmd_linePrevious" && aCommand != "cmd_scrollPageUp")
michael@0 6796 return false;
michael@0 6797
michael@0 6798 return (this.tab.getViewport().y == 0);
michael@0 6799 },
michael@0 6800
michael@0 6801 isCommandEnabled : function isCommandEnabled(aCommand) {
michael@0 6802 return this.supportsCommand(aCommand);
michael@0 6803 },
michael@0 6804
michael@0 6805 doCommand : function doCommand(aCommand){
michael@0 6806 sendMessageToJava({ type: "ToggleChrome:Focus" });
michael@0 6807 },
michael@0 6808
michael@0 6809 onEvent : function onEvent(aEvent) { }
michael@0 6810 };
michael@0 6811
michael@0 6812 var SearchEngines = {
michael@0 6813 _contextMenuId: null,
michael@0 6814 PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
michael@0 6815 PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
michael@0 6816
michael@0 6817 init: function init() {
michael@0 6818 Services.obs.addObserver(this, "SearchEngines:Add", false);
michael@0 6819 Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
michael@0 6820 Services.obs.addObserver(this, "SearchEngines:Remove", false);
michael@0 6821 Services.obs.addObserver(this, "SearchEngines:RestoreDefaults", false);
michael@0 6822 Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
michael@0 6823
michael@0 6824 let filter = {
michael@0 6825 matches: function (aElement) {
michael@0 6826 // Copied from body of isTargetAKeywordField function in nsContextMenu.js
michael@0 6827 if(!(aElement instanceof HTMLInputElement))
michael@0 6828 return false;
michael@0 6829 let form = aElement.form;
michael@0 6830 if (!form || aElement.type == "password")
michael@0 6831 return false;
michael@0 6832
michael@0 6833 let method = form.method.toUpperCase();
michael@0 6834
michael@0 6835 // These are the following types of forms we can create keywords for:
michael@0 6836 //
michael@0 6837 // method encoding type can create keyword
michael@0 6838 // GET * YES
michael@0 6839 // * YES
michael@0 6840 // POST * YES
michael@0 6841 // POST application/x-www-form-urlencoded YES
michael@0 6842 // POST text/plain NO ( a little tricky to do)
michael@0 6843 // POST multipart/form-data NO
michael@0 6844 // POST everything else YES
michael@0 6845 return (method == "GET" || method == "") ||
michael@0 6846 (form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
michael@0 6847 }
michael@0 6848 };
michael@0 6849 SelectionHandler.addAction({
michael@0 6850 id: "search_add_action",
michael@0 6851 label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine"),
michael@0 6852 icon: "drawable://ab_add_search_engine",
michael@0 6853 selector: filter,
michael@0 6854 action: function(aElement) {
michael@0 6855 UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
michael@0 6856 SearchEngines.addEngine(aElement);
michael@0 6857 }
michael@0 6858 });
michael@0 6859 },
michael@0 6860
michael@0 6861 uninit: function uninit() {
michael@0 6862 Services.obs.removeObserver(this, "SearchEngines:Add");
michael@0 6863 Services.obs.removeObserver(this, "SearchEngines:GetVisible");
michael@0 6864 Services.obs.removeObserver(this, "SearchEngines:Remove");
michael@0 6865 Services.obs.removeObserver(this, "SearchEngines:RestoreDefaults");
michael@0 6866 Services.obs.removeObserver(this, "SearchEngines:SetDefault");
michael@0 6867 if (this._contextMenuId != null)
michael@0 6868 NativeWindow.contextmenus.remove(this._contextMenuId);
michael@0 6869 },
michael@0 6870
michael@0 6871 // Fetch list of search engines. all ? All engines : Visible engines only.
michael@0 6872 _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv, all) {
michael@0 6873 if (!Components.isSuccessCode(rv)) {
michael@0 6874 Cu.reportError("Could not initialize search service, bailing out.");
michael@0 6875 return;
michael@0 6876 }
michael@0 6877
michael@0 6878 let engineData = Services.search.getVisibleEngines({});
michael@0 6879 let searchEngines = engineData.map(function (engine) {
michael@0 6880 return {
michael@0 6881 name: engine.name,
michael@0 6882 identifier: engine.identifier,
michael@0 6883 iconURI: (engine.iconURI ? engine.iconURI.spec : null),
michael@0 6884 hidden: engine.hidden
michael@0 6885 };
michael@0 6886 });
michael@0 6887
michael@0 6888 let suggestTemplate = null;
michael@0 6889 let suggestEngine = null;
michael@0 6890
michael@0 6891 // Check to see if the default engine supports search suggestions. We only need to check
michael@0 6892 // the default engine because we only show suggestions for the default engine in the UI.
michael@0 6893 let engine = Services.search.defaultEngine;
michael@0 6894 if (engine.supportsResponseType("application/x-suggestions+json")) {
michael@0 6895 suggestEngine = engine.name;
michael@0 6896 suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec;
michael@0 6897 }
michael@0 6898
michael@0 6899 // By convention, the currently configured default engine is at position zero in searchEngines.
michael@0 6900 sendMessageToJava({
michael@0 6901 type: "SearchEngines:Data",
michael@0 6902 searchEngines: searchEngines,
michael@0 6903 suggest: {
michael@0 6904 engine: suggestEngine,
michael@0 6905 template: suggestTemplate,
michael@0 6906 enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED),
michael@0 6907 prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
michael@0 6908 }
michael@0 6909 });
michael@0 6910
michael@0 6911 // Send a speculative connection to the default engine.
michael@0 6912 let connector = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
michael@0 6913 let searchURI = Services.search.defaultEngine.getSubmission("dummy").uri;
michael@0 6914 let callbacks = window.QueryInterface(Ci.nsIInterfaceRequestor)
michael@0 6915 .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsILoadContext);
michael@0 6916 try {
michael@0 6917 connector.speculativeConnect(searchURI, callbacks);
michael@0 6918 } catch (e) {}
michael@0 6919 },
michael@0 6920
michael@0 6921 // Helper method to extract the engine name from a JSON. Simplifies the observe function.
michael@0 6922 _extractEngineFromJSON: function _extractEngineFromJSON(aData) {
michael@0 6923 let data = JSON.parse(aData);
michael@0 6924 return Services.search.getEngineByName(data.engine);
michael@0 6925 },
michael@0 6926
michael@0 6927 observe: function observe(aSubject, aTopic, aData) {
michael@0 6928 let engine;
michael@0 6929 switch(aTopic) {
michael@0 6930 case "SearchEngines:Add":
michael@0 6931 this.displaySearchEnginesList(aData);
michael@0 6932 break;
michael@0 6933 case "SearchEngines:GetVisible":
michael@0 6934 Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
michael@0 6935 break;
michael@0 6936 case "SearchEngines:Remove":
michael@0 6937 // Make sure the engine isn't hidden before removing it, to make sure it's
michael@0 6938 // visible if the user later re-adds it (works around bug 341833)
michael@0 6939 engine = this._extractEngineFromJSON(aData);
michael@0 6940 engine.hidden = false;
michael@0 6941 Services.search.removeEngine(engine);
michael@0 6942 break;
michael@0 6943 case "SearchEngines:RestoreDefaults":
michael@0 6944 // Un-hides all default engines.
michael@0 6945 Services.search.restoreDefaultEngines();
michael@0 6946 break;
michael@0 6947 case "SearchEngines:SetDefault":
michael@0 6948 engine = this._extractEngineFromJSON(aData);
michael@0 6949 // Move the new default search engine to the top of the search engine list.
michael@0 6950 Services.search.moveEngine(engine, 0);
michael@0 6951 Services.search.defaultEngine = engine;
michael@0 6952 break;
michael@0 6953
michael@0 6954 default:
michael@0 6955 dump("Unexpected message type observed: " + aTopic);
michael@0 6956 break;
michael@0 6957 }
michael@0 6958 },
michael@0 6959
michael@0 6960 // Display context menu listing names of the search engines available to be added.
michael@0 6961 displaySearchEnginesList: function displaySearchEnginesList(aData) {
michael@0 6962 let data = JSON.parse(aData);
michael@0 6963 let tab = BrowserApp.getTabForId(data.tabId);
michael@0 6964
michael@0 6965 if (!tab)
michael@0 6966 return;
michael@0 6967
michael@0 6968 let browser = tab.browser;
michael@0 6969 let engines = browser.engines;
michael@0 6970
michael@0 6971 let p = new Prompt({
michael@0 6972 window: browser.contentWindow
michael@0 6973 }).setSingleChoiceItems(engines.map(function(e) {
michael@0 6974 return { label: e.title };
michael@0 6975 })).show((function(data) {
michael@0 6976 if (data.button == -1)
michael@0 6977 return;
michael@0 6978
michael@0 6979 this.addOpenSearchEngine(engines[data.button]);
michael@0 6980 engines.splice(data.button, 1);
michael@0 6981
michael@0 6982 if (engines.length < 1) {
michael@0 6983 // Broadcast message that there are no more add-able search engines.
michael@0 6984 let newEngineMessage = {
michael@0 6985 type: "Link:OpenSearch",
michael@0 6986 tabID: tab.id,
michael@0 6987 visible: false
michael@0 6988 };
michael@0 6989
michael@0 6990 sendMessageToJava(newEngineMessage);
michael@0 6991 }
michael@0 6992 }).bind(this));
michael@0 6993 },
michael@0 6994
michael@0 6995 addOpenSearchEngine: function addOpenSearchEngine(engine) {
michael@0 6996 Services.search.addEngine(engine.url, Ci.nsISearchEngine.DATA_XML, engine.iconURL, false, {
michael@0 6997 onSuccess: function() {
michael@0 6998 // Display a toast confirming addition of new search engine.
michael@0 6999 NativeWindow.toast.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [engine.title], 1), "long");
michael@0 7000 },
michael@0 7001
michael@0 7002 onError: function(aCode) {
michael@0 7003 let errorMessage;
michael@0 7004 if (aCode == 2) {
michael@0 7005 // Engine is a duplicate.
michael@0 7006 errorMessage = "alertSearchEngineDuplicateToast";
michael@0 7007
michael@0 7008 } else {
michael@0 7009 // Unknown failure. Display general error message.
michael@0 7010 errorMessage = "alertSearchEngineErrorToast";
michael@0 7011 }
michael@0 7012
michael@0 7013 NativeWindow.toast.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), "long");
michael@0 7014 }
michael@0 7015 });
michael@0 7016 },
michael@0 7017
michael@0 7018 addEngine: function addEngine(aElement) {
michael@0 7019 let form = aElement.form;
michael@0 7020 let charset = aElement.ownerDocument.characterSet;
michael@0 7021 let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null);
michael@0 7022 let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
michael@0 7023 let method = form.method.toUpperCase();
michael@0 7024 let formData = [];
michael@0 7025
michael@0 7026 for (let i = 0; i < form.elements.length; ++i) {
michael@0 7027 let el = form.elements[i];
michael@0 7028 if (!el.type)
michael@0 7029 continue;
michael@0 7030
michael@0 7031 // make this text field a generic search parameter
michael@0 7032 if (aElement == el) {
michael@0 7033 formData.push({ name: el.name, value: "{searchTerms}" });
michael@0 7034 continue;
michael@0 7035 }
michael@0 7036
michael@0 7037 let type = el.type.toLowerCase();
michael@0 7038 let escapedName = escape(el.name);
michael@0 7039 let escapedValue = escape(el.value);
michael@0 7040
michael@0 7041 // add other form elements as parameters
michael@0 7042 switch (el.type) {
michael@0 7043 case "checkbox":
michael@0 7044 case "radio":
michael@0 7045 if (!el.checked) break;
michael@0 7046 case "text":
michael@0 7047 case "hidden":
michael@0 7048 case "textarea":
michael@0 7049 formData.push({ name: escapedName, value: escapedValue });
michael@0 7050 break;
michael@0 7051 case "select-one":
michael@0 7052 for (let option of el.options) {
michael@0 7053 if (option.selected) {
michael@0 7054 formData.push({ name: escapedName, value: escapedValue });
michael@0 7055 break;
michael@0 7056 }
michael@0 7057 }
michael@0 7058 }
michael@0 7059 }
michael@0 7060
michael@0 7061 // prompt user for name of search engine
michael@0 7062 let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine");
michael@0 7063 let title = { value: (aElement.ownerDocument.title || docURI.host) };
michael@0 7064 if (!Services.prompt.prompt(null, promptTitle, null, title, null, {}))
michael@0 7065 return;
michael@0 7066
michael@0 7067 // fetch the favicon for this page
michael@0 7068 let dbFile = FileUtils.getFile("ProfD", ["browser.db"]);
michael@0 7069 let mDBConn = Services.storage.openDatabase(dbFile);
michael@0 7070 let stmts = [];
michael@0 7071 stmts[0] = mDBConn.createStatement("SELECT favicon FROM history_with_favicons WHERE url = ?");
michael@0 7072 stmts[0].bindStringParameter(0, docURI.spec);
michael@0 7073 let favicon = null;
michael@0 7074 Services.search.init(function addEngine_cb(rv) {
michael@0 7075 if (!Components.isSuccessCode(rv)) {
michael@0 7076 Cu.reportError("Could not initialize search service, bailing out.");
michael@0 7077 return;
michael@0 7078 }
michael@0 7079 mDBConn.executeAsync(stmts, stmts.length, {
michael@0 7080 handleResult: function (results) {
michael@0 7081 let bytes = results.getNextRow().getResultByName("favicon");
michael@0 7082 if (bytes && bytes.length) {
michael@0 7083 favicon = "data:image/x-icon;base64," + btoa(String.fromCharCode.apply(null, bytes));
michael@0 7084 }
michael@0 7085 },
michael@0 7086 handleCompletion: function (reason) {
michael@0 7087 // if there's already an engine with this name, add a number to
michael@0 7088 // make the name unique (e.g., "Google" becomes "Google 2")
michael@0 7089 let name = title.value;
michael@0 7090 for (let i = 2; Services.search.getEngineByName(name); i++)
michael@0 7091 name = title.value + " " + i;
michael@0 7092
michael@0 7093 Services.search.addEngineWithDetails(name, favicon, null, null, method, formURL);
michael@0 7094 let engine = Services.search.getEngineByName(name);
michael@0 7095 engine.wrappedJSObject._queryCharset = charset;
michael@0 7096 for (let i = 0; i < formData.length; ++i) {
michael@0 7097 let param = formData[i];
michael@0 7098 if (param.name && param.value)
michael@0 7099 engine.addParam(param.name, param.value, null);
michael@0 7100 }
michael@0 7101 }
michael@0 7102 });
michael@0 7103 });
michael@0 7104 }
michael@0 7105 };
michael@0 7106
michael@0 7107 var ActivityObserver = {
michael@0 7108 init: function ao_init() {
michael@0 7109 Services.obs.addObserver(this, "application-background", false);
michael@0 7110 Services.obs.addObserver(this, "application-foreground", false);
michael@0 7111 },
michael@0 7112
michael@0 7113 observe: function ao_observe(aSubject, aTopic, aData) {
michael@0 7114 let isForeground = false;
michael@0 7115 let tab = BrowserApp.selectedTab;
michael@0 7116
michael@0 7117 switch (aTopic) {
michael@0 7118 case "application-background" :
michael@0 7119 let doc = (tab ? tab.browser.contentDocument : null);
michael@0 7120 if (doc && doc.mozFullScreen) {
michael@0 7121 doc.mozCancelFullScreen();
michael@0 7122 }
michael@0 7123 isForeground = false;
michael@0 7124 break;
michael@0 7125 case "application-foreground" :
michael@0 7126 isForeground = true;
michael@0 7127 break;
michael@0 7128 }
michael@0 7129
michael@0 7130 if (tab && tab.getActive() != isForeground) {
michael@0 7131 tab.setActive(isForeground);
michael@0 7132 }
michael@0 7133 }
michael@0 7134 };
michael@0 7135
michael@0 7136 #ifndef MOZ_ANDROID_SYNTHAPKS
michael@0 7137 var WebappsUI = {
michael@0 7138 init: function init() {
michael@0 7139 Cu.import("resource://gre/modules/Webapps.jsm");
michael@0 7140 Cu.import("resource://gre/modules/AppsUtils.jsm");
michael@0 7141 DOMApplicationRegistry.allAppsLaunchable = true;
michael@0 7142
michael@0 7143 Services.obs.addObserver(this, "webapps-ask-install", false);
michael@0 7144 Services.obs.addObserver(this, "webapps-launch", false);
michael@0 7145 Services.obs.addObserver(this, "webapps-uninstall", false);
michael@0 7146 Services.obs.addObserver(this, "webapps-install-error", false);
michael@0 7147 },
michael@0 7148
michael@0 7149 uninit: function unint() {
michael@0 7150 Services.obs.removeObserver(this, "webapps-ask-install");
michael@0 7151 Services.obs.removeObserver(this, "webapps-launch");
michael@0 7152 Services.obs.removeObserver(this, "webapps-uninstall");
michael@0 7153 Services.obs.removeObserver(this, "webapps-install-error");
michael@0 7154 },
michael@0 7155
michael@0 7156 DEFAULT_ICON: "chrome://browser/skin/images/default-app-icon.png",
michael@0 7157 DEFAULT_PREFS_FILENAME: "default-prefs.js",
michael@0 7158
michael@0 7159 observe: function observe(aSubject, aTopic, aData) {
michael@0 7160 let data = {};
michael@0 7161 try {
michael@0 7162 data = JSON.parse(aData);
michael@0 7163 data.mm = aSubject;
michael@0 7164 } catch(ex) { }
michael@0 7165 switch (aTopic) {
michael@0 7166 case "webapps-install-error":
michael@0 7167 let msg = "";
michael@0 7168 switch (aData) {
michael@0 7169 case "INVALID_MANIFEST":
michael@0 7170 case "MANIFEST_PARSE_ERROR":
michael@0 7171 msg = Strings.browser.GetStringFromName("webapps.manifestInstallError");
michael@0 7172 break;
michael@0 7173 case "NETWORK_ERROR":
michael@0 7174 case "MANIFEST_URL_ERROR":
michael@0 7175 msg = Strings.browser.GetStringFromName("webapps.networkInstallError");
michael@0 7176 break;
michael@0 7177 default:
michael@0 7178 msg = Strings.browser.GetStringFromName("webapps.installError");
michael@0 7179 }
michael@0 7180 NativeWindow.toast.show(msg, "short");
michael@0 7181 console.log("Error installing app: " + aData);
michael@0 7182 break;
michael@0 7183 case "webapps-ask-install":
michael@0 7184 this.doInstall(data);
michael@0 7185 break;
michael@0 7186 case "webapps-launch":
michael@0 7187 this.openURL(data.manifestURL, data.origin);
michael@0 7188 break;
michael@0 7189 case "webapps-uninstall":
michael@0 7190 sendMessageToJava({
michael@0 7191 type: "Webapps:Uninstall",
michael@0 7192 origin: data.origin
michael@0 7193 });
michael@0 7194 break;
michael@0 7195 }
michael@0 7196 },
michael@0 7197
michael@0 7198 doInstall: function doInstall(aData) {
michael@0 7199 let jsonManifest = aData.isPackage ? aData.app.updateManifest : aData.app.manifest;
michael@0 7200 let manifest = new ManifestHelper(jsonManifest, aData.app.origin);
michael@0 7201
michael@0 7202 if (Services.prompt.confirm(null, Strings.browser.GetStringFromName("webapps.installTitle"), manifest.name + "\n" + aData.app.origin)) {
michael@0 7203 // Get a profile for the app to be installed in. We'll download everything before creating the icons.
michael@0 7204 let origin = aData.app.origin;
michael@0 7205 sendMessageToJava({
michael@0 7206 type: "Webapps:Preinstall",
michael@0 7207 name: manifest.name,
michael@0 7208 manifestURL: aData.app.manifestURL,
michael@0 7209 origin: origin
michael@0 7210 }, (data) => {
michael@0 7211 let profilePath = data.profile;
michael@0 7212 if (!profilePath)
michael@0 7213 return;
michael@0 7214
michael@0 7215 let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
michael@0 7216 file.initWithPath(profilePath);
michael@0 7217
michael@0 7218 let self = this;
michael@0 7219 DOMApplicationRegistry.confirmInstall(aData, file,
michael@0 7220 function (aManifest) {
michael@0 7221 let localeManifest = new ManifestHelper(aManifest, aData.app.origin);
michael@0 7222
michael@0 7223 // the manifest argument is the manifest from within the zip file,
michael@0 7224 // TODO so now would be a good time to ask about permissions.
michael@0 7225 self.makeBase64Icon(localeManifest.biggestIconURL || this.DEFAULT_ICON,
michael@0 7226 function(scaledIcon, fullsizeIcon) {
michael@0 7227 // if java returned a profile path to us, try to use it to pre-populate the app cache
michael@0 7228 // also save the icon so that it can be used in the splash screen
michael@0 7229 try {
michael@0 7230 let iconFile = file.clone();
michael@0 7231 iconFile.append("logo.png");
michael@0 7232 let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
michael@0 7233 persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
michael@0 7234 persist.persistFlags |= Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
michael@0 7235
michael@0 7236 let source = Services.io.newURI(fullsizeIcon, "UTF8", null);
michael@0 7237 persist.saveURI(source, null, null, null, null, iconFile, null);
michael@0 7238
michael@0 7239 // aData.app.origin may now point to the app: url that hosts this app
michael@0 7240 sendMessageToJava({
michael@0 7241 type: "Webapps:Postinstall",
michael@0 7242 name: localeManifest.name,
michael@0 7243 manifestURL: aData.app.manifestURL,
michael@0 7244 originalOrigin: origin,
michael@0 7245 origin: aData.app.origin,
michael@0 7246 iconURL: fullsizeIcon
michael@0 7247 });
michael@0 7248 if (!!aData.isPackage) {
michael@0 7249 // For packaged apps, put a notification in the notification bar.
michael@0 7250 let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
michael@0 7251 let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
michael@0 7252 alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", {
michael@0 7253 observe: function () {
michael@0 7254 self.openURL(aData.app.manifestURL, aData.app.origin);
michael@0 7255 }
michael@0 7256 }, "webapp");
michael@0 7257 }
michael@0 7258
michael@0 7259 // Create a system notification allowing the user to launch the app
michael@0 7260 let observer = {
michael@0 7261 observe: function (aSubject, aTopic) {
michael@0 7262 if (aTopic == "alertclickcallback") {
michael@0 7263 WebappsUI.openURL(aData.app.manifestURL, origin);
michael@0 7264 }
michael@0 7265 }
michael@0 7266 };
michael@0 7267
michael@0 7268 let message = Strings.browser.GetStringFromName("webapps.alertSuccess");
michael@0 7269 let alerts = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
michael@0 7270 alerts.showAlertNotification("drawable://alert_app", localeManifest.name, message, true, "", observer, "webapp");
michael@0 7271 } catch(ex) {
michael@0 7272 console.log(ex);
michael@0 7273 }
michael@0 7274 self.writeDefaultPrefs(file, localeManifest);
michael@0 7275 }
michael@0 7276 );
michael@0 7277 }
michael@0 7278 );
michael@0 7279 });
michael@0 7280 } else {
michael@0 7281 DOMApplicationRegistry.denyInstall(aData);
michael@0 7282 }
michael@0 7283 },
michael@0 7284
michael@0 7285 writeDefaultPrefs: function webapps_writeDefaultPrefs(aProfile, aManifest) {
michael@0 7286 // build any app specific default prefs
michael@0 7287 let prefs = [];
michael@0 7288 if (aManifest.orientation) {
michael@0 7289 prefs.push({name:"app.orientation.default", value: aManifest.orientation.join(",") });
michael@0 7290 }
michael@0 7291
michael@0 7292 // write them into the app profile
michael@0 7293 let defaultPrefsFile = aProfile.clone();
michael@0 7294 defaultPrefsFile.append(this.DEFAULT_PREFS_FILENAME);
michael@0 7295 this._writeData(defaultPrefsFile, prefs);
michael@0 7296 },
michael@0 7297
michael@0 7298 _writeData: function(aFile, aPrefs) {
michael@0 7299 if (aPrefs.length > 0) {
michael@0 7300 let array = new TextEncoder().encode(JSON.stringify(aPrefs));
michael@0 7301 OS.File.writeAtomic(aFile.path, array, { tmpPath: aFile.path + ".tmp" }).then(null, function onError(reason) {
michael@0 7302 console.log("Error writing default prefs: " + reason);
michael@0 7303 });
michael@0 7304 }
michael@0 7305 },
michael@0 7306
michael@0 7307 openURL: function openURL(aManifestURL, aOrigin) {
michael@0 7308 sendMessageToJava({
michael@0 7309 type: "Webapps:Open",
michael@0 7310 manifestURL: aManifestURL,
michael@0 7311 origin: aOrigin
michael@0 7312 });
michael@0 7313 },
michael@0 7314
michael@0 7315 get iconSize() {
michael@0 7316 let iconSize = 64;
michael@0 7317 try {
michael@0 7318 let jni = new JNI();
michael@0 7319 let cls = jni.findClass("org/mozilla/gecko/GeckoAppShell");
michael@0 7320 let method = jni.getStaticMethodID(cls, "getPreferredIconSize", "()I");
michael@0 7321 iconSize = jni.callStaticIntMethod(cls, method);
michael@0 7322 jni.close();
michael@0 7323 } catch(ex) {
michael@0 7324 console.log(ex);
michael@0 7325 }
michael@0 7326
michael@0 7327 delete this.iconSize;
michael@0 7328 return this.iconSize = iconSize;
michael@0 7329 },
michael@0 7330
michael@0 7331 makeBase64Icon: function loadAndMakeBase64Icon(aIconURL, aCallbackFunction) {
michael@0 7332 let size = this.iconSize;
michael@0 7333
michael@0 7334 let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
michael@0 7335 canvas.width = canvas.height = size;
michael@0 7336 let ctx = canvas.getContext("2d");
michael@0 7337 let favicon = new Image();
michael@0 7338 favicon.onload = function() {
michael@0 7339 ctx.drawImage(favicon, 0, 0, size, size);
michael@0 7340 let scaledIcon = canvas.toDataURL("image/png", "");
michael@0 7341
michael@0 7342 canvas.width = favicon.width;
michael@0 7343 canvas.height = favicon.height;
michael@0 7344 ctx.drawImage(favicon, 0, 0, favicon.width, favicon.height);
michael@0 7345 let fullsizeIcon = canvas.toDataURL("image/png", "");
michael@0 7346
michael@0 7347 canvas = null;
michael@0 7348 aCallbackFunction.call(null, scaledIcon, fullsizeIcon);
michael@0 7349 };
michael@0 7350 favicon.onerror = function() {
michael@0 7351 Cu.reportError("CreateShortcut: favicon image load error");
michael@0 7352
michael@0 7353 // if the image failed to load, and it was not our default icon, attempt to
michael@0 7354 // use our default as a fallback
michael@0 7355 if (favicon.src != WebappsUI.DEFAULT_ICON) {
michael@0 7356 favicon.src = WebappsUI.DEFAULT_ICON;
michael@0 7357 }
michael@0 7358 };
michael@0 7359
michael@0 7360 favicon.src = aIconURL;
michael@0 7361 },
michael@0 7362
michael@0 7363 createShortcut: function createShortcut(aTitle, aURL, aIconURL, aType) {
michael@0 7364 this.makeBase64Icon(aIconURL, function _createShortcut(icon) {
michael@0 7365 try {
michael@0 7366 let shell = Cc["@mozilla.org/browser/shell-service;1"].createInstance(Ci.nsIShellService);
michael@0 7367 shell.createShortcut(aTitle, aURL, icon, aType);
michael@0 7368 } catch(e) {
michael@0 7369 Cu.reportError(e);
michael@0 7370 }
michael@0 7371 });
michael@0 7372 }
michael@0 7373 }
michael@0 7374 #endif
michael@0 7375
michael@0 7376 var RemoteDebugger = {
michael@0 7377 init: function rd_init() {
michael@0 7378 Services.prefs.addObserver("devtools.debugger.", this, false);
michael@0 7379
michael@0 7380 if (this._isEnabled())
michael@0 7381 this._start();
michael@0 7382 },
michael@0 7383
michael@0 7384 observe: function rd_observe(aSubject, aTopic, aData) {
michael@0 7385 if (aTopic != "nsPref:changed")
michael@0 7386 return;
michael@0 7387
michael@0 7388 switch (aData) {
michael@0 7389 case "devtools.debugger.remote-enabled":
michael@0 7390 if (this._isEnabled())
michael@0 7391 this._start();
michael@0 7392 else
michael@0 7393 this._stop();
michael@0 7394 break;
michael@0 7395
michael@0 7396 case "devtools.debugger.remote-port":
michael@0 7397 if (this._isEnabled())
michael@0 7398 this._restart();
michael@0 7399 break;
michael@0 7400 }
michael@0 7401 },
michael@0 7402
michael@0 7403 uninit: function rd_uninit() {
michael@0 7404 Services.prefs.removeObserver("devtools.debugger.", this);
michael@0 7405 this._stop();
michael@0 7406 },
michael@0 7407
michael@0 7408 _getPort: function _rd_getPort() {
michael@0 7409 return Services.prefs.getIntPref("devtools.debugger.remote-port");
michael@0 7410 },
michael@0 7411
michael@0 7412 _isEnabled: function rd_isEnabled() {
michael@0 7413 return Services.prefs.getBoolPref("devtools.debugger.remote-enabled");
michael@0 7414 },
michael@0 7415
michael@0 7416 /**
michael@0 7417 * Prompt the user to accept or decline the incoming connection.
michael@0 7418 * This is passed to DebuggerService.init as a callback.
michael@0 7419 *
michael@0 7420 * @return true if the connection should be permitted, false otherwise
michael@0 7421 */
michael@0 7422 _showConnectionPrompt: function rd_showConnectionPrompt() {
michael@0 7423 let title = Strings.browser.GetStringFromName("remoteIncomingPromptTitle");
michael@0 7424 let msg = Strings.browser.GetStringFromName("remoteIncomingPromptMessage");
michael@0 7425 let disable = Strings.browser.GetStringFromName("remoteIncomingPromptDisable");
michael@0 7426 let cancel = Strings.browser.GetStringFromName("remoteIncomingPromptCancel");
michael@0 7427 let agree = Strings.browser.GetStringFromName("remoteIncomingPromptAccept");
michael@0 7428
michael@0 7429 // Make prompt. Note: button order is in reverse.
michael@0 7430 let prompt = new Prompt({
michael@0 7431 window: null,
michael@0 7432 hint: "remotedebug",
michael@0 7433 title: title,
michael@0 7434 message: msg,
michael@0 7435 buttons: [ agree, cancel, disable ],
michael@0 7436 priority: 1
michael@0 7437 });
michael@0 7438
michael@0 7439 // The debugger server expects a synchronous response, so spin on result since Prompt is async.
michael@0 7440 let result = null;
michael@0 7441
michael@0 7442 prompt.show(function(data) {
michael@0 7443 result = data.button;
michael@0 7444 });
michael@0 7445
michael@0 7446 // Spin this thread while we wait for a result.
michael@0 7447 let thread = Services.tm.currentThread;
michael@0 7448 while (result == null)
michael@0 7449 thread.processNextEvent(true);
michael@0 7450
michael@0 7451 if (result === 0)
michael@0 7452 return true;
michael@0 7453 if (result === 2) {
michael@0 7454 Services.prefs.setBoolPref("devtools.debugger.remote-enabled", false);
michael@0 7455 this._stop();
michael@0 7456 }
michael@0 7457 return false;
michael@0 7458 },
michael@0 7459
michael@0 7460 _restart: function rd_restart() {
michael@0 7461 this._stop();
michael@0 7462 this._start();
michael@0 7463 },
michael@0 7464
michael@0 7465 _start: function rd_start() {
michael@0 7466 try {
michael@0 7467 if (!DebuggerServer.initialized) {
michael@0 7468 DebuggerServer.init(this._showConnectionPrompt.bind(this));
michael@0 7469 DebuggerServer.addBrowserActors();
michael@0 7470 DebuggerServer.addActors("chrome://browser/content/dbg-browser-actors.js");
michael@0 7471 }
michael@0 7472
michael@0 7473 let port = this._getPort();
michael@0 7474 DebuggerServer.openListener(port);
michael@0 7475 dump("Remote debugger listening on port " + port);
michael@0 7476 } catch(e) {
michael@0 7477 dump("Remote debugger didn't start: " + e);
michael@0 7478 }
michael@0 7479 },
michael@0 7480
michael@0 7481 _stop: function rd_start() {
michael@0 7482 DebuggerServer.closeListener();
michael@0 7483 dump("Remote debugger stopped");
michael@0 7484 }
michael@0 7485 };
michael@0 7486
michael@0 7487 var Telemetry = {
michael@0 7488 addData: function addData(aHistogramId, aValue) {
michael@0 7489 let histogram = Services.telemetry.getHistogramById(aHistogramId);
michael@0 7490 histogram.add(aValue);
michael@0 7491 },
michael@0 7492 };
michael@0 7493
michael@0 7494 let Reader = {
michael@0 7495 // Version of the cache database schema
michael@0 7496 DB_VERSION: 1,
michael@0 7497
michael@0 7498 DEBUG: 0,
michael@0 7499
michael@0 7500 READER_ADD_SUCCESS: 0,
michael@0 7501 READER_ADD_FAILED: 1,
michael@0 7502 READER_ADD_DUPLICATE: 2,
michael@0 7503
michael@0 7504 // Don't try to parse the page if it has too many elements (for memory and
michael@0 7505 // performance reasons)
michael@0 7506 MAX_ELEMS_TO_PARSE: 3000,
michael@0 7507
michael@0 7508 isEnabledForParseOnLoad: false,
michael@0 7509
michael@0 7510 init: function Reader_init() {
michael@0 7511 this.log("Init()");
michael@0 7512 this._requests = {};
michael@0 7513
michael@0 7514 this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
michael@0 7515
michael@0 7516 Services.obs.addObserver(this, "Reader:Add", false);
michael@0 7517 Services.obs.addObserver(this, "Reader:Remove", false);
michael@0 7518
michael@0 7519 Services.prefs.addObserver("reader.parse-on-load.", this, false);
michael@0 7520 },
michael@0 7521
michael@0 7522 pageAction: {
michael@0 7523 readerModeCallback: function(){
michael@0 7524 sendMessageToJava({
michael@0 7525 type: "Reader:Click",
michael@0 7526 });
michael@0 7527 },
michael@0 7528
michael@0 7529 readerModeActiveCallback: function(){
michael@0 7530 sendMessageToJava({
michael@0 7531 type: "Reader:LongClick",
michael@0 7532 });
michael@0 7533
michael@0 7534 // Create a relative timestamp for telemetry
michael@0 7535 let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized;
michael@0 7536 UITelemetry.addEvent("save.1", "pageaction", uptime, "reader");
michael@0 7537 },
michael@0 7538 },
michael@0 7539
michael@0 7540 updatePageAction: function(tab) {
michael@0 7541 if (this.pageAction.id) {
michael@0 7542 NativeWindow.pageactions.remove(this.pageAction.id);
michael@0 7543 delete this.pageAction.id;
michael@0 7544 }
michael@0 7545
michael@0 7546 // Create a relative timestamp for telemetry
michael@0 7547 let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized;
michael@0 7548
michael@0 7549 if (tab.readerActive) {
michael@0 7550 this.pageAction.id = NativeWindow.pageactions.add({
michael@0 7551 title: Strings.browser.GetStringFromName("readerMode.exit"),
michael@0 7552 icon: "drawable://reader_active",
michael@0 7553 clickCallback: this.pageAction.readerModeCallback,
michael@0 7554 important: true
michael@0 7555 });
michael@0 7556
michael@0 7557 // Only start a reader session if the viewer is in the foreground. We do
michael@0 7558 // not track background reader viewers.
michael@0 7559 UITelemetry.startSession("reader.1", uptime);
michael@0 7560 return;
michael@0 7561 }
michael@0 7562
michael@0 7563 // Only stop a reader session if the foreground viewer is not visible.
michael@0 7564 UITelemetry.stopSession("reader.1", "", uptime);
michael@0 7565
michael@0 7566 if (tab.readerEnabled) {
michael@0 7567 this.pageAction.id = NativeWindow.pageactions.add({
michael@0 7568 title: Strings.browser.GetStringFromName("readerMode.enter"),
michael@0 7569 icon: "drawable://reader",
michael@0 7570 clickCallback:this.pageAction.readerModeCallback,
michael@0 7571 longClickCallback: this.pageAction.readerModeActiveCallback,
michael@0 7572 important: true
michael@0 7573 });
michael@0 7574 }
michael@0 7575 },
michael@0 7576
michael@0 7577 observe: function(aMessage, aTopic, aData) {
michael@0 7578 switch(aTopic) {
michael@0 7579 case "Reader:Add": {
michael@0 7580 let args = JSON.parse(aData);
michael@0 7581 if ('fromAboutReader' in args) {
michael@0 7582 // Ignore adds initiated from aboutReader menu banner
michael@0 7583 break;
michael@0 7584 }
michael@0 7585
michael@0 7586 let tabID = null;
michael@0 7587 let url, urlWithoutRef;
michael@0 7588
michael@0 7589 if ('tabID' in args) {
michael@0 7590 tabID = args.tabID;
michael@0 7591
michael@0 7592 let tab = BrowserApp.getTabForId(tabID);
michael@0 7593 let currentURI = tab.browser.currentURI;
michael@0 7594
michael@0 7595 url = currentURI.spec;
michael@0 7596 urlWithoutRef = currentURI.specIgnoringRef;
michael@0 7597 } else if ('url' in args) {
michael@0 7598 let uri = Services.io.newURI(args.url, null, null);
michael@0 7599 url = uri.spec;
michael@0 7600 urlWithoutRef = uri.specIgnoringRef;
michael@0 7601 } else {
michael@0 7602 throw new Error("Reader:Add requires a tabID or an URL as argument");
michael@0 7603 }
michael@0 7604
michael@0 7605 let sendResult = function(result, article) {
michael@0 7606 article = article || {};
michael@0 7607 this.log("Reader:Add success=" + result + ", url=" + url + ", title=" + article.title + ", excerpt=" + article.excerpt);
michael@0 7608
michael@0 7609 sendMessageToJava({
michael@0 7610 type: "Reader:Added",
michael@0 7611 result: result,
michael@0 7612 title: article.title,
michael@0 7613 url: url,
michael@0 7614 length: article.length,
michael@0 7615 excerpt: article.excerpt
michael@0 7616 });
michael@0 7617 }.bind(this);
michael@0 7618
michael@0 7619 let handleArticle = function(article) {
michael@0 7620 if (!article) {
michael@0 7621 sendResult(this.READER_ADD_FAILED, null);
michael@0 7622 return;
michael@0 7623 }
michael@0 7624
michael@0 7625 this.storeArticleInCache(article, function(success) {
michael@0 7626 let result = (success ? this.READER_ADD_SUCCESS : this.READER_ADD_FAILED);
michael@0 7627 sendResult(result, article);
michael@0 7628 }.bind(this));
michael@0 7629 }.bind(this);
michael@0 7630
michael@0 7631 this.getArticleFromCache(urlWithoutRef, function (article) {
michael@0 7632 // If the article is already in reading list, bail
michael@0 7633 if (article) {
michael@0 7634 sendResult(this.READER_ADD_DUPLICATE, null);
michael@0 7635 return;
michael@0 7636 }
michael@0 7637
michael@0 7638 if (tabID != null) {
michael@0 7639 this.getArticleForTab(tabID, urlWithoutRef, handleArticle);
michael@0 7640 } else {
michael@0 7641 this.parseDocumentFromURL(urlWithoutRef, handleArticle);
michael@0 7642 }
michael@0 7643 }.bind(this));
michael@0 7644 break;
michael@0 7645 }
michael@0 7646
michael@0 7647 case "Reader:Remove": {
michael@0 7648 let url = aData;
michael@0 7649 this.removeArticleFromCache(url, function(success) {
michael@0 7650 this.log("Reader:Remove success=" + success + ", url=" + url);
michael@0 7651
michael@0 7652 if (success) {
michael@0 7653 sendMessageToJava({
michael@0 7654 type: "Reader:Removed",
michael@0 7655 url: url
michael@0 7656 });
michael@0 7657 }
michael@0 7658 }.bind(this));
michael@0 7659 break;
michael@0 7660 }
michael@0 7661
michael@0 7662 case "nsPref:changed": {
michael@0 7663 if (aData.startsWith("reader.parse-on-load.")) {
michael@0 7664 this.isEnabledForParseOnLoad = this.getStateForParseOnLoad();
michael@0 7665 }
michael@0 7666 break;
michael@0 7667 }
michael@0 7668 }
michael@0 7669 },
michael@0 7670
michael@0 7671 getStateForParseOnLoad: function Reader_getStateForParseOnLoad() {
michael@0 7672 let isEnabled = Services.prefs.getBoolPref("reader.parse-on-load.enabled");
michael@0 7673 let isForceEnabled = Services.prefs.getBoolPref("reader.parse-on-load.force-enabled");
michael@0 7674 // For low-memory devices, don't allow reader mode since it takes up a lot of memory.
michael@0 7675 // See https://bugzilla.mozilla.org/show_bug.cgi?id=792603 for details.
michael@0 7676 return isForceEnabled || (isEnabled && !BrowserApp.isOnLowMemoryPlatform);
michael@0 7677 },
michael@0 7678
michael@0 7679 parseDocumentFromURL: function Reader_parseDocumentFromURL(url, callback) {
michael@0 7680 // If there's an on-going request for the same URL, simply append one
michael@0 7681 // more callback to it to be called when the request is done.
michael@0 7682 if (url in this._requests) {
michael@0 7683 let request = this._requests[url];
michael@0 7684 request.callbacks.push(callback);
michael@0 7685 return;
michael@0 7686 }
michael@0 7687
michael@0 7688 let request = { url: url, callbacks: [callback] };
michael@0 7689 this._requests[url] = request;
michael@0 7690
michael@0 7691 try {
michael@0 7692 this.log("parseDocumentFromURL: " + url);
michael@0 7693
michael@0 7694 // First, try to find a cached parsed article in the DB
michael@0 7695 this.getArticleFromCache(url, function(article) {
michael@0 7696 if (article) {
michael@0 7697 this.log("Page found in cache, return article immediately");
michael@0 7698 this._runCallbacksAndFinish(request, article);
michael@0 7699 return;
michael@0 7700 }
michael@0 7701
michael@0 7702 if (!this._requests) {
michael@0 7703 this.log("Reader has been destroyed, abort");
michael@0 7704 return;
michael@0 7705 }
michael@0 7706
michael@0 7707 // Article hasn't been found in the cache DB, we need to
michael@0 7708 // download the page and parse the article out of it.
michael@0 7709 this._downloadAndParseDocument(url, request);
michael@0 7710 }.bind(this));
michael@0 7711 } catch (e) {
michael@0 7712 this.log("Error parsing document from URL: " + e);
michael@0 7713 this._runCallbacksAndFinish(request, null);
michael@0 7714 }
michael@0 7715 },
michael@0 7716
michael@0 7717 getArticleForTab: function Reader_getArticleForTab(tabId, url, callback) {
michael@0 7718 let tab = BrowserApp.getTabForId(tabId);
michael@0 7719 if (tab) {
michael@0 7720 let article = tab.savedArticle;
michael@0 7721 if (article && article.url == url) {
michael@0 7722 this.log("Saved article found in tab");
michael@0 7723 callback(article);
michael@0 7724 return;
michael@0 7725 }
michael@0 7726 }
michael@0 7727
michael@0 7728 this.parseDocumentFromURL(url, callback);
michael@0 7729 },
michael@0 7730
michael@0 7731 parseDocumentFromTab: function(tabId, callback) {
michael@0 7732 try {
michael@0 7733 this.log("parseDocumentFromTab: " + tabId);
michael@0 7734
michael@0 7735 let tab = BrowserApp.getTabForId(tabId);
michael@0 7736 let url = tab.browser.contentWindow.location.href;
michael@0 7737 let uri = Services.io.newURI(url, null, null);
michael@0 7738
michael@0 7739 if (!this._shouldCheckUri(uri)) {
michael@0 7740 callback(null);
michael@0 7741 return;
michael@0 7742 }
michael@0 7743
michael@0 7744 // First, try to find a cached parsed article in the DB
michael@0 7745 this.getArticleFromCache(url, function(article) {
michael@0 7746 if (article) {
michael@0 7747 this.log("Page found in cache, return article immediately");
michael@0 7748 callback(article);
michael@0 7749 return;
michael@0 7750 }
michael@0 7751
michael@0 7752 let doc = tab.browser.contentWindow.document;
michael@0 7753 this._readerParse(uri, doc, function (article) {
michael@0 7754 if (!article) {
michael@0 7755 this.log("Failed to parse page");
michael@0 7756 callback(null);
michael@0 7757 return;
michael@0 7758 }
michael@0 7759
michael@0 7760 callback(article);
michael@0 7761 }.bind(this));
michael@0 7762 }.bind(this));
michael@0 7763 } catch (e) {
michael@0 7764 this.log("Error parsing document from tab: " + e);
michael@0 7765 callback(null);
michael@0 7766 }
michael@0 7767 },
michael@0 7768
michael@0 7769 getArticleFromCache: function Reader_getArticleFromCache(url, callback) {
michael@0 7770 this._getCacheDB(function(cacheDB) {
michael@0 7771 if (!cacheDB) {
michael@0 7772 callback(false);
michael@0 7773 return;
michael@0 7774 }
michael@0 7775
michael@0 7776 let transaction = cacheDB.transaction(cacheDB.objectStoreNames);
michael@0 7777 let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
michael@0 7778
michael@0 7779 let request = articles.get(url);
michael@0 7780
michael@0 7781 request.onerror = function(event) {
michael@0 7782 this.log("Error getting article from the cache DB: " + url);
michael@0 7783 callback(null);
michael@0 7784 }.bind(this);
michael@0 7785
michael@0 7786 request.onsuccess = function(event) {
michael@0 7787 this.log("Got article from the cache DB: " + event.target.result);
michael@0 7788 callback(event.target.result);
michael@0 7789 }.bind(this);
michael@0 7790 }.bind(this));
michael@0 7791 },
michael@0 7792
michael@0 7793 storeArticleInCache: function Reader_storeArticleInCache(article, callback) {
michael@0 7794 this._getCacheDB(function(cacheDB) {
michael@0 7795 if (!cacheDB) {
michael@0 7796 callback(false);
michael@0 7797 return;
michael@0 7798 }
michael@0 7799
michael@0 7800 let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
michael@0 7801 let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
michael@0 7802
michael@0 7803 let request = articles.add(article);
michael@0 7804
michael@0 7805 request.onerror = function(event) {
michael@0 7806 this.log("Error storing article in the cache DB: " + article.url);
michael@0 7807 callback(false);
michael@0 7808 }.bind(this);
michael@0 7809
michael@0 7810 request.onsuccess = function(event) {
michael@0 7811 this.log("Stored article in the cache DB: " + article.url);
michael@0 7812 callback(true);
michael@0 7813 }.bind(this);
michael@0 7814 }.bind(this));
michael@0 7815 },
michael@0 7816
michael@0 7817 removeArticleFromCache: function Reader_removeArticleFromCache(url, callback) {
michael@0 7818 this._getCacheDB(function(cacheDB) {
michael@0 7819 if (!cacheDB) {
michael@0 7820 callback(false);
michael@0 7821 return;
michael@0 7822 }
michael@0 7823
michael@0 7824 let transaction = cacheDB.transaction(cacheDB.objectStoreNames, "readwrite");
michael@0 7825 let articles = transaction.objectStore(cacheDB.objectStoreNames[0]);
michael@0 7826
michael@0 7827 let request = articles.delete(url);
michael@0 7828
michael@0 7829 request.onerror = function(event) {
michael@0 7830 this.log("Error removing article from the cache DB: " + url);
michael@0 7831 callback(false);
michael@0 7832 }.bind(this);
michael@0 7833
michael@0 7834 request.onsuccess = function(event) {
michael@0 7835 this.log("Removed article from the cache DB: " + url);
michael@0 7836 callback(true);
michael@0 7837 }.bind(this);
michael@0 7838 }.bind(this));
michael@0 7839 },
michael@0 7840
michael@0 7841 uninit: function Reader_uninit() {
michael@0 7842 Services.prefs.removeObserver("reader.parse-on-load.", this);
michael@0 7843
michael@0 7844 Services.obs.removeObserver(this, "Reader:Add");
michael@0 7845 Services.obs.removeObserver(this, "Reader:Remove");
michael@0 7846
michael@0 7847 let requests = this._requests;
michael@0 7848 for (let url in requests) {
michael@0 7849 let request = requests[url];
michael@0 7850 if (request.browser) {
michael@0 7851 let browser = request.browser;
michael@0 7852 browser.parentNode.removeChild(browser);
michael@0 7853 }
michael@0 7854 }
michael@0 7855 delete this._requests;
michael@0 7856
michael@0 7857 if (this._cacheDB) {
michael@0 7858 this._cacheDB.close();
michael@0 7859 delete this._cacheDB;
michael@0 7860 }
michael@0 7861 },
michael@0 7862
michael@0 7863 log: function(msg) {
michael@0 7864 if (this.DEBUG)
michael@0 7865 dump("Reader: " + msg);
michael@0 7866 },
michael@0 7867
michael@0 7868 _shouldCheckUri: function Reader_shouldCheckUri(uri) {
michael@0 7869 if ((uri.prePath + "/") === uri.spec) {
michael@0 7870 this.log("Not parsing home page: " + uri.spec);
michael@0 7871 return false;
michael@0 7872 }
michael@0 7873
michael@0 7874 if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
michael@0 7875 this.log("Not parsing URI scheme: " + uri.scheme);
michael@0 7876 return false;
michael@0 7877 }
michael@0 7878
michael@0 7879 return true;
michael@0 7880 },
michael@0 7881
michael@0 7882 _readerParse: function Reader_readerParse(uri, doc, callback) {
michael@0 7883 let numTags = doc.getElementsByTagName("*").length;
michael@0 7884 if (numTags > this.MAX_ELEMS_TO_PARSE) {
michael@0 7885 this.log("Aborting parse for " + uri.spec + "; " + numTags + " elements found");
michael@0 7886 callback(null);
michael@0 7887 return;
michael@0 7888 }
michael@0 7889
michael@0 7890 let worker = new ChromeWorker("readerWorker.js");
michael@0 7891 worker.onmessage = function (evt) {
michael@0 7892 let article = evt.data;
michael@0 7893
michael@0 7894 // Append URL to the article data. specIgnoringRef will ignore any hash
michael@0 7895 // in the URL.
michael@0 7896 if (article) {
michael@0 7897 article.url = uri.specIgnoringRef;
michael@0 7898 let flags = Ci.nsIDocumentEncoder.OutputSelectionOnly | Ci.nsIDocumentEncoder.OutputAbsoluteLinks;
michael@0 7899 article.title = Cc["@mozilla.org/parserutils;1"].getService(Ci.nsIParserUtils)
michael@0 7900 .convertToPlainText(article.title, flags, 0);
michael@0 7901 }
michael@0 7902
michael@0 7903 callback(article);
michael@0 7904 };
michael@0 7905
michael@0 7906 try {
michael@0 7907 worker.postMessage({
michael@0 7908 uri: {
michael@0 7909 spec: uri.spec,
michael@0 7910 host: uri.host,
michael@0 7911 prePath: uri.prePath,
michael@0 7912 scheme: uri.scheme,
michael@0 7913 pathBase: Services.io.newURI(".", null, uri).spec
michael@0 7914 },
michael@0 7915 doc: new XMLSerializer().serializeToString(doc)
michael@0 7916 });
michael@0 7917 } catch (e) {
michael@0 7918 dump("Reader: could not build Readability arguments: " + e);
michael@0 7919 callback(null);
michael@0 7920 }
michael@0 7921 },
michael@0 7922
michael@0 7923 _runCallbacksAndFinish: function Reader_runCallbacksAndFinish(request, result) {
michael@0 7924 delete this._requests[request.url];
michael@0 7925
michael@0 7926 request.callbacks.forEach(function(callback) {
michael@0 7927 callback(result);
michael@0 7928 });
michael@0 7929 },
michael@0 7930
michael@0 7931 _downloadDocument: function Reader_downloadDocument(url, callback) {
michael@0 7932 // We want to parse those arbitrary pages safely, outside the privileged
michael@0 7933 // context of chrome. We create a hidden browser element to fetch the
michael@0 7934 // loaded page's document object then discard the browser element.
michael@0 7935
michael@0 7936 let browser = document.createElement("browser");
michael@0 7937 browser.setAttribute("type", "content");
michael@0 7938 browser.setAttribute("collapsed", "true");
michael@0 7939 browser.setAttribute("disablehistory", "true");
michael@0 7940
michael@0 7941 document.documentElement.appendChild(browser);
michael@0 7942 browser.stop();
michael@0 7943
michael@0 7944 browser.webNavigation.allowAuth = false;
michael@0 7945 browser.webNavigation.allowImages = false;
michael@0 7946 browser.webNavigation.allowJavascript = false;
michael@0 7947 browser.webNavigation.allowMetaRedirects = true;
michael@0 7948 browser.webNavigation.allowPlugins = false;
michael@0 7949
michael@0 7950 browser.addEventListener("DOMContentLoaded", function (event) {
michael@0 7951 let doc = event.originalTarget;
michael@0 7952
michael@0 7953 // ignore on frames and other documents
michael@0 7954 if (doc != browser.contentDocument)
michael@0 7955 return;
michael@0 7956
michael@0 7957 this.log("Done loading: " + doc);
michael@0 7958 if (doc.location.href == "about:blank") {
michael@0 7959 callback(null);
michael@0 7960
michael@0 7961 // Request has finished with error, remove browser element
michael@0 7962 browser.parentNode.removeChild(browser);
michael@0 7963 return;
michael@0 7964 }
michael@0 7965
michael@0 7966 callback(doc);
michael@0 7967 }.bind(this));
michael@0 7968
michael@0 7969 browser.loadURIWithFlags(url, Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
michael@0 7970 null, null, null);
michael@0 7971
michael@0 7972 return browser;
michael@0 7973 },
michael@0 7974
michael@0 7975 _downloadAndParseDocument: function Reader_downloadAndParseDocument(url, request) {
michael@0 7976 try {
michael@0 7977 this.log("Needs to fetch page, creating request: " + url);
michael@0 7978
michael@0 7979 request.browser = this._downloadDocument(url, function(doc) {
michael@0 7980 this.log("Finished loading page: " + doc);
michael@0 7981
michael@0 7982 if (!doc) {
michael@0 7983 this.log("Error loading page");
michael@0 7984 this._runCallbacksAndFinish(request, null);
michael@0 7985 return;
michael@0 7986 }
michael@0 7987
michael@0 7988 this.log("Parsing response with Readability");
michael@0 7989
michael@0 7990 let uri = Services.io.newURI(url, null, null);
michael@0 7991 this._readerParse(uri, doc, function (article) {
michael@0 7992 // Delete reference to the browser element as we've finished parsing.
michael@0 7993 let browser = request.browser;
michael@0 7994 if (browser) {
michael@0 7995 browser.parentNode.removeChild(browser);
michael@0 7996 delete request.browser;
michael@0 7997 }
michael@0 7998
michael@0 7999 if (!article) {
michael@0 8000 this.log("Failed to parse page");
michael@0 8001 this._runCallbacksAndFinish(request, null);
michael@0 8002 return;
michael@0 8003 }
michael@0 8004
michael@0 8005 this.log("Parsing has been successful");
michael@0 8006
michael@0 8007 this._runCallbacksAndFinish(request, article);
michael@0 8008 }.bind(this));
michael@0 8009 }.bind(this));
michael@0 8010 } catch (e) {
michael@0 8011 this.log("Error downloading and parsing document: " + e);
michael@0 8012 this._runCallbacksAndFinish(request, null);
michael@0 8013 }
michael@0 8014 },
michael@0 8015
michael@0 8016 _getCacheDB: function Reader_getCacheDB(callback) {
michael@0 8017 if (this._cacheDB) {
michael@0 8018 callback(this._cacheDB);
michael@0 8019 return;
michael@0 8020 }
michael@0 8021
michael@0 8022 let request = window.indexedDB.open("about:reader", this.DB_VERSION);
michael@0 8023
michael@0 8024 request.onerror = function(event) {
michael@0 8025 this.log("Error connecting to the cache DB");
michael@0 8026 this._cacheDB = null;
michael@0 8027 callback(null);
michael@0 8028 }.bind(this);
michael@0 8029
michael@0 8030 request.onsuccess = function(event) {
michael@0 8031 this.log("Successfully connected to the cache DB");
michael@0 8032 this._cacheDB = event.target.result;
michael@0 8033 callback(this._cacheDB);
michael@0 8034 }.bind(this);
michael@0 8035
michael@0 8036 request.onupgradeneeded = function(event) {
michael@0 8037 this.log("Database schema upgrade from " +
michael@0 8038 event.oldVersion + " to " + event.newVersion);
michael@0 8039
michael@0 8040 let cacheDB = event.target.result;
michael@0 8041
michael@0 8042 // Create the articles object store
michael@0 8043 this.log("Creating articles object store");
michael@0 8044 cacheDB.createObjectStore("articles", { keyPath: "url" });
michael@0 8045
michael@0 8046 this.log("Database upgrade done: " + this.DB_VERSION);
michael@0 8047 }.bind(this);
michael@0 8048 }
michael@0 8049 };
michael@0 8050
michael@0 8051 var ExternalApps = {
michael@0 8052 _contextMenuId: null,
michael@0 8053
michael@0 8054 // extend _getLink to pickup html5 media links.
michael@0 8055 _getMediaLink: function(aElement) {
michael@0 8056 let uri = NativeWindow.contextmenus._getLink(aElement);
michael@0 8057 if (uri == null && aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && (aElement instanceof Ci.nsIDOMHTMLMediaElement)) {
michael@0 8058 try {
michael@0 8059 let mediaSrc = aElement.currentSrc || aElement.src;
michael@0 8060 uri = ContentAreaUtils.makeURI(mediaSrc, null, null);
michael@0 8061 } catch (e) {}
michael@0 8062 }
michael@0 8063 return uri;
michael@0 8064 },
michael@0 8065
michael@0 8066 init: function helper_init() {
michael@0 8067 this._contextMenuId = NativeWindow.contextmenus.add(function(aElement) {
michael@0 8068 let uri = null;
michael@0 8069 var node = aElement;
michael@0 8070 while (node && !uri) {
michael@0 8071 uri = ExternalApps._getMediaLink(node);
michael@0 8072 node = node.parentNode;
michael@0 8073 }
michael@0 8074 let apps = [];
michael@0 8075 if (uri)
michael@0 8076 apps = HelperApps.getAppsForUri(uri);
michael@0 8077
michael@0 8078 return apps.length == 1 ? Strings.browser.formatStringFromName("helperapps.openWithApp2", [apps[0].name], 1) :
michael@0 8079 Strings.browser.GetStringFromName("helperapps.openWithList2");
michael@0 8080 }, this.filter, this.openExternal);
michael@0 8081 },
michael@0 8082
michael@0 8083 uninit: function helper_uninit() {
michael@0 8084 if (this._contextMenuId !== null) {
michael@0 8085 NativeWindow.contextmenus.remove(this._contextMenuId);
michael@0 8086 }
michael@0 8087 this._contextMenuId = null;
michael@0 8088 },
michael@0 8089
michael@0 8090 filter: {
michael@0 8091 matches: function(aElement) {
michael@0 8092 let uri = ExternalApps._getMediaLink(aElement);
michael@0 8093 let apps = [];
michael@0 8094 if (uri) {
michael@0 8095 apps = HelperApps.getAppsForUri(uri);
michael@0 8096 }
michael@0 8097 return apps.length > 0;
michael@0 8098 }
michael@0 8099 },
michael@0 8100
michael@0 8101 openExternal: function(aElement) {
michael@0 8102 let uri = ExternalApps._getMediaLink(aElement);
michael@0 8103 HelperApps.launchUri(uri);
michael@0 8104 },
michael@0 8105
michael@0 8106 shouldCheckUri: function(uri) {
michael@0 8107 if (!(uri.schemeIs("http") || uri.schemeIs("https") || uri.schemeIs("file"))) {
michael@0 8108 return false;
michael@0 8109 }
michael@0 8110
michael@0 8111 return true;
michael@0 8112 },
michael@0 8113
michael@0 8114 updatePageAction: function updatePageAction(uri) {
michael@0 8115 HelperApps.getAppsForUri(uri, { filterHttp: true }, (apps) => {
michael@0 8116 this.clearPageAction();
michael@0 8117 if (apps.length > 0)
michael@0 8118 this._setUriForPageAction(uri, apps);
michael@0 8119 });
michael@0 8120 },
michael@0 8121
michael@0 8122 updatePageActionUri: function updatePageActionUri(uri) {
michael@0 8123 this._pageActionUri = uri;
michael@0 8124 },
michael@0 8125
michael@0 8126 _setUriForPageAction: function setUriForPageAction(uri, apps) {
michael@0 8127 this.updatePageActionUri(uri);
michael@0 8128
michael@0 8129 // If the pageaction is already added, simply update the URI to be launched when 'onclick' is triggered.
michael@0 8130 if (this._pageActionId != undefined)
michael@0 8131 return;
michael@0 8132
michael@0 8133 this._pageActionId = NativeWindow.pageactions.add({
michael@0 8134 title: Strings.browser.GetStringFromName("openInApp.pageAction"),
michael@0 8135 icon: "drawable://icon_openinapp",
michael@0 8136
michael@0 8137 clickCallback: () => {
michael@0 8138 // Create a relative timestamp for telemetry
michael@0 8139 let uptime = Date.now() - Services.startup.getStartupInfo().linkerInitialized;
michael@0 8140 UITelemetry.addEvent("launch.1", "pageaction", uptime, "helper");
michael@0 8141
michael@0 8142 if (apps.length > 1) {
michael@0 8143 // Use the HelperApps prompt here to filter out any Http handlers
michael@0 8144 HelperApps.prompt(apps, {
michael@0 8145 title: Strings.browser.GetStringFromName("openInApp.pageAction"),
michael@0 8146 buttons: [
michael@0 8147 Strings.browser.GetStringFromName("openInApp.ok"),
michael@0 8148 Strings.browser.GetStringFromName("openInApp.cancel")
michael@0 8149 ]
michael@0 8150 }, (result) => {
michael@0 8151 if (result.button != 0) {
michael@0 8152 return;
michael@0 8153 }
michael@0 8154 apps[result.icongrid0].launch(this._pageActionUri);
michael@0 8155 });
michael@0 8156 } else {
michael@0 8157 apps[0].launch(this._pageActionUri);
michael@0 8158 }
michael@0 8159 }
michael@0 8160 });
michael@0 8161 },
michael@0 8162
michael@0 8163 clearPageAction: function clearPageAction() {
michael@0 8164 if(!this._pageActionId)
michael@0 8165 return;
michael@0 8166
michael@0 8167 NativeWindow.pageactions.remove(this._pageActionId);
michael@0 8168 delete this._pageActionId;
michael@0 8169 },
michael@0 8170 };
michael@0 8171
michael@0 8172 var Distribution = {
michael@0 8173 // File used to store campaign data
michael@0 8174 _file: null,
michael@0 8175
michael@0 8176 init: function dc_init() {
michael@0 8177 Services.obs.addObserver(this, "Distribution:Set", false);
michael@0 8178 Services.obs.addObserver(this, "prefservice:after-app-defaults", false);
michael@0 8179 Services.obs.addObserver(this, "Campaign:Set", false);
michael@0 8180
michael@0 8181 // Look for file outside the APK:
michael@0 8182 // /data/data/org.mozilla.xxx/distribution.json
michael@0 8183 this._file = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
michael@0 8184 this._file.append("distribution.json");
michael@0 8185 this.readJSON(this._file, this.update);
michael@0 8186 },
michael@0 8187
michael@0 8188 uninit: function dc_uninit() {
michael@0 8189 Services.obs.removeObserver(this, "Distribution:Set");
michael@0 8190 Services.obs.removeObserver(this, "prefservice:after-app-defaults");
michael@0 8191 Services.obs.removeObserver(this, "Campaign:Set");
michael@0 8192 },
michael@0 8193
michael@0 8194 observe: function dc_observe(aSubject, aTopic, aData) {
michael@0 8195 switch (aTopic) {
michael@0 8196 case "Distribution:Set":
michael@0 8197 // Reload the default prefs so we can observe "prefservice:after-app-defaults"
michael@0 8198 Services.prefs.QueryInterface(Ci.nsIObserver).observe(null, "reload-default-prefs", null);
michael@0 8199 break;
michael@0 8200
michael@0 8201 case "prefservice:after-app-defaults":
michael@0 8202 this.getPrefs();
michael@0 8203 break;
michael@0 8204
michael@0 8205 case "Campaign:Set": {
michael@0 8206 // Update the prefs for this session
michael@0 8207 try {
michael@0 8208 this.update(JSON.parse(aData));
michael@0 8209 } catch (ex) {
michael@0 8210 Cu.reportError("Distribution: Could not parse JSON: " + ex);
michael@0 8211 return;
michael@0 8212 }
michael@0 8213
michael@0 8214 // Asynchronously copy the data to the file.
michael@0 8215 let array = new TextEncoder().encode(aData);
michael@0 8216 OS.File.writeAtomic(this._file.path, array, { tmpPath: this._file.path + ".tmp" });
michael@0 8217 break;
michael@0 8218 }
michael@0 8219 }
michael@0 8220 },
michael@0 8221
michael@0 8222 update: function dc_update(aData) {
michael@0 8223 // Force the distribution preferences on the default branch
michael@0 8224 let defaults = Services.prefs.getDefaultBranch(null);
michael@0 8225 defaults.setCharPref("distribution.id", aData.id);
michael@0 8226 defaults.setCharPref("distribution.version", aData.version);
michael@0 8227 },
michael@0 8228
michael@0 8229 getPrefs: function dc_getPrefs() {
michael@0 8230 // Get the distribution directory, and bail if it doesn't exist.
michael@0 8231 let file = FileUtils.getDir("XREAppDist", [], false);
michael@0 8232 if (!file.exists())
michael@0 8233 return;
michael@0 8234
michael@0 8235 file.append("preferences.json");
michael@0 8236 this.readJSON(file, this.applyPrefs);
michael@0 8237 },
michael@0 8238
michael@0 8239 applyPrefs: function dc_applyPrefs(aData) {
michael@0 8240 // Check for required Global preferences
michael@0 8241 let global = aData["Global"];
michael@0 8242 if (!(global && global["id"] && global["version"] && global["about"])) {
michael@0 8243 Cu.reportError("Distribution: missing or incomplete Global preferences");
michael@0 8244 return;
michael@0 8245 }
michael@0 8246
michael@0 8247 // Force the distribution preferences on the default branch
michael@0 8248 let defaults = Services.prefs.getDefaultBranch(null);
michael@0 8249 defaults.setCharPref("distribution.id", global["id"]);
michael@0 8250 defaults.setCharPref("distribution.version", global["version"]);
michael@0 8251
michael@0 8252 let locale = Services.prefs.getCharPref("general.useragent.locale");
michael@0 8253 let aboutString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
michael@0 8254 aboutString.data = global["about." + locale] || global["about"];
michael@0 8255 defaults.setComplexValue("distribution.about", Ci.nsISupportsString, aboutString);
michael@0 8256
michael@0 8257 let prefs = aData["Preferences"];
michael@0 8258 for (let key in prefs) {
michael@0 8259 try {
michael@0 8260 let value = prefs[key];
michael@0 8261 switch (typeof value) {
michael@0 8262 case "boolean":
michael@0 8263 defaults.setBoolPref(key, value);
michael@0 8264 break;
michael@0 8265 case "number":
michael@0 8266 defaults.setIntPref(key, value);
michael@0 8267 break;
michael@0 8268 case "string":
michael@0 8269 case "undefined":
michael@0 8270 defaults.setCharPref(key, value);
michael@0 8271 break;
michael@0 8272 }
michael@0 8273 } catch (e) { /* ignore bad prefs and move on */ }
michael@0 8274 }
michael@0 8275
michael@0 8276 // Apply a lightweight theme if necessary
michael@0 8277 if (prefs["lightweightThemes.isThemeSelected"])
michael@0 8278 Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
michael@0 8279
michael@0 8280 let localizedString = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString);
michael@0 8281 let localizeablePrefs = aData["LocalizablePreferences"];
michael@0 8282 for (let key in localizeablePrefs) {
michael@0 8283 try {
michael@0 8284 let value = localizeablePrefs[key];
michael@0 8285 value = value.replace("%LOCALE%", locale, "g");
michael@0 8286 localizedString.data = "data:text/plain," + key + "=" + value;
michael@0 8287 defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
michael@0 8288 } catch (e) { /* ignore bad prefs and move on */ }
michael@0 8289 }
michael@0 8290
michael@0 8291 let localizeablePrefsOverrides = aData["LocalizablePreferences." + locale];
michael@0 8292 for (let key in localizeablePrefsOverrides) {
michael@0 8293 try {
michael@0 8294 let value = localizeablePrefsOverrides[key];
michael@0 8295 localizedString.data = "data:text/plain," + key + "=" + value;
michael@0 8296 defaults.setComplexValue(key, Ci.nsIPrefLocalizedString, localizedString);
michael@0 8297 } catch (e) { /* ignore bad prefs and move on */ }
michael@0 8298 }
michael@0 8299
michael@0 8300 sendMessageToJava({ type: "Distribution:Set:OK" });
michael@0 8301 },
michael@0 8302
michael@0 8303 // aFile is an nsIFile
michael@0 8304 // aCallback takes the parsed JSON object as a parameter
michael@0 8305 readJSON: function dc_readJSON(aFile, aCallback) {
michael@0 8306 Task.spawn(function() {
michael@0 8307 let bytes = yield OS.File.read(aFile.path);
michael@0 8308 let raw = new TextDecoder().decode(bytes) || "";
michael@0 8309
michael@0 8310 try {
michael@0 8311 aCallback(JSON.parse(raw));
michael@0 8312 } catch (e) {
michael@0 8313 Cu.reportError("Distribution: Could not parse JSON: " + e);
michael@0 8314 }
michael@0 8315 }).then(null, function onError(reason) {
michael@0 8316 if (!(reason instanceof OS.File.Error && reason.becauseNoSuchFile)) {
michael@0 8317 Cu.reportError("Distribution: Could not read from " + aFile.leafName + " file");
michael@0 8318 }
michael@0 8319 });
michael@0 8320 }
michael@0 8321 };
michael@0 8322
michael@0 8323 var Tabs = {
michael@0 8324 _enableTabExpiration: false,
michael@0 8325 _domains: new Set(),
michael@0 8326
michael@0 8327 init: function() {
michael@0 8328 // On low-memory platforms, always allow tab expiration. On high-mem
michael@0 8329 // platforms, allow it to be turned on once we hit a low-mem situation.
michael@0 8330 if (BrowserApp.isOnLowMemoryPlatform) {
michael@0 8331 this._enableTabExpiration = true;
michael@0 8332 } else {
michael@0 8333 Services.obs.addObserver(this, "memory-pressure", false);
michael@0 8334 }
michael@0 8335
michael@0 8336 Services.obs.addObserver(this, "Session:Prefetch", false);
michael@0 8337
michael@0 8338 BrowserApp.deck.addEventListener("pageshow", this, false);
michael@0 8339 BrowserApp.deck.addEventListener("TabOpen", this, false);
michael@0 8340 },
michael@0 8341
michael@0 8342 uninit: function() {
michael@0 8343 if (!this._enableTabExpiration) {
michael@0 8344 // If _enableTabExpiration is true then we won't have this
michael@0 8345 // observer registered any more.
michael@0 8346 Services.obs.removeObserver(this, "memory-pressure");
michael@0 8347 }
michael@0 8348
michael@0 8349 Services.obs.removeObserver(this, "Session:Prefetch");
michael@0 8350
michael@0 8351 BrowserApp.deck.removeEventListener("pageshow", this);
michael@0 8352 BrowserApp.deck.removeEventListener("TabOpen", this);
michael@0 8353 },
michael@0 8354
michael@0 8355 observe: function(aSubject, aTopic, aData) {
michael@0 8356 switch (aTopic) {
michael@0 8357 case "memory-pressure":
michael@0 8358 if (aData != "heap-minimize") {
michael@0 8359 // We received a low-memory related notification. This will enable
michael@0 8360 // expirations.
michael@0 8361 this._enableTabExpiration = true;
michael@0 8362 Services.obs.removeObserver(this, "memory-pressure");
michael@0 8363 } else {
michael@0 8364 // Use "heap-minimize" as a trigger to expire the most stale tab.
michael@0 8365 this.expireLruTab();
michael@0 8366 }
michael@0 8367 break;
michael@0 8368 case "Session:Prefetch":
michael@0 8369 if (aData) {
michael@0 8370 let uri = Services.io.newURI(aData, null, null);
michael@0 8371 if (uri && !this._domains.has(uri.host)) {
michael@0 8372 try {
michael@0 8373 Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
michael@0 8374 this._domains.add(uri.host);
michael@0 8375 } catch (e) {}
michael@0 8376 }
michael@0 8377 }
michael@0 8378 break;
michael@0 8379 }
michael@0 8380 },
michael@0 8381
michael@0 8382 handleEvent: function(aEvent) {
michael@0 8383 switch (aEvent.type) {
michael@0 8384 case "pageshow":
michael@0 8385 // Clear the domain cache whenever a page get loaded into any browser.
michael@0 8386 this._domains.clear();
michael@0 8387 break;
michael@0 8388 case "TabOpen":
michael@0 8389 // Use opening a new tab as a trigger to expire the most stale tab.
michael@0 8390 this.expireLruTab();
michael@0 8391 break;
michael@0 8392 }
michael@0 8393 },
michael@0 8394
michael@0 8395 // Manage the most-recently-used list of tabs. Each tab has a timestamp
michael@0 8396 // associated with it that indicates when it was last touched.
michael@0 8397 expireLruTab: function() {
michael@0 8398 if (!this._enableTabExpiration) {
michael@0 8399 return false;
michael@0 8400 }
michael@0 8401 let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000;
michael@0 8402 if (expireTimeMs < 0) {
michael@0 8403 // This behaviour is disabled.
michael@0 8404 return false;
michael@0 8405 }
michael@0 8406 let tabs = BrowserApp.tabs;
michael@0 8407 let selected = BrowserApp.selectedTab;
michael@0 8408 let lruTab = null;
michael@0 8409 // Find the least recently used non-zombie tab.
michael@0 8410 for (let i = 0; i < tabs.length; i++) {
michael@0 8411 if (tabs[i] == selected || tabs[i].browser.__SS_restore) {
michael@0 8412 // This tab is selected or already a zombie, skip it.
michael@0 8413 continue;
michael@0 8414 }
michael@0 8415 if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) {
michael@0 8416 lruTab = tabs[i];
michael@0 8417 }
michael@0 8418 }
michael@0 8419 // If the tab was last touched more than browser.tabs.expireTime seconds ago,
michael@0 8420 // zombify it.
michael@0 8421 if (lruTab) {
michael@0 8422 let tabAgeMs = Date.now() - lruTab.lastTouchedAt;
michael@0 8423 if (tabAgeMs > expireTimeMs) {
michael@0 8424 MemoryObserver.zombify(lruTab);
michael@0 8425 Telemetry.addData("FENNEC_TAB_EXPIRED", tabAgeMs / 1000);
michael@0 8426 return true;
michael@0 8427 }
michael@0 8428 }
michael@0 8429 return false;
michael@0 8430 },
michael@0 8431
michael@0 8432 // For debugging
michael@0 8433 dump: function(aPrefix) {
michael@0 8434 let tabs = BrowserApp.tabs;
michael@0 8435 for (let i = 0; i < tabs.length; i++) {
michael@0 8436 dump(aPrefix + " | " + "Tab [" + tabs[i].browser.contentWindow.location.href + "]: lastTouchedAt:" + tabs[i].lastTouchedAt + ", zombie:" + tabs[i].browser.__SS_restore);
michael@0 8437 }
michael@0 8438 },
michael@0 8439 };
michael@0 8440
michael@0 8441 function ContextMenuItem(args) {
michael@0 8442 this.id = uuidgen.generateUUID().toString();
michael@0 8443 this.args = args;
michael@0 8444 }
michael@0 8445
michael@0 8446 ContextMenuItem.prototype = {
michael@0 8447 get order() {
michael@0 8448 return this.args.order || 0;
michael@0 8449 },
michael@0 8450
michael@0 8451 matches: function(elt, x, y) {
michael@0 8452 return this.args.selector.matches(elt, x, y);
michael@0 8453 },
michael@0 8454
michael@0 8455 callback: function(elt) {
michael@0 8456 this.args.callback(elt);
michael@0 8457 },
michael@0 8458
michael@0 8459 addVal: function(name, elt, defaultValue) {
michael@0 8460 if (!(name in this.args))
michael@0 8461 return defaultValue;
michael@0 8462
michael@0 8463 if (typeof this.args[name] == "function")
michael@0 8464 return this.args[name](elt);
michael@0 8465
michael@0 8466 return this.args[name];
michael@0 8467 },
michael@0 8468
michael@0 8469 getValue: function(elt) {
michael@0 8470 return {
michael@0 8471 id: this.id,
michael@0 8472 label: this.addVal("label", elt),
michael@0 8473 showAsActions: this.addVal("showAsActions", elt),
michael@0 8474 icon: this.addVal("icon", elt),
michael@0 8475 isGroup: this.addVal("isGroup", elt, false),
michael@0 8476 inGroup: this.addVal("inGroup", elt, false),
michael@0 8477 disabled: this.addVal("disabled", elt, false),
michael@0 8478 selected: this.addVal("selected", elt, false),
michael@0 8479 isParent: this.addVal("isParent", elt, false),
michael@0 8480 };
michael@0 8481 }
michael@0 8482 }
michael@0 8483
michael@0 8484 function HTMLContextMenuItem(elt, target) {
michael@0 8485 ContextMenuItem.call(this, { });
michael@0 8486
michael@0 8487 this.menuElementRef = Cu.getWeakReference(elt);
michael@0 8488 this.targetElementRef = Cu.getWeakReference(target);
michael@0 8489 }
michael@0 8490
michael@0 8491 HTMLContextMenuItem.prototype = Object.create(ContextMenuItem.prototype, {
michael@0 8492 order: {
michael@0 8493 value: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER
michael@0 8494 },
michael@0 8495
michael@0 8496 matches: {
michael@0 8497 value: function(target) {
michael@0 8498 let t = this.targetElementRef.get();
michael@0 8499 return t === target;
michael@0 8500 },
michael@0 8501 },
michael@0 8502
michael@0 8503 callback: {
michael@0 8504 value: function(target) {
michael@0 8505 let elt = this.menuElementRef.get();
michael@0 8506 if (!elt) {
michael@0 8507 return;
michael@0 8508 }
michael@0 8509
michael@0 8510 // If this is a menu item, show a new context menu with the submenu in it
michael@0 8511 if (elt instanceof Ci.nsIDOMHTMLMenuElement) {
michael@0 8512 try {
michael@0 8513 NativeWindow.contextmenus.menus = {};
michael@0 8514
michael@0 8515 let elt = this.menuElementRef.get();
michael@0 8516 let target = this.targetElementRef.get();
michael@0 8517 if (!elt) {
michael@0 8518 return;
michael@0 8519 }
michael@0 8520
michael@0 8521 var items = NativeWindow.contextmenus._getHTMLContextMenuItemsForMenu(elt, target);
michael@0 8522 // This menu will always only have one context, but we still make sure its the "right" one.
michael@0 8523 var context = NativeWindow.contextmenus._getContextType(target);
michael@0 8524 if (items.length > 0) {
michael@0 8525 NativeWindow.contextmenus._addMenuItems(items, context);
michael@0 8526 }
michael@0 8527
michael@0 8528 } catch(ex) {
michael@0 8529 Cu.reportError(ex);
michael@0 8530 }
michael@0 8531 } else {
michael@0 8532 // otherwise just click the menu item
michael@0 8533 elt.click();
michael@0 8534 }
michael@0 8535 },
michael@0 8536 },
michael@0 8537
michael@0 8538 getValue: {
michael@0 8539 value: function(target) {
michael@0 8540 let elt = this.menuElementRef.get();
michael@0 8541 if (!elt) {
michael@0 8542 return null;
michael@0 8543 }
michael@0 8544
michael@0 8545 if (elt.hasAttribute("hidden")) {
michael@0 8546 return null;
michael@0 8547 }
michael@0 8548
michael@0 8549 return {
michael@0 8550 id: this.id,
michael@0 8551 icon: elt.icon,
michael@0 8552 label: elt.label,
michael@0 8553 disabled: elt.disabled,
michael@0 8554 menu: elt instanceof Ci.nsIDOMHTMLMenuElement
michael@0 8555 };
michael@0 8556 }
michael@0 8557 },
michael@0 8558 });

mercurial