Wed, 31 Dec 2014 07:22:50 +0100
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 | }); |