#filter substitution // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; let Cc = Components.classes; let Ci = Components.interfaces; let Cu = Components.utils; let Cr = Components.results; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/AddonManager.jsm"); Cu.import("resource://gre/modules/FileUtils.jsm"); Cu.import("resource://gre/modules/JNI.jsm"); Cu.import('resource://gre/modules/Payment.jsm'); Cu.import("resource://gre/modules/PermissionPromptHelper.jsm"); Cu.import("resource://gre/modules/ContactService.jsm"); Cu.import("resource://gre/modules/NotificationDB.jsm"); Cu.import("resource://gre/modules/SpatialNavigation.jsm"); Cu.import("resource://gre/modules/UITelemetry.jsm"); #ifdef ACCESSIBILITY Cu.import("resource://gre/modules/accessibility/AccessFu.jsm"); #endif XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", "resource://gre/modules/PluralForm.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", "resource://gre/modules/devtools/dbg-server.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides", "resource://gre/modules/UserAgentOverrides.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", "resource://gre/modules/LoginManagerContent.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); #ifdef MOZ_SAFE_BROWSING XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", "resource://gre/modules/SafeBrowsing.jsm"); #endif XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer", "resource://gre/modules/Sanitizer.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "Prompt", "resource://gre/modules/Prompt.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "HelperApps", "resource://gre/modules/HelperApps.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", "resource://gre/modules/FormHistory.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator"); // Lazily-loaded browser scripts: [ ["SelectHelper", "chrome://browser/content/SelectHelper.js"], ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"], ["AboutReader", "chrome://browser/content/aboutReader.js"], ["WebAppRT", "chrome://browser/content/WebAppRT.js"], ["MasterPassword", "chrome://browser/content/MasterPassword.js"], ["PluginHelper", "chrome://browser/content/PluginHelper.js"], ["OfflineApps", "chrome://browser/content/OfflineApps.js"], ["Linkifier", "chrome://browser/content/Linkify.js"], ].forEach(function (aScript) { let [name, script] = aScript; XPCOMUtils.defineLazyGetter(window, name, function() { let sandbox = {}; Services.scriptloader.loadSubScript(script, sandbox); return sandbox[name]; }); }); // Lazily-loaded browser scripts that use observer notifcations: var LazyNotificationGetter = { observers: [], shutdown: function lng_shutdown() { this.observers.forEach(function(o) { Services.obs.removeObserver(o, o.notification); }); this.observers = []; } }; [ #ifdef MOZ_WEBRTC ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"], #endif ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"], ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"], ["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"], ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"], ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"], ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"], ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"], ].forEach(function (aScript) { let [name, notifications, script] = aScript; XPCOMUtils.defineLazyGetter(window, name, function() { let sandbox = {}; Services.scriptloader.loadSubScript(script, sandbox); return sandbox[name]; }); notifications.forEach(function (aNotification) { let o = { notification: aNotification, observe: function(s, t, d) { window[name].observe(s, t, d); } }; Services.obs.addObserver(o, aNotification, false); LazyNotificationGetter.observers.push(o); }); }); XPCOMUtils.defineLazyServiceGetter(this, "Haptic", "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback"); XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils", "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils"); XPCOMUtils.defineLazyServiceGetter(window, "URIFixup", "@mozilla.org/docshell/urifixup;1", "nsIURIFixup"); #ifdef MOZ_WEBRTC XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService"); #endif const kStateActive = 0x00000001; // :active pseudoclass for elements const kXLinkNamespace = "http://www.w3.org/1999/xlink"; const kDefaultCSSViewportWidth = 980; const kDefaultCSSViewportHeight = 480; const kViewportRemeasureThrottle = 500; const kDoNotTrackPrefState = Object.freeze({ NO_PREF: "0", DISALLOW_TRACKING: "1", ALLOW_TRACKING: "2", }); function dump(a) { Services.console.logStringMessage(a); } function sendMessageToJava(aMessage) { return Services.androidBridge.handleGeckoMessage(JSON.stringify(aMessage)); } function doChangeMaxLineBoxWidth(aWidth) { gReflowPending = null; let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); let docShell = webNav.QueryInterface(Ci.nsIDocShell); let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); let range = null; if (BrowserApp.selectedTab._mReflozPoint) { range = BrowserApp.selectedTab._mReflozPoint.range; } docViewer.changeMaxLineBoxWidth(aWidth); if (range) { BrowserEventHandler._zoomInAndSnapToRange(range); } } function fuzzyEquals(a, b) { return (Math.abs(a - b) < 1e-6); } /** * Convert a font size to CSS pixels (px) from twentieiths-of-a-point * (twips). */ function convertFromTwipsToPx(aSize) { return aSize/240 * 16.0; } #ifdef MOZ_CRASHREPORTER Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", "@mozilla.org/xre/app-info;1", "nsICrashReporter"); #endif XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() { let ContentAreaUtils = {}; Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils); return ContentAreaUtils; }); XPCOMUtils.defineLazyModuleGetter(this, "Rect", "resource://gre/modules/Geometry.jsm"); function resolveGeckoURI(aURI) { if (!aURI) throw "Can't resolve an empty uri"; if (aURI.startsWith("chrome://")) { let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]); return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec; } else if (aURI.startsWith("resource://")) { let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler); return handler.resolveURI(Services.io.newURI(aURI, null, null)); } return aURI; } function shouldShowProgress(url) { return (url != "about:home" && !url.startsWith("about:reader")); } /** * Cache of commonly used string bundles. */ var Strings = {}; [ ["brand", "chrome://branding/locale/brand.properties"], ["browser", "chrome://browser/locale/browser.properties"], ["charset", "chrome://global/locale/charsetTitles.properties"] ].forEach(function (aStringBundle) { let [name, bundle] = aStringBundle; XPCOMUtils.defineLazyGetter(Strings, name, function() { return Services.strings.createBundle(bundle); }); }); const kFormHelperModeDisabled = 0; const kFormHelperModeEnabled = 1; const kFormHelperModeDynamic = 2; // disabled on tablets var BrowserApp = { _tabs: [], _selectedTab: null, _prefObservers: [], isGuest: false, get isTablet() { let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); delete this.isTablet; return this.isTablet = sysInfo.get("tablet"); }, get isOnLowMemoryPlatform() { let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory); delete this.isOnLowMemoryPlatform; return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform(); }, deck: null, startup: function startup() { window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess(); dump("zerdatime " + Date.now() + " - browser chrome startup finished."); this.deck = document.getElementById("browsers"); BrowserEventHandler.init(); ViewportHandler.init(); Services.androidBridge.browserApp = this; Services.obs.addObserver(this, "Locale:Changed", false); Services.obs.addObserver(this, "Tab:Load", false); Services.obs.addObserver(this, "Tab:Selected", false); Services.obs.addObserver(this, "Tab:Closed", false); Services.obs.addObserver(this, "Session:Back", false); Services.obs.addObserver(this, "Session:ShowHistory", false); Services.obs.addObserver(this, "Session:Forward", false); Services.obs.addObserver(this, "Session:Reload", false); Services.obs.addObserver(this, "Session:Stop", false); Services.obs.addObserver(this, "SaveAs:PDF", false); Services.obs.addObserver(this, "Browser:Quit", false); Services.obs.addObserver(this, "Preferences:Set", false); Services.obs.addObserver(this, "ScrollTo:FocusedInput", false); Services.obs.addObserver(this, "Sanitize:ClearData", false); Services.obs.addObserver(this, "FullScreen:Exit", false); Services.obs.addObserver(this, "Viewport:Change", false); Services.obs.addObserver(this, "Viewport:Flush", false); Services.obs.addObserver(this, "Viewport:FixedMarginsChanged", false); Services.obs.addObserver(this, "Passwords:Init", false); Services.obs.addObserver(this, "FormHistory:Init", false); Services.obs.addObserver(this, "gather-telemetry", false); Services.obs.addObserver(this, "keyword-search", false); Services.obs.addObserver(this, "sessionstore-state-purge-complete", false); function showFullScreenWarning() { NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short"); } window.addEventListener("fullscreen", function() { sendMessageToJava({ type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide" }); }, false); window.addEventListener("mozfullscreenchange", function() { sendMessageToJava({ type: document.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop" }); if (document.mozFullScreen) showFullScreenWarning(); }, false); // When a restricted key is pressed in DOM full-screen mode, we should display // the "Press ESC to exit" warning message. window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true); NativeWindow.init(); LightWeightThemeWebInstaller.init(); Downloads.init(); FormAssistant.init(); IndexedDB.init(); HealthReportStatusListener.init(); XPInstallObserver.init(); CharacterEncoding.init(); ActivityObserver.init(); WebappsUI.init(); RemoteDebugger.init(); Reader.init(); UserAgentOverrides.init(); DesktopUserAgent.init(); Distribution.init(); Tabs.init(); UITelemetry.init(); #ifdef ACCESSIBILITY AccessFu.attach(window); #endif // Init LoginManager Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); let url = null; let pinned = false; if ("arguments" in window) { if (window.arguments[0]) url = window.arguments[0]; if (window.arguments[1]) gScreenWidth = window.arguments[1]; if (window.arguments[2]) gScreenHeight = window.arguments[2]; if (window.arguments[3]) pinned = window.arguments[3]; if (window.arguments[4]) this.isGuest = window.arguments[4]; } let status = this.startupStatus(); if (pinned) { WebAppRT.init(status, url, function(aUrl) { BrowserApp.addTab(aUrl); }); } else { SearchEngines.init(); this.initContextMenu(); } // The order that context menu items are added is important // Make sure the "Open in App" context menu item appears at the bottom of the list ExternalApps.init(); // XXX maybe we don't do this if the launch was kicked off from external Services.io.offline = false; // Broadcast a UIReady message so add-ons know we are finished with startup let event = document.createEvent("Events"); event.initEvent("UIReady", true, false); window.dispatchEvent(event); if (status) this.onAppUpdated(); // Store the low-precision buffer pref this.gUseLowPrecision = Services.prefs.getBoolPref("layers.low-precision-buffer"); // notify java that gecko has loaded sendMessageToJava({ type: "Gecko:Ready" }); #ifdef MOZ_SAFE_BROWSING // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008. setTimeout(function() { SafeBrowsing.init(); }, 5000); #endif }, startupStatus: function() { let savedmstone = null; try { savedmstone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone"); } catch (e) { } #expand let ourmstone = "__MOZ_APP_VERSION__"; if (ourmstone != savedmstone) { Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourmstone); return savedmstone ? "upgrade" : "new"; } return ""; }, /** * Pass this a locale string, such as "fr" or "es_ES". */ setLocale: function (locale) { console.log("browser.js: requesting locale set: " + locale); sendMessageToJava({ type: "Locale:Set", locale: locale }); }, initContextMenu: function ba_initContextMenu() { // TODO: These should eventually move into more appropriate classes NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"), NativeWindow.contextmenus.linkOpenableNonPrivateContext, function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id }); let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened"); let label = PluralForm.get(1, newtabStrings).replace("#1", 1); NativeWindow.toast.show(label, "short"); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInPrivateTab"), NativeWindow.contextmenus.linkOpenableContext, function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true }); let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened"); let label = PluralForm.get(1, newtabStrings).replace("#1", 1); NativeWindow.toast.show(label, "short"); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyLink"), NativeWindow.contextmenus.linkCopyableContext, function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); NativeWindow.contextmenus._copyStringToDefaultClipboard(url); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyEmailAddress"), NativeWindow.contextmenus.emailLinkContext, function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); let emailAddr = NativeWindow.contextmenus._stripScheme(url); NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyPhoneNumber"), NativeWindow.contextmenus.phoneNumberLinkContext, function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); let phoneNumber = NativeWindow.contextmenus._stripScheme(url); NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.shareLink"), NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkShareableContext), function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); let title = aTarget.textContent || aTarget.title; NativeWindow.contextmenus._shareStringWithDefault(url, title); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.shareEmailAddress"), NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext), function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); let emailAddr = NativeWindow.contextmenus._stripScheme(url); let title = aTarget.textContent || aTarget.title; NativeWindow.contextmenus._shareStringWithDefault(emailAddr, title); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.sharePhoneNumber"), NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext), function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); let phoneNumber = NativeWindow.contextmenus._stripScheme(url); let title = aTarget.textContent || aTarget.title; NativeWindow.contextmenus._shareStringWithDefault(phoneNumber, title); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"), NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext), function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); sendMessageToJava({ type: "Contact:Add", email: url }); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"), NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext), function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); sendMessageToJava({ type: "Contact:Add", phone: url }); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"), NativeWindow.contextmenus.linkBookmarkableContext, function(aTarget) { let url = NativeWindow.contextmenus._getLinkURL(aTarget); let title = aTarget.textContent || aTarget.title || url; sendMessageToJava({ type: "Bookmark:Insert", url: url, title: title }); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.playMedia"), NativeWindow.contextmenus.mediaContext("media-paused"), function(aTarget) { aTarget.play(); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.pauseMedia"), NativeWindow.contextmenus.mediaContext("media-playing"), function(aTarget) { aTarget.pause(); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.showControls2"), NativeWindow.contextmenus.mediaContext("media-hidingcontrols"), function(aTarget) { aTarget.setAttribute("controls", true); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.shareMedia"), NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.SelectorContext("video")), function(aTarget) { let url = (aTarget.currentSrc || aTarget.src); let title = aTarget.textContent || aTarget.title; NativeWindow.contextmenus._shareStringWithDefault(url, title); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.fullScreen"), NativeWindow.contextmenus.SelectorContext("video:not(:-moz-full-screen)"), function(aTarget) { aTarget.mozRequestFullScreen(); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.mute"), NativeWindow.contextmenus.mediaContext("media-unmuted"), function(aTarget) { aTarget.muted = true; }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"), NativeWindow.contextmenus.mediaContext("media-muted"), function(aTarget) { aTarget.muted = false; }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyImageLocation"), NativeWindow.contextmenus.imageLocationCopyableContext, function(aTarget) { let url = aTarget.src; NativeWindow.contextmenus._copyStringToDefaultClipboard(url); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.shareImage"), NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext), function(aTarget) { let doc = aTarget.ownerDocument; let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools) .getImgCacheForDocument(doc); let props = imageCache.findEntryProperties(aTarget.currentURI, doc.characterSet); let src = aTarget.src; let type = ""; try { type = String(props.get("type", Ci.nsISupportsCString)); } catch(ex) { type = ""; } sendMessageToJava({ type: "Share:Image", url: src, mime: type, }); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.saveImage"), NativeWindow.contextmenus.imageSaveableContext, function(aTarget) { ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle", false, true, aTarget.ownerDocument.documentURIObject, aTarget.ownerDocument); }); NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.setImageAs"), NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext), function(aTarget) { let src = aTarget.src; sendMessageToJava({ type: "Image:SetAs", url: src }); }); NativeWindow.contextmenus.add( function(aTarget) { if (aTarget instanceof HTMLVideoElement) { // If a video element is zero width or height, its essentially // an HTMLAudioElement. if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 ) return Strings.browser.GetStringFromName("contextmenu.saveAudio"); return Strings.browser.GetStringFromName("contextmenu.saveVideo"); } else if (aTarget instanceof HTMLAudioElement) { return Strings.browser.GetStringFromName("contextmenu.saveAudio"); } return Strings.browser.GetStringFromName("contextmenu.saveVideo"); }, NativeWindow.contextmenus.mediaSaveableContext, function(aTarget) { let url = aTarget.currentSrc || aTarget.src; let filePickerTitleKey = (aTarget instanceof HTMLVideoElement && (aTarget.videoWidth != 0 && aTarget.videoHeight != 0)) ? "SaveVideoTitle" : "SaveAudioTitle"; // Skipped trying to pull MIME type out of cache for now ContentAreaUtils.internalSave(url, null, null, null, null, false, filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject, aTarget.ownerDocument, true, null); }); }, onAppUpdated: function() { // initialize the form history and passwords databases on upgrades Services.obs.notifyObservers(null, "FormHistory:Init", ""); Services.obs.notifyObservers(null, "Passwords:Init", ""); // Migrate user-set "plugins.click_to_play" pref. See bug 884694. // Because the default value is true, a user-set pref means that the pref was set to false. if (Services.prefs.prefHasUserValue("plugins.click_to_play")) { Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED); Services.prefs.clearUserPref("plugins.click_to_play"); } }, shutdown: function shutdown() { NativeWindow.uninit(); LightWeightThemeWebInstaller.uninit(); FormAssistant.uninit(); IndexedDB.uninit(); ViewportHandler.uninit(); XPInstallObserver.uninit(); HealthReportStatusListener.uninit(); CharacterEncoding.uninit(); SearchEngines.uninit(); WebappsUI.uninit(); RemoteDebugger.uninit(); Reader.uninit(); UserAgentOverrides.uninit(); DesktopUserAgent.uninit(); ExternalApps.uninit(); Distribution.uninit(); Tabs.uninit(); }, // This function returns false during periods where the browser displayed document is // different from the browser content document, so user actions and some kinds of viewport // updates should be ignored. This period starts when we start loading a new page or // switch tabs, and ends when the new browser content document has been drawn and handed // off to the compositor. isBrowserContentDocumentDisplayed: function() { try { if (!Services.androidBridge.isContentDocumentDisplayed()) return false; } catch (e) { return false; } let tab = this.selectedTab; if (!tab) return false; return tab.contentDocumentIsDisplayed; }, contentDocumentChanged: function() { window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true; Services.androidBridge.contentDocumentChanged(); }, get tabs() { return this._tabs; }, get selectedTab() { return this._selectedTab; }, set selectedTab(aTab) { if (this._selectedTab == aTab) return; if (this._selectedTab) { Tabs.touch(this._selectedTab); this._selectedTab.setActive(false); } this._selectedTab = aTab; if (!aTab) return; Tabs.touch(aTab); aTab.setActive(true); aTab.setResolution(aTab._zoom, true); this.contentDocumentChanged(); this.deck.selectedPanel = aTab.browser; // Focus the browser so that things like selection will be styled correctly. aTab.browser.focus(); }, get selectedBrowser() { if (this._selectedTab) return this._selectedTab.browser; return null; }, getTabForId: function getTabForId(aId) { let tabs = this._tabs; for (let i=0; i < tabs.length; i++) { if (tabs[i].id == aId) return tabs[i]; } return null; }, getTabForBrowser: function getTabForBrowser(aBrowser) { let tabs = this._tabs; for (let i = 0; i < tabs.length; i++) { if (tabs[i].browser == aBrowser) return tabs[i]; } return null; }, getTabForWindow: function getTabForWindow(aWindow) { let tabs = this._tabs; for (let i = 0; i < tabs.length; i++) { if (tabs[i].browser.contentWindow == aWindow) return tabs[i]; } return null; }, getBrowserForWindow: function getBrowserForWindow(aWindow) { let tabs = this._tabs; for (let i = 0; i < tabs.length; i++) { if (tabs[i].browser.contentWindow == aWindow) return tabs[i].browser; } return null; }, getBrowserForDocument: function getBrowserForDocument(aDocument) { let tabs = this._tabs; for (let i = 0; i < tabs.length; i++) { if (tabs[i].browser.contentDocument == aDocument) return tabs[i].browser; } return null; }, loadURI: function loadURI(aURI, aBrowser, aParams) { aBrowser = aBrowser || this.selectedBrowser; if (!aBrowser) return; aParams = aParams || {}; let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE; let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null; let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null; let charset = "charset" in aParams ? aParams.charset : null; let tab = this.getTabForBrowser(aBrowser); if (tab) { if (!tab.aboutHomePage) tab.aboutHomePage = ("aboutHomePage" in aParams) ? aParams.aboutHomePage : ""; if ("userSearch" in aParams) tab.userSearch = aParams.userSearch; } try { aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData); } catch(e) { if (tab) { let message = { type: "Content:LoadError", tabID: tab.id }; sendMessageToJava(message); dump("Handled load error: " + e) } } }, addTab: function addTab(aURI, aParams) { aParams = aParams || {}; let newTab = new Tab(aURI, aParams); this._tabs.push(newTab); let selected = "selected" in aParams ? aParams.selected : true; if (selected) this.selectedTab = newTab; let pinned = "pinned" in aParams ? aParams.pinned : false; if (pinned) { let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); ss.setTabValue(newTab, "appOrigin", aURI); } let evt = document.createEvent("UIEvents"); evt.initUIEvent("TabOpen", true, false, window, null); newTab.browser.dispatchEvent(evt); Tabs.expireLruTab(); return newTab; }, // Use this method to close a tab from JS. This method sends a message // to Java to close the tab in the Java UI (we'll get a Tab:Closed message // back from Java when that happens). closeTab: function closeTab(aTab) { if (!aTab) { Cu.reportError("Error trying to close tab (tab doesn't exist)"); return; } let message = { type: "Tab:Close", tabID: aTab.id }; sendMessageToJava(message); }, // Calling this will update the state in BrowserApp after a tab has been // closed in the Java UI. _handleTabClosed: function _handleTabClosed(aTab) { if (aTab == this.selectedTab) this.selectedTab = null; let evt = document.createEvent("UIEvents"); evt.initUIEvent("TabClose", true, false, window, null); aTab.browser.dispatchEvent(evt); aTab.destroy(); this._tabs.splice(this._tabs.indexOf(aTab), 1); }, // Use this method to select a tab from JS. This method sends a message // to Java to select the tab in the Java UI (we'll get a Tab:Selected message // back from Java when that happens). selectTab: function selectTab(aTab) { if (!aTab) { Cu.reportError("Error trying to select tab (tab doesn't exist)"); return; } // There's nothing to do if the tab is already selected if (aTab == this.selectedTab) return; let message = { type: "Tab:Select", tabID: aTab.id }; sendMessageToJava(message); }, /** * Gets an open tab with the given URL. * * @param aURL URL to look for * @return the tab with the given URL, or null if no such tab exists */ getTabWithURL: function getTabWithURL(aURL) { let uri = Services.io.newURI(aURL, null, null); for (let i = 0; i < this._tabs.length; ++i) { let tab = this._tabs[i]; if (tab.browser.currentURI.equals(uri)) { return tab; } } return null; }, /** * If a tab with the given URL already exists, that tab is selected. * Otherwise, a new tab is opened with the given URL. * * @param aURL URL to open */ selectOrOpenTab: function selectOrOpenTab(aURL) { let tab = this.getTabWithURL(aURL); if (tab == null) { this.addTab(aURL); } else { this.selectTab(tab); } }, // This method updates the state in BrowserApp after a tab has been selected // in the Java UI. _handleTabSelected: function _handleTabSelected(aTab) { this.selectedTab = aTab; let evt = document.createEvent("UIEvents"); evt.initUIEvent("TabSelect", true, false, window, null); aTab.browser.dispatchEvent(evt); }, quit: function quit() { // Figure out if there's at least one other browser window around. let lastBrowser = true; let e = Services.wm.getEnumerator("navigator:browser"); while (e.hasMoreElements() && lastBrowser) { let win = e.getNext(); if (!win.closed && win != window) lastBrowser = false; } if (lastBrowser) { // Let everyone know we are closing the last browser window let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); Services.obs.notifyObservers(closingCanceled, "browser-lastwindow-close-requested", null); if (closingCanceled.data) return; Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null); } window.QueryInterface(Ci.nsIDOMChromeWindow).minimize(); window.close(); }, saveAsPDF: function saveAsPDF(aBrowser) { // Create the final destination file location let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null); fileName = fileName.trim() + ".pdf"; let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); let downloadsDir = dm.defaultDownloadsDirectory; let file = downloadsDir.clone(); file.append(fileName); file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8)); let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings; printSettings.printSilent = true; printSettings.showPrintProgress = false; printSettings.printBGImages = true; printSettings.printBGColors = true; printSettings.printToFile = true; printSettings.toFileName = file.path; printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs; printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; //XXX we probably need a preference here, the header can be useful printSettings.footerStrCenter = ""; printSettings.footerStrLeft = ""; printSettings.footerStrRight = ""; printSettings.headerStrCenter = ""; printSettings.headerStrLeft = ""; printSettings.headerStrRight = ""; // Create a valid mimeInfo for the PDF let ms = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); let mimeInfo = ms.getFromTypeAndExtension("application/pdf", "pdf"); let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebBrowserPrint); let cancelable = { cancel: function (aReason) { webBrowserPrint.cancel(); } } let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aBrowser.contentWindow); let download = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD, aBrowser.currentURI, Services.io.newFileURI(file), "", mimeInfo, Date.now() * 1000, null, cancelable, isPrivate); webBrowserPrint.print(printSettings, download); }, notifyPrefObservers: function(aPref) { this._prefObservers[aPref].forEach(function(aRequestId) { this.getPreferences(aRequestId, [aPref], 1); }, this); }, handlePreferencesRequest: function handlePreferencesRequest(aRequestId, aPrefNames, aListen) { let prefs = []; for (let prefName of aPrefNames) { let pref = { name: prefName, type: "", value: null }; if (aListen) { if (this._prefObservers[prefName]) this._prefObservers[prefName].push(aRequestId); else this._prefObservers[prefName] = [ aRequestId ]; Services.prefs.addObserver(prefName, this, false); } // These pref names are not "real" pref names. // They are used in the setting menu, // and these are passed when initializing the setting menu. switch (prefName) { // The plugin pref is actually two separate prefs, so // we need to handle it differently case "plugin.enable": pref.type = "string";// Use a string type for java's ListPreference pref.value = PluginHelper.getPluginPreference(); prefs.push(pref); continue; // Handle master password case "privacy.masterpassword.enabled": pref.type = "bool"; pref.value = MasterPassword.enabled; prefs.push(pref); continue; // Handle do-not-track preference case "privacy.donottrackheader": pref.type = "string"; let enableDNT = Services.prefs.getBoolPref("privacy.donottrackheader.enabled"); if (!enableDNT) { pref.value = kDoNotTrackPrefState.NO_PREF; } else { let dntState = Services.prefs.getIntPref("privacy.donottrackheader.value"); pref.value = (dntState === 0) ? kDoNotTrackPrefState.ALLOW_TRACKING : kDoNotTrackPrefState.DISALLOW_TRACKING; } prefs.push(pref); continue; #ifdef MOZ_CRASHREPORTER // Crash reporter submit pref must be fetched from nsICrashReporter service. case "datareporting.crashreporter.submitEnabled": pref.type = "bool"; pref.value = CrashReporter.submitReports; prefs.push(pref); continue; #endif } // Pref name translation. switch (prefName) { #ifdef MOZ_TELEMETRY_REPORTING // Telemetry pref differs based on build. case Telemetry.SHARED_PREF_TELEMETRY_ENABLED: #ifdef MOZ_TELEMETRY_ON_BY_DEFAULT prefName = "toolkit.telemetry.enabledPreRelease"; #else prefName = "toolkit.telemetry.enabled"; #endif break; #endif } try { switch (Services.prefs.getPrefType(prefName)) { case Ci.nsIPrefBranch.PREF_BOOL: pref.type = "bool"; pref.value = Services.prefs.getBoolPref(prefName); break; case Ci.nsIPrefBranch.PREF_INT: pref.type = "int"; pref.value = Services.prefs.getIntPref(prefName); break; case Ci.nsIPrefBranch.PREF_STRING: default: pref.type = "string"; try { // Try in case it's a localized string (will throw an exception if not) pref.value = Services.prefs.getComplexValue(prefName, Ci.nsIPrefLocalizedString).data; } catch (e) { pref.value = Services.prefs.getCharPref(prefName); } break; } } catch (e) { dump("Error reading pref [" + prefName + "]: " + e); // preference does not exist; do not send it continue; } // Some Gecko preferences use integers or strings to reference // state instead of directly representing the value. // Since the Java UI uses the type to determine which ui elements // to show and how to handle them, we need to normalize these // preferences to the correct type. switch (prefName) { // (string) index for determining which multiple choice value to display. case "browser.chrome.titlebarMode": case "network.cookie.cookieBehavior": case "font.size.inflation.minTwips": pref.type = "string"; pref.value = pref.value.toString(); break; } prefs.push(pref); } sendMessageToJava({ type: "Preferences:Data", requestId: aRequestId, // opaque request identifier, can be any string/int/whatever preferences: prefs }); }, setPreferences: function setPreferences(aPref) { let json = JSON.parse(aPref); switch (json.name) { // The plugin pref is actually two separate prefs, so // we need to handle it differently case "plugin.enable": PluginHelper.setPluginPreference(json.value); return; // MasterPassword pref is not real, we just need take action and leave case "privacy.masterpassword.enabled": if (MasterPassword.enabled) MasterPassword.removePassword(json.value); else MasterPassword.setPassword(json.value); return; // "privacy.donottrackheader" is not "real" pref name, it's used in the setting menu. case "privacy.donottrackheader": switch (json.value) { // Don't tell anything about tracking me case kDoNotTrackPrefState.NO_PREF: Services.prefs.setBoolPref("privacy.donottrackheader.enabled", false); Services.prefs.clearUserPref("privacy.donottrackheader.value"); break; // Accept tracking me case kDoNotTrackPrefState.ALLOW_TRACKING: Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true); Services.prefs.setIntPref("privacy.donottrackheader.value", 0); break; // Not accept tracking me case kDoNotTrackPrefState.DISALLOW_TRACKING: Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true); Services.prefs.setIntPref("privacy.donottrackheader.value", 1); break; } return; // Enabling or disabling suggestions will prevent future prompts case SearchEngines.PREF_SUGGEST_ENABLED: Services.prefs.setBoolPref(SearchEngines.PREF_SUGGEST_PROMPTED, true); break; #ifdef MOZ_CRASHREPORTER // Crash reporter preference is in a service; set and return. case "datareporting.crashreporter.submitEnabled": CrashReporter.submitReports = json.value; return; #endif // When sending to Java, we normalized special preferences that use // integers and strings to represent booleans. Here, we convert them back // to their actual types so we can store them. case "browser.chrome.titlebarMode": case "network.cookie.cookieBehavior": case "font.size.inflation.minTwips": json.type = "int"; json.value = parseInt(json.value); break; } // Pref name translation. switch (json.name) { #ifdef MOZ_TELEMETRY_REPORTING // Telemetry pref differs based on build. case Telemetry.SHARED_PREF_TELEMETRY_ENABLED: #ifdef MOZ_TELEMETRY_ON_BY_DEFAULT json.name = "toolkit.telemetry.enabledPreRelease"; #else json.name = "toolkit.telemetry.enabled"; #endif break; #endif } switch (json.type) { case "bool": Services.prefs.setBoolPref(json.name, json.value); break; case "int": Services.prefs.setIntPref(json.name, json.value); break; default: { let pref = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString); pref.data = json.value; Services.prefs.setComplexValue(json.name, Ci.nsISupportsString, pref); break; } } }, sanitize: function (aItems) { let json = JSON.parse(aItems); let success = true; for (let key in json) { if (!json[key]) continue; try { switch (key) { case "history_downloads": Sanitizer.clearItem("history"); // If we're also removing downloaded files, don't clear the // download history yet since it will be handled when the files are // removed. if (!json["downloadFiles"]) { Sanitizer.clearItem("downloads"); } break; case "cookies_sessions": Sanitizer.clearItem("cookies"); Sanitizer.clearItem("sessions"); break; default: Sanitizer.clearItem(key); } } catch (e) { dump("sanitize error: " + e); success = false; } } sendMessageToJava({ type: "Sanitize:Finished", success: success }); }, getFocusedInput: function(aBrowser, aOnlyInputElements = false) { if (!aBrowser) return null; let doc = aBrowser.contentDocument; if (!doc) return null; let focused = doc.activeElement; while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) { doc = focused.contentDocument; focused = doc.activeElement; } if (focused instanceof HTMLInputElement && focused.mozIsTextField(false)) return focused; if (aOnlyInputElements) return null; if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) { if (focused instanceof HTMLBodyElement) { // we are putting focus into a contentEditable frame. scroll the frame into // view instead of the contentEditable document contained within, because that // results in a better user experience focused = focused.ownerDocument.defaultView.frameElement; } return focused; } return null; }, scrollToFocusedInput: function(aBrowser, aAllowZoom = true) { let formHelperMode = Services.prefs.getIntPref("formhelper.mode"); if (formHelperMode == kFormHelperModeDisabled) return; let focused = this.getFocusedInput(aBrowser); if (focused) { let shouldZoom = Services.prefs.getBoolPref("formhelper.autozoom"); if (formHelperMode == kFormHelperModeDynamic && this.isTablet) shouldZoom = false; // _zoomToElement will handle not sending any message if this input is already mostly filling the screen BrowserEventHandler._zoomToElement(focused, -1, false, aAllowZoom && shouldZoom && !ViewportHandler.getViewportMetadata(aBrowser.contentWindow).isSpecified); } }, observe: function(aSubject, aTopic, aData) { let browser = this.selectedBrowser; switch (aTopic) { case "Session:Back": browser.goBack(); break; case "Session:Forward": browser.goForward(); break; case "Session:Reload": { let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; // Check to see if this is a message to enable/disable mixed content blocking. if (aData) { let allowMixedContent = JSON.parse(aData).allowMixedContent; if (allowMixedContent) { // Set a flag to disable mixed content blocking. flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT; } else { // Set mixedContentChannel to null to re-enable mixed content blocking. let docShell = browser.webNavigation.QueryInterface(Ci.nsIDocShell); docShell.mixedContentChannel = null; } } // Try to use the session history to reload so that framesets are // handled properly. If the window has no session history, fall back // to using the web navigation's reload method. let webNav = browser.webNavigation; try { let sh = webNav.sessionHistory; if (sh) webNav = sh.QueryInterface(Ci.nsIWebNavigation); } catch (e) {} webNav.reload(flags); break; } case "Session:Stop": browser.stop(); break; case "Session:ShowHistory": { let data = JSON.parse(aData); this.showHistory(data.fromIndex, data.toIndex, data.selIndex); break; } case "Tab:Load": { let data = JSON.parse(aData); // Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from // inheriting the currently loaded document's principal. let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; if (data.userEntered) { flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER; } let delayLoad = ("delayLoad" in data) ? data.delayLoad : false; let params = { selected: ("selected" in data) ? data.selected : !delayLoad, parentId: ("parentId" in data) ? data.parentId : -1, flags: flags, tabID: data.tabID, isPrivate: (data.isPrivate === true), aboutHomePage: ("aboutHomePage" in data) ? data.aboutHomePage : "", pinned: (data.pinned === true), delayLoad: (delayLoad === true), desktopMode: (data.desktopMode === true) }; let url = data.url; if (data.engine) { let engine = Services.search.getEngineByName(data.engine); if (engine) { params.userSearch = url; let submission = engine.getSubmission(url); url = submission.uri.spec; params.postData = submission.postData; } } if (data.newTab) { this.addTab(url, params); } else { if (data.tabId) { // Use a specific browser instead of the selected browser, if it exists let specificBrowser = this.getTabForId(data.tabId).browser; if (specificBrowser) browser = specificBrowser; } this.loadURI(url, browser, params); } break; } case "Tab:Selected": this._handleTabSelected(this.getTabForId(parseInt(aData))); break; case "Tab:Closed": this._handleTabClosed(this.getTabForId(parseInt(aData))); break; case "keyword-search": // This event refers to a search via the URL bar, not a bookmarks // keyword search. Note that this code assumes that the user can only // perform a keyword search on the selected tab. this.selectedTab.userSearch = aData; let engine = aSubject.QueryInterface(Ci.nsISearchEngine); sendMessageToJava({ type: "Search:Keyword", identifier: engine.identifier, name: engine.name, }); break; case "Browser:Quit": this.quit(); break; case "SaveAs:PDF": this.saveAsPDF(browser); break; case "Preferences:Set": this.setPreferences(aData); break; case "ScrollTo:FocusedInput": // these messages come from a change in the viewable area and not user interaction // we allow scrolling to the selected input, but not zooming the page this.scrollToFocusedInput(browser, false); break; case "Sanitize:ClearData": this.sanitize(aData); break; case "FullScreen:Exit": browser.contentDocument.mozCancelFullScreen(); break; case "Viewport:Change": if (this.isBrowserContentDocumentDisplayed()) this.selectedTab.setViewport(JSON.parse(aData)); break; case "Viewport:Flush": this.contentDocumentChanged(); break; case "Passwords:Init": { let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"]. getService(Ci.nsILoginManagerStorage); storage.init(); Services.obs.removeObserver(this, "Passwords:Init"); break; } case "FormHistory:Init": { // Force creation/upgrade of formhistory.sqlite FormHistory.count({}); Services.obs.removeObserver(this, "FormHistory:Init"); break; } case "sessionstore-state-purge-complete": sendMessageToJava({ type: "Session:StatePurged" }); break; case "gather-telemetry": sendMessageToJava({ type: "Telemetry:Gather" }); break; case "Viewport:FixedMarginsChanged": gViewportMargins = JSON.parse(aData); this.selectedTab.updateViewportSize(gScreenWidth); break; case "nsPref:changed": this.notifyPrefObservers(aData); break; case "Locale:Changed": // TODO: do we need to be more nuanced here -- e.g., checking for the // OS locale -- or should it always be false on Fennec? Services.prefs.setBoolPref("intl.locale.matchOS", false); Services.prefs.setCharPref("general.useragent.locale", aData); break; default: dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n'); break; } }, get defaultBrowserWidth() { delete this.defaultBrowserWidth; let width = Services.prefs.getIntPref("browser.viewport.desktopWidth"); return this.defaultBrowserWidth = width; }, // nsIAndroidBrowserApp getBrowserTab: function(tabId) { return this.getTabForId(tabId); }, getPreferences: function getPreferences(requestId, prefNames, count) { this.handlePreferencesRequest(requestId, prefNames, false); }, observePreferences: function observePreferences(requestId, prefNames, count) { this.handlePreferencesRequest(requestId, prefNames, true); }, removePreferenceObservers: function removePreferenceObservers(aRequestId) { let newPrefObservers = []; for (let prefName in this._prefObservers) { let requestIds = this._prefObservers[prefName]; // Remove the requestID from the preference handlers let i = requestIds.indexOf(aRequestId); if (i >= 0) { requestIds.splice(i, 1); } // If there are no more request IDs, remove the observer if (requestIds.length == 0) { Services.prefs.removeObserver(prefName, this); } else { newPrefObservers[prefName] = requestIds; } } this._prefObservers = newPrefObservers; }, // This method will print a list from fromIndex to toIndex, optionally // selecting selIndex(if fromIndex<=selIndex<=toIndex) showHistory: function(fromIndex, toIndex, selIndex) { let browser = this.selectedBrowser; let hist = browser.sessionHistory; let listitems = []; for (let i = toIndex; i >= fromIndex; i--) { let entry = hist.getEntryAtIndex(i, false); let item = { label: entry.title || entry.URI.spec, selected: (i == selIndex) }; listitems.push(item); } let p = new Prompt({ window: browser.contentWindow }).setSingleChoiceItems(listitems).show(function(data) { let selected = data.button; if (selected == -1) return; browser.gotoIndex(toIndex-selected); }); }, }; var NativeWindow = { init: function() { Services.obs.addObserver(this, "Menu:Clicked", false); Services.obs.addObserver(this, "PageActions:Clicked", false); Services.obs.addObserver(this, "PageActions:LongClicked", false); Services.obs.addObserver(this, "Doorhanger:Reply", false); Services.obs.addObserver(this, "Toast:Click", false); Services.obs.addObserver(this, "Toast:Hidden", false); this.contextmenus.init(); }, uninit: function() { Services.obs.removeObserver(this, "Menu:Clicked"); Services.obs.removeObserver(this, "PageActions:Clicked"); Services.obs.removeObserver(this, "PageActions:LongClicked"); Services.obs.removeObserver(this, "Doorhanger:Reply"); Services.obs.removeObserver(this, "Toast:Click", false); Services.obs.removeObserver(this, "Toast:Hidden", false); this.contextmenus.uninit(); }, loadDex: function(zipFile, implClass) { sendMessageToJava({ type: "Dex:Load", zipfile: zipFile, impl: implClass || "Main" }); }, unloadDex: function(zipFile) { sendMessageToJava({ type: "Dex:Unload", zipfile: zipFile }); }, toast: { _callbacks: {}, show: function(aMessage, aDuration, aOptions) { let msg = { type: "Toast:Show", message: aMessage, duration: aDuration }; if (aOptions && aOptions.button) { msg.button = { label: aOptions.button.label, id: uuidgen.generateUUID().toString(), // If the caller specified a button, make sure we convert any chrome urls // to jar:jar urls so that the frontend can show them icon: aOptions.button.icon ? resolveGeckoURI(aOptions.button.icon) : null, }; this._callbacks[msg.button.id] = aOptions.button.callback; } sendMessageToJava(msg); } }, pageactions: { _items: { }, add: function(aOptions) { let id = uuidgen.generateUUID().toString(); sendMessageToJava({ type: "PageActions:Add", id: id, title: aOptions.title, icon: resolveGeckoURI(aOptions.icon), important: "important" in aOptions ? aOptions.important : false }); this._items[id] = { clickCallback: aOptions.clickCallback, longClickCallback: aOptions.longClickCallback }; return id; }, remove: function(id) { sendMessageToJava({ type: "PageActions:Remove", id: id }); delete this._items[id]; } }, menu: { _callbacks: [], _menuId: 1, toolsMenuID: -1, add: function() { let options; if (arguments.length == 1) { options = arguments[0]; } else if (arguments.length == 3) { options = { name: arguments[0], icon: arguments[1], callback: arguments[2] }; } else { throw "Incorrect number of parameters"; } options.type = "Menu:Add"; options.id = this._menuId; sendMessageToJava(options); this._callbacks[this._menuId] = options.callback; this._menuId++; return this._menuId - 1; }, remove: function(aId) { sendMessageToJava({ type: "Menu:Remove", id: aId }); }, update: function(aId, aOptions) { if (!aOptions) return; sendMessageToJava({ type: "Menu:Update", id: aId, options: aOptions }); } }, doorhanger: { _callbacks: {}, _callbacksId: 0, _promptId: 0, /** * @param aOptions * An options JavaScript object holding additional properties for the * notification. The following properties are currently supported: * persistence: An integer. The notification will not automatically * dismiss for this many page loads. If persistence is set * to -1, the doorhanger will never automatically dismiss. * persistWhileVisible: * A boolean. If true, a visible notification will always * persist across location changes. * timeout: A time in milliseconds. The notification will not * automatically dismiss before this time. * checkbox: A string to appear next to a checkbox under the notification * message. The button callback functions will be called with * the checked state as an argument. */ show: function(aMessage, aValue, aButtons, aTabID, aOptions) { if (aButtons == null) { aButtons = []; } aButtons.forEach((function(aButton) { this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId }; aButton.callback = this._callbacksId; this._callbacksId++; }).bind(this)); this._promptId++; let json = { type: "Doorhanger:Add", message: aMessage, value: aValue, buttons: aButtons, // use the current tab if none is provided tabID: aTabID || BrowserApp.selectedTab.id, options: aOptions || {} }; sendMessageToJava(json); }, hide: function(aValue, aTabID) { sendMessageToJava({ type: "Doorhanger:Remove", value: aValue, tabID: aTabID }); } }, observe: function(aSubject, aTopic, aData) { if (aTopic == "Menu:Clicked") { if (this.menu._callbacks[aData]) this.menu._callbacks[aData](); } else if (aTopic == "PageActions:Clicked") { if (this.pageactions._items[aData].clickCallback) this.pageactions._items[aData].clickCallback(); } else if (aTopic == "PageActions:LongClicked") { if (this.pageactions._items[aData].longClickCallback) this.pageactions._items[aData].longClickCallback(); } else if (aTopic == "Toast:Click") { if (this.toast._callbacks[aData]) { this.toast._callbacks[aData](); delete this.toast._callbacks[aData]; } } else if (aTopic == "Toast:Hidden") { if (this.toast._callbacks[aData]) delete this.toast._callbacks[aData]; } else if (aTopic == "Doorhanger:Reply") { let data = JSON.parse(aData); let reply_id = data["callback"]; if (this.doorhanger._callbacks[reply_id]) { // Pass the value of the optional checkbox to the callback let checked = data["checked"]; this.doorhanger._callbacks[reply_id].cb(checked, data.inputs); let prompt = this.doorhanger._callbacks[reply_id].prompt; for (let id in this.doorhanger._callbacks) { if (this.doorhanger._callbacks[id].prompt == prompt) { delete this.doorhanger._callbacks[id]; } } } } }, contextmenus: { items: {}, // a list of context menu items that we may show _nativeItemsSeparator: 0, // the index to insert native context menu items at _contextId: 0, // id to assign to new context menu items if they are added init: function() { Services.obs.addObserver(this, "Gesture:LongPress", false); }, uninit: function() { Services.obs.removeObserver(this, "Gesture:LongPress"); }, add: function(aName, aSelector, aCallback) { if (!aName) throw "Menu items must have a name"; let item = { name: aName, context: aSelector, callback: aCallback, matches: function(aElt, aX, aY) { return this.context.matches(aElt, aX, aY); }, getValue: function(aElt) { return { label: (typeof this.name == "function") ? this.name(aElt) : this.name, id: this.id } } }; item.id = this._contextId++; this.items[item.id] = item; return item.id; }, remove: function(aId) { delete this.items[aId]; }, SelectorContext: function(aSelector) { return { matches: function(aElt) { if (aElt.mozMatchesSelector) return aElt.mozMatchesSelector(aSelector); return false; } } }, linkOpenableNonPrivateContext: { matches: function linkOpenableNonPrivateContextMatches(aElement) { let doc = aElement.ownerDocument; if (!doc || PrivateBrowsingUtils.isWindowPrivate(doc.defaultView)) { return false; } return NativeWindow.contextmenus.linkOpenableContext.matches(aElement); } }, linkOpenableContext: { matches: function linkOpenableContextMatches(aElement) { let uri = NativeWindow.contextmenus._getLink(aElement); if (uri) { let scheme = uri.scheme; let dontOpen = /^(javascript|mailto|news|snews|tel)$/; return (scheme && !dontOpen.test(scheme)); } return false; } }, linkCopyableContext: { matches: function linkCopyableContextMatches(aElement) { let uri = NativeWindow.contextmenus._getLink(aElement); if (uri) { let scheme = uri.scheme; let dontCopy = /^(mailto|tel)$/; return (scheme && !dontCopy.test(scheme)); } return false; } }, linkShareableContext: { matches: function linkShareableContextMatches(aElement) { let uri = NativeWindow.contextmenus._getLink(aElement); if (uri) { let scheme = uri.scheme; let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/; return (scheme && !dontShare.test(scheme)); } return false; } }, linkBookmarkableContext: { matches: function linkBookmarkableContextMatches(aElement) { let uri = NativeWindow.contextmenus._getLink(aElement); if (uri) { let scheme = uri.scheme; let dontBookmark = /^(mailto|tel)$/; return (scheme && !dontBookmark.test(scheme)); } return false; } }, emailLinkContext: { matches: function emailLinkContextMatches(aElement) { let uri = NativeWindow.contextmenus._getLink(aElement); if (uri) return uri.schemeIs("mailto"); return false; } }, phoneNumberLinkContext: { matches: function phoneNumberLinkContextMatches(aElement) { let uri = NativeWindow.contextmenus._getLink(aElement); if (uri) return uri.schemeIs("tel"); return false; } }, textContext: { matches: function textContext(aElement) { return ((aElement instanceof Ci.nsIDOMHTMLInputElement && aElement.mozIsTextField(false)) || aElement instanceof Ci.nsIDOMHTMLTextAreaElement); } }, imageLocationCopyableContext: { matches: function imageLinkCopyableContextMatches(aElement) { return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI); } }, imageSaveableContext: { matches: function imageSaveableContextMatches(aElement) { if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) { // The image must be loaded to allow saving let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE)); } return false; } }, mediaSaveableContext: { matches: function mediaSaveableContextMatches(aElement) { return (aElement instanceof HTMLVideoElement || aElement instanceof HTMLAudioElement); } }, mediaContext: function(aMode) { return { matches: function(aElt) { if (aElt instanceof Ci.nsIDOMHTMLMediaElement) { let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE; if (hasError) return false; let paused = aElt.paused || aElt.ended; if (paused && aMode == "media-paused") return true; if (!paused && aMode == "media-playing") return true; let controls = aElt.controls; if (!controls && aMode == "media-hidingcontrols") return true; let muted = aElt.muted; if (muted && aMode == "media-muted") return true; else if (!muted && aMode == "media-unmuted") return true; } return false; } } }, get _target() { if (this._targetRef) return this._targetRef.get(); return null; }, set _target(aTarget) { if (aTarget) this._targetRef = Cu.getWeakReference(aTarget); else this._targetRef = null; }, _addHTMLContextMenuItems: function cm_addContextMenuItems(aMenu, aParent) { for (let i = 0; i < aMenu.childNodes.length; i++) { let item = aMenu.childNodes[i]; if (!item.label) continue; let id = this._contextId++; let menuitem = { id: id, isGroup: false, callback: (function(aTarget, aX, aY) { // If this is a menu item, show a new context menu with the submenu in it if (item instanceof Ci.nsIDOMHTMLMenuElement) { this.menuitems = []; this._nativeItemsSeparator = 0; this._addHTMLContextMenuItems(item, id); this._innerShow(aTarget, aX, aY); } else { // oltherwise just click the item item.click(); } }).bind(this), getValue: function(aElt) { if (item.hasAttribute("hidden")) return null; return { icon: item.icon, label: item.label, id: id, disabled: item.disabled, parent: item instanceof Ci.nsIDOMHTMLMenuElement } } }; this.menuitems.splice(this._nativeItemsSeparator, 0, menuitem); this._nativeItemsSeparator++; } }, _getMenuItemForId: function(aId) { if (!this.menuitems) return null; for (let i = 0; i < this.menuitems.length; i++) { if (this.menuitems[i].id == aId) return this.menuitems[i]; } return null; }, // Checks if there are context menu items to show, and if it finds them // sends a contextmenu event to content. We also send showing events to // any html5 context menus we are about to show _sendToContent: function(aX, aY) { // find and store the top most element this context menu is being shown for // use the highlighted element if possible, otherwise look for nearby clickable elements // If we still don't find one we fall back to using anything let target = BrowserEventHandler._highlightElement || ElementTouchHelper.elementFromPoint(aX, aY); if (!target) target = ElementTouchHelper.anyElementFromPoint(aX, aY); if (!target) return; // store a weakref to the target to be used when the context menu event returns this._target = target; this.menuitems = []; let menuitemsSet = false; Services.obs.notifyObservers(null, "before-build-contextmenu", ""); // now walk up the tree and for each node look for any context menu items that apply let element = target; this._nativeItemsSeparator = 0; while (element) { // first check for any html5 context menus that might exist let contextmenu = element.contextMenu; if (contextmenu) { // send this before we build the list to make sure the site can update the menu contextmenu.QueryInterface(Components.interfaces.nsIHTMLMenu); contextmenu.sendShowEvent(); this._addHTMLContextMenuItems(contextmenu, null); } // then check for any context menu items registered in the ui for (let itemId of Object.keys(this.items)) { let item = this.items[itemId]; if (!this._getMenuItemForId(item.id) && item.matches(element, aX, aY)) { this.menuitems.push(item); } } element = element.parentNode; } // only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap) if (this.menuitems.length > 0) { let event = target.ownerDocument.createEvent("MouseEvent"); event.initMouseEvent("contextmenu", true, true, content, 0, aX, aY, aX, aY, false, false, false, false, 0, null); target.ownerDocument.defaultView.addEventListener("contextmenu", this, false); target.dispatchEvent(event); } else { this._target = null; BrowserEventHandler._cancelTapHighlight(); if (SelectionHandler.canSelect(target)) { if (!SelectionHandler.startSelection(target, aX, aY)) { SelectionHandler.attachCaret(target); } } } }, // Actually shows the native context menu by passing a list of context menu items to // show to the Java. _show: function(aEvent) { let popupNode = this._target; this._target = null; if (aEvent.defaultPrevented || !popupNode) { return; } this._innerShow(popupNode, aEvent.clientX, aEvent.clientY); }, _innerShow: function(aTarget, aX, aY) { Haptic.performSimpleAction(Haptic.LongPress); // spin through the tree looking for a title for this context menu let node = aTarget; let title =""; while(node && !title) { if (node.hasAttribute && node.hasAttribute("title")) { title = node.getAttribute("title"); } else if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) || (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) { title = this._getLinkURL(node); } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) { title = node.currentURI.spec; } else if (node instanceof Ci.nsIDOMHTMLMediaElement) { title = (node.currentSrc || node.src); } node = node.parentNode; } // convert this.menuitems object to an array for sending to native code let itemArray = []; for (let i = 0; i < this.menuitems.length; i++) { let val = this.menuitems[i].getValue(aTarget); // hidden menu items will return null from getValue if (val) itemArray.push(val); } if (itemArray.length == 0) return; let prompt = new Prompt({ window: aTarget.ownerDocument.defaultView, title: title }).setSingleChoiceItems(itemArray) .show((function(data) { if (data.button == -1) { // prompt was cancelled return; } let selectedId = itemArray[data.button].id; let selectedItem = this._getMenuItemForId(selectedId); this.menuitems = null; if (selectedItem && selectedItem.callback) { if (selectedItem.matches) { // for menuitems added using the native UI, pass the dom element that matched that item to the callback while (aTarget) { if (selectedItem.matches(aTarget, aX, aY)) { selectedItem.callback.call(selectedItem, aTarget, aX, aY); break; } aTarget = aTarget.parentNode; } } else { // if this was added using the html5 context menu api, just click on the context menu item selectedItem.callback.call(selectedItem, aTarget, aX, aY); } } }).bind(this)); }, handleEvent: function(aEvent) { BrowserEventHandler._cancelTapHighlight(); aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false); this._show(aEvent); }, observe: function(aSubject, aTopic, aData) { let data = JSON.parse(aData); // content gets first crack at cancelling context menus this._sendToContent(data.x, data.y); }, // XXX - These are stolen from Util.js, we should remove them if we bring it back makeURLAbsolute: function makeURLAbsolute(base, url) { // Note: makeURI() will throw if url is not a valid URI return this.makeURI(url, null, this.makeURI(base)).spec; }, makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) { return Services.io.newURI(aURL, aOriginCharset, aBaseURI); }, _getLink: function(aElement) { if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) || (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) || aElement instanceof Ci.nsIDOMHTMLLinkElement || aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) { try { let url = NativeWindow.contextmenus._getLinkURL(aElement); return Services.io.newURI(url, null, null); } catch (e) {} } return null; }, _disableInGuest: function _disableInGuest(selector) { return { matches: function _disableInGuestMatches(aElement, aX, aY) { if (BrowserApp.isGuest) return false; return selector.matches(aElement, aX, aY); } } }, _getLinkURL: function ch_getLinkURL(aLink) { let href = aLink.href; if (href) return href; href = aLink.getAttributeNS(kXLinkNamespace, "href"); if (!href || !href.match(/\S/)) { // Without this we try to save as the current doc, // for example, HTML case also throws if empty throw "Empty href"; } return this.makeURLAbsolute(aLink.baseURI, href); }, _copyStringToDefaultClipboard: function(aString) { let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); clipboard.copyString(aString); }, _shareStringWithDefault: function(aSharedString, aTitle) { let sharing = Cc["@mozilla.org/uriloader/external-sharing-app-service;1"].getService(Ci.nsIExternalSharingAppService); sharing.shareWithDefault(aSharedString, "text/plain", aTitle); }, _stripScheme: function(aString) { return aString.slice(aString.indexOf(":") + 1); } } }; var LightWeightThemeWebInstaller = { init: function sh_init() { let temp = {}; Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp); let theme = new temp.LightweightThemeConsumer(document); BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true); BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true); BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true); }, uninit: function() { BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true); BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true); BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true); }, handleEvent: function (event) { switch (event.type) { case "InstallBrowserTheme": case "PreviewBrowserTheme": case "ResetBrowserThemePreview": // ignore requests from background tabs if (event.target.ownerDocument.defaultView.top != content) return; } switch (event.type) { case "InstallBrowserTheme": this._installRequest(event); break; case "PreviewBrowserTheme": this._preview(event); break; case "ResetBrowserThemePreview": this._resetPreview(event); break; case "pagehide": case "TabSelect": this._resetPreview(); break; } }, get _manager () { let temp = {}; Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp); delete this._manager; return this._manager = temp.LightweightThemeManager; }, _installRequest: function (event) { let node = event.target; let data = this._getThemeFromNode(node); if (!data) return; if (this._isAllowed(node)) { this._install(data); return; } let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton"); let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1); let buttons = [{ label: allowButtonText, callback: function () { LightWeightThemeWebInstaller._install(data); } }]; NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id); }, _install: function (newLWTheme) { this._manager.currentTheme = newLWTheme; }, _previewWindow: null, _preview: function (event) { if (!this._isAllowed(event.target)) return; let data = this._getThemeFromNode(event.target); if (!data) return; this._resetPreview(); this._previewWindow = event.target.ownerDocument.defaultView; this._previewWindow.addEventListener("pagehide", this, true); BrowserApp.deck.addEventListener("TabSelect", this, false); this._manager.previewTheme(data); }, _resetPreview: function (event) { if (!this._previewWindow || event && !this._isAllowed(event.target)) return; this._previewWindow.removeEventListener("pagehide", this, true); this._previewWindow = null; BrowserApp.deck.removeEventListener("TabSelect", this, false); this._manager.resetPreview(); }, _isAllowed: function (node) { let pm = Services.perms; let uri = node.ownerDocument.documentURIObject; return pm.testPermission(uri, "install") == pm.ALLOW_ACTION; }, _getThemeFromNode: function (node) { return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI); } }; var DesktopUserAgent = { DESKTOP_UA: null, init: function ua_init() { Services.obs.addObserver(this, "DesktopMode:Change", false); UserAgentOverrides.addComplexOverride(this.onRequest.bind(this)); // See https://developer.mozilla.org/en/Gecko_user_agent_string_reference this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"] .getService(Ci.nsIHttpProtocolHandler).userAgent .replace(/Android; [a-zA-Z]+/, "X11; Linux x86_64") .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101"); }, uninit: function ua_uninit() { Services.obs.removeObserver(this, "DesktopMode:Change"); }, onRequest: function(channel, defaultUA) { let channelWindow = this._getWindowForRequest(channel); let tab = BrowserApp.getTabForWindow(channelWindow); if (tab == null) return null; return this.getUserAgentForTab(tab); }, getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) { let tab = BrowserApp.getTabForWindow(aWindow.top); if (tab) return this.getUserAgentForTab(tab); return null; }, getUserAgentForTab: function ua_getUserAgentForTab(aTab) { // Send desktop UA if "Request Desktop Site" is enabled. if (aTab.desktopMode) return this.DESKTOP_UA; return null; }, _getRequestLoadContext: function ua_getRequestLoadContext(aRequest) { if (aRequest && aRequest.notificationCallbacks) { try { return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext); } catch (ex) { } } if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) { try { return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); } catch (ex) { } } return null; }, _getWindowForRequest: function ua_getWindowForRequest(aRequest) { let loadContext = this._getRequestLoadContext(aRequest); if (loadContext) { try { return loadContext.associatedWindow; } catch (e) { // loadContext.associatedWindow can throw when there's no window } } return null; }, observe: function ua_observe(aSubject, aTopic, aData) { if (aTopic === "DesktopMode:Change") { let args = JSON.parse(aData); let tab = BrowserApp.getTabForId(args.tabId); if (tab != null) tab.reloadWithMode(args.desktopMode); } } }; function nsBrowserAccess() { } nsBrowserAccess.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]), _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aContext) { let isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); if (isExternal && aURI && aURI.schemeIs("chrome")) return null; let loadflags = isExternal ? Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL : Ci.nsIWebNavigation.LOAD_FLAGS_NONE; if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) { switch (aContext) { case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL: aWhere = Services.prefs.getIntPref("browser.link.open_external"); break; default: // OPEN_NEW or an illegal value aWhere = Services.prefs.getIntPref("browser.link.open_newwindow"); } } Services.io.offline = false; let referrer; if (aOpener) { try { let location = aOpener.location; referrer = Services.io.newURI(location, null, null); } catch(e) { } } let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); let pinned = false; if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) { pinned = true; let spec = aURI.spec; let tabs = BrowserApp.tabs; for (let i = 0; i < tabs.length; i++) { let appOrigin = ss.getTabValue(tabs[i], "appOrigin"); if (appOrigin == spec) { let tab = tabs[i]; BrowserApp.selectTab(tab); return tab.browser; } } } let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW || aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB || aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB); let isPrivate = false; if (newTab) { let parentId = -1; if (!isExternal && aOpener) { let parent = BrowserApp.getTabForWindow(aOpener.top); if (parent) { parentId = parent.id; isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow); } } // BrowserApp.addTab calls loadURIWithFlags with the appropriate params let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags, referrerURI: referrer, external: isExternal, parentId: parentId, selected: true, isPrivate: isPrivate, pinned: pinned }); return tab.browser; } // OPEN_CURRENTWINDOW and illegal values let browser = BrowserApp.selectedBrowser; if (aURI && browser) browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null); return browser; }, openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) { let browser = this._getBrowser(aURI, aOpener, aWhere, aContext); return browser ? browser.contentWindow : null; }, openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) { let browser = this._getBrowser(aURI, aOpener, aWhere, aContext); return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null; }, isTabContentWindow: function(aWindow) { return BrowserApp.getBrowserForWindow(aWindow) != null; }, get contentWindow() { return BrowserApp.selectedBrowser.contentWindow; } }; // track the last known screen size so that new tabs // get created with the right size rather than being 1x1 let gScreenWidth = 1; let gScreenHeight = 1; let gReflowPending = null; // The margins that should be applied to the viewport for fixed position // children. This is used to avoid browser chrome permanently obscuring // fixed position content, and also to make sure window-sized pages take // into account said browser chrome. let gViewportMargins = { top: 0, right: 0, bottom: 0, left: 0}; function Tab(aURL, aParams) { this.browser = null; this.id = 0; this.lastTouchedAt = Date.now(); this._zoom = 1.0; this._drawZoom = 1.0; this._fixedMarginLeft = 0; this._fixedMarginTop = 0; this._fixedMarginRight = 0; this._fixedMarginBottom = 0; this._readerEnabled = false; this._readerActive = false; this.userScrollPos = { x: 0, y: 0 }; this.viewportExcludesHorizontalMargins = true; this.viewportExcludesVerticalMargins = true; this.viewportMeasureCallback = null; this.lastPageSizeAfterViewportRemeasure = { width: 0, height: 0 }; this.contentDocumentIsDisplayed = true; this.pluginDoorhangerTimeout = null; this.shouldShowPluginDoorhanger = true; this.clickToPlayPluginsActivated = false; this.desktopMode = false; this.originalURI = null; this.aboutHomePage = null; this.savedArticle = null; this.hasTouchListener = false; this.browserWidth = 0; this.browserHeight = 0; this.create(aURL, aParams); } Tab.prototype = { create: function(aURL, aParams) { if (this.browser) return; aParams = aParams || {}; this.browser = document.createElement("browser"); this.browser.setAttribute("type", "content-targetable"); this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight); // Make sure the previously selected panel remains selected. The selected panel of a deck is // not stable when panels are added. let selectedPanel = BrowserApp.deck.selectedPanel; BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null); BrowserApp.deck.selectedPanel = selectedPanel; if (BrowserApp.manifestUrl) { let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService); let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl); if (manifest) { let app = manifest.QueryInterface(Ci.mozIApplication); this.browser.docShell.setIsApp(app.localId); } } // Must be called after appendChild so the docshell has been created. this.setActive(false); let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate; if (isPrivate) { this.browser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing = true; } this.browser.stop(); let frameLoader = this.browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; frameLoader.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL; // only set tab uri if uri is valid let uri = null; let title = aParams.title || aURL; try { uri = Services.io.newURI(aURL, null, null).spec; } catch (e) {} // When the tab is stubbed from Java, there's a window between the stub // creation and the tab creation in Gecko where the stub could be removed // (which is easiest to hit during startup). We need to differentiate // between tab stubs from Java and new tabs from Gecko to prevent breakage. let stub = false; if (!aParams.zombifying) { if ("tabID" in aParams) { this.id = aParams.tabID; stub = true; } else { let jni = new JNI(); let cls = jni.findClass("org/mozilla/gecko/Tabs"); let method = jni.getStaticMethodID(cls, "getNextTabId", "()I"); this.id = jni.callStaticIntMethod(cls, method); jni.close(); } this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false; let message = { type: "Tab:Added", tabID: this.id, uri: uri, parentId: ("parentId" in aParams) ? aParams.parentId : -1, external: ("external" in aParams) ? aParams.external : false, selected: ("selected" in aParams) ? aParams.selected : true, title: title, delayLoad: aParams.delayLoad || false, desktopMode: this.desktopMode, isPrivate: isPrivate, stub: stub }; sendMessageToJava(message); this.overscrollController = new OverscrollController(this); } this.browser.contentWindow.controllers.insertControllerAt(0, this.overscrollController); let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL | Ci.nsIWebProgress.NOTIFY_LOCATION | Ci.nsIWebProgress.NOTIFY_SECURITY; this.browser.addProgressListener(this, flags); this.browser.sessionHistory.addSHistoryListener(this); this.browser.addEventListener("DOMContentLoaded", this, true); this.browser.addEventListener("DOMFormHasPassword", this, true); this.browser.addEventListener("DOMLinkAdded", this, true); this.browser.addEventListener("DOMTitleChanged", this, true); this.browser.addEventListener("DOMWindowClose", this, true); this.browser.addEventListener("DOMWillOpenModalDialog", this, true); this.browser.addEventListener("DOMAutoComplete", this, true); this.browser.addEventListener("blur", this, true); this.browser.addEventListener("scroll", this, true); this.browser.addEventListener("MozScrolledAreaChanged", this, true); // Note that the XBL binding is untrusted this.browser.addEventListener("PluginBindingAttached", this, true, true); this.browser.addEventListener("pageshow", this, true); this.browser.addEventListener("MozApplicationManifest", this, true); Services.obs.addObserver(this, "before-first-paint", false); Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false); if (aParams.delayLoad) { // If this is a zombie tab, attach restore data so the tab will be // restored when selected this.browser.__SS_data = { entries: [{ url: aURL, title: title }], index: 1 }; this.browser.__SS_restore = true; } else { let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE; let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null; let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null; let charset = "charset" in aParams ? aParams.charset : null; // The search term the user entered to load the current URL this.userSearch = "userSearch" in aParams ? aParams.userSearch : ""; try { this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData); } catch(e) { let message = { type: "Content:LoadError", tabID: this.id }; sendMessageToJava(message); dump("Handled load error: " + e); } } }, /** * Retrieves the font size in twips for a given element. */ getInflatedFontSizeFor: function(aElement) { // GetComputedStyle should always give us CSS pixels for a font size. let fontSizeStr = this.window.getComputedStyle(aElement)['fontSize']; let fontSize = fontSizeStr.slice(0, -2); return aElement.fontSizeInflation * fontSize; }, /** * This returns the zoom necessary to match the font size of an element to * the minimum font size specified by the browser.zoom.reflowOnZoom.minFontSizeTwips * preference. */ getZoomToMinFontSize: function(aElement) { // We only use the font.size.inflation.minTwips preference because this is // the only one that is controlled by the user-interface in the 'Settings' // menu. Thus, if font.size.inflation.emPerLine is changed, this does not // effect reflow-on-zoom. let minFontSize = convertFromTwipsToPx(Services.prefs.getIntPref("font.size.inflation.minTwips")); return minFontSize / this.getInflatedFontSizeFor(aElement); }, performReflowOnZoom: function(aViewport) { let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom; let viewportWidth = gScreenWidth / zoom; let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout"); if (gReflowPending) { clearTimeout(gReflowPending); } // We add in a bit of fudge just so that the end characters // don't accidentally get clipped. 15px is an arbitrary choice. gReflowPending = setTimeout(doChangeMaxLineBoxWidth, reflozTimeout, viewportWidth - 15); }, /** * Reloads the tab with the desktop mode setting. */ reloadWithMode: function (aDesktopMode) { // Set desktop mode for tab and send change to Java if (this.desktopMode != aDesktopMode) { this.desktopMode = aDesktopMode; sendMessageToJava({ type: "DesktopMode:Changed", desktopMode: aDesktopMode, tabID: this.id }); } // Only reload the page for http/https schemes let currentURI = this.browser.currentURI; if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https")) return; let url = currentURI.spec; let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; if (this.originalURI && !this.originalURI.equals(currentURI)) { // We were redirected; reload the original URL url = this.originalURI.spec; flags |= Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY; } else { // Many sites use mobile-specific URLs, such as: // http://m.yahoo.com // http://www.google.com/m // If the user clicks "Request Desktop Site" while on a mobile site, it // will appear to do nothing since the mobile URL is still being // requested. To address this, we do the following: // 1) Remove the path from the URL (http://www.google.com/m?q=query -> http://www.google.com) // 2) If a host subdomain is "m", remove it (http://en.m.wikipedia.org -> http://en.wikipedia.org) // This means the user is sent to site's home page, but this is better // than the setting having no effect at all. if (aDesktopMode) url = currentURI.prePath.replace(/([\/\.])m\./g, "$1"); else flags |= Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY; } this.browser.docShell.loadURI(url, flags, null, null, null); }, destroy: function() { if (!this.browser) return; this.browser.contentWindow.controllers.removeController(this.overscrollController); this.browser.removeProgressListener(this); this.browser.sessionHistory.removeSHistoryListener(this); this.browser.removeEventListener("DOMContentLoaded", this, true); this.browser.removeEventListener("DOMFormHasPassword", this, true); this.browser.removeEventListener("DOMLinkAdded", this, true); this.browser.removeEventListener("DOMTitleChanged", this, true); this.browser.removeEventListener("DOMWindowClose", this, true); this.browser.removeEventListener("DOMWillOpenModalDialog", this, true); this.browser.removeEventListener("DOMAutoComplete", this, true); this.browser.removeEventListener("blur", this, true); this.browser.removeEventListener("scroll", this, true); this.browser.removeEventListener("MozScrolledAreaChanged", this, true); this.browser.removeEventListener("PluginBindingAttached", this, true); this.browser.removeEventListener("pageshow", this, true); this.browser.removeEventListener("MozApplicationManifest", this, true); Services.obs.removeObserver(this, "before-first-paint"); Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this); // Make sure the previously selected panel remains selected. The selected panel of a deck is // not stable when panels are removed. let selectedPanel = BrowserApp.deck.selectedPanel; BrowserApp.deck.removeChild(this.browser); BrowserApp.deck.selectedPanel = selectedPanel; this.browser = null; this.savedArticle = null; }, // This should be called to update the browser when the tab gets selected/unselected setActive: function setActive(aActive) { if (!this.browser || !this.browser.docShell) return; if (aActive) { this.browser.setAttribute("type", "content-primary"); this.browser.focus(); this.browser.docShellIsActive = true; Reader.updatePageAction(this); ExternalApps.updatePageAction(this.browser.currentURI); } else { this.browser.setAttribute("type", "content-targetable"); this.browser.docShellIsActive = false; } }, getActive: function getActive() { return this.browser.docShellIsActive; }, setDisplayPort: function(aDisplayPort) { let zoom = this._zoom; let resolution = aDisplayPort.resolution; if (zoom <= 0 || resolution <= 0) return; // "zoom" is the user-visible zoom of the "this" tab // "resolution" is the zoom at which we wish gecko to render "this" tab at // these two may be different if we are, for example, trying to render a // large area of the page at low resolution because the user is panning real // fast. // The gecko scroll position is in CSS pixels. The display port rect // values (aDisplayPort), however, are in CSS pixels multiplied by the desired // rendering resolution. Therefore care must be taken when doing math with // these sets of values, to ensure that they are normalized to the same coordinate // space first. let element = this.browser.contentDocument.documentElement; if (!element) return; // we should never be drawing background tabs at resolutions other than the user- // visible zoom. for foreground tabs, however, if we are drawing at some other // resolution, we need to set the resolution as specified. let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); if (BrowserApp.selectedTab == this) { if (resolution != this._drawZoom) { this._drawZoom = resolution; cwu.setResolution(resolution / window.devicePixelRatio, resolution / window.devicePixelRatio); } } else if (!fuzzyEquals(resolution, zoom)) { dump("Warning: setDisplayPort resolution did not match zoom for background tab! (" + resolution + " != " + zoom + ")"); } // Finally, we set the display port, taking care to convert everything into the CSS-pixel // coordinate space, because that is what the function accepts. Also we have to fudge the // displayport somewhat to make sure it gets through all the conversions gecko will do on it // without deforming too much. See https://bugzilla.mozilla.org/show_bug.cgi?id=737510#c10 // for details on what these operations are. let geckoScrollX = this.browser.contentWindow.scrollX; let geckoScrollY = this.browser.contentWindow.scrollY; aDisplayPort = this._dirtiestHackEverToWorkAroundGeckoRounding(aDisplayPort, geckoScrollX, geckoScrollY); let displayPort = { x: (aDisplayPort.left / resolution) - geckoScrollX, y: (aDisplayPort.top / resolution) - geckoScrollY, width: (aDisplayPort.right - aDisplayPort.left) / resolution, height: (aDisplayPort.bottom - aDisplayPort.top) / resolution }; if (this._oldDisplayPort == null || !fuzzyEquals(displayPort.x, this._oldDisplayPort.x) || !fuzzyEquals(displayPort.y, this._oldDisplayPort.y) || !fuzzyEquals(displayPort.width, this._oldDisplayPort.width) || !fuzzyEquals(displayPort.height, this._oldDisplayPort.height)) { if (BrowserApp.gUseLowPrecision) { // Set the display-port to be 4x the size of the critical display-port, // on each dimension, giving us a 0.25x lower precision buffer around the // critical display-port. Spare area is *not* redistributed to the other // axis, as display-list building and invalidation cost scales with the // size of the display-port. let pageRect = cwu.getRootBounds(); let pageXMost = pageRect.right - geckoScrollX; let pageYMost = pageRect.bottom - geckoScrollY; let dpW = Math.min(pageRect.right - pageRect.left, displayPort.width * 4); let dpH = Math.min(pageRect.bottom - pageRect.top, displayPort.height * 4); let dpX = Math.min(Math.max(displayPort.x - displayPort.width * 1.5, pageRect.left - geckoScrollX), pageXMost - dpW); let dpY = Math.min(Math.max(displayPort.y - displayPort.height * 1.5, pageRect.top - geckoScrollY), pageYMost - dpH); cwu.setDisplayPortForElement(dpX, dpY, dpW, dpH, element); cwu.setCriticalDisplayPortForElement(displayPort.x, displayPort.y, displayPort.width, displayPort.height, element); } else { cwu.setDisplayPortForElement(displayPort.x, displayPort.y, displayPort.width, displayPort.height, element); } } this._oldDisplayPort = displayPort; }, /* * Yes, this is ugly. But it's currently the safest way to account for the rounding errors that occur * when we pump the displayport coordinates through gecko and they pop out in the compositor. * * In general, the values are converted from page-relative device pixels to viewport-relative app units, * and then back to page-relative device pixels (now as ints). The first half of this is only slightly * lossy, but it's enough to throw off the numbers a little. Because of this, when gecko calls * ScaleToOutsidePixels to generate the final rect, the rect may get expanded more than it should, * ending up a pixel larger than it started off. This is undesirable in general, but specifically * bad for tiling, because it means we means we end up painting one line of pixels from a tile, * causing an otherwise unnecessary upload of the whole tile. * * In order to counteract the rounding error, this code simulates the conversions that will happen * to the display port, and calculates whether or not that final ScaleToOutsidePixels is actually * expanding the rect more than it should. If so, it determines how much rounding error was introduced * up until that point, and adjusts the original values to compensate for that rounding error. */ _dirtiestHackEverToWorkAroundGeckoRounding: function(aDisplayPort, aGeckoScrollX, aGeckoScrollY) { const APP_UNITS_PER_CSS_PIXEL = 60.0; const EXTRA_FUDGE = 0.04; let resolution = aDisplayPort.resolution; // Some helper functions that simulate conversion processes in gecko function cssPixelsToAppUnits(aVal) { return Math.floor((aVal * APP_UNITS_PER_CSS_PIXEL) + 0.5); } function appUnitsToDevicePixels(aVal) { return aVal / APP_UNITS_PER_CSS_PIXEL * resolution; } function devicePixelsToAppUnits(aVal) { return cssPixelsToAppUnits(aVal / resolution); } // Stash our original (desired) displayport width and height away, we need it // later and we might modify the displayport in between. let originalWidth = aDisplayPort.right - aDisplayPort.left; let originalHeight = aDisplayPort.bottom - aDisplayPort.top; // This is the first conversion the displayport goes through, going from page-relative // device pixels to viewport-relative app units. let appUnitDisplayPort = { x: cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX), y: cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY), w: cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution), h: cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution) }; // This is the translation gecko applies when converting back from viewport-relative // device pixels to page-relative device pixels. let geckoTransformX = -Math.floor((-aGeckoScrollX * resolution) + 0.5); let geckoTransformY = -Math.floor((-aGeckoScrollY * resolution) + 0.5); // The final "left" value as calculated in gecko is: // left = geckoTransformX + Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)) // In a perfect world, this value would be identical to aDisplayPort.left, which is what // we started with. However, this may not be the case if the value being floored has accumulated // enough error to drop below what it should be. // For example, assume geckoTransformX is 0, and aDisplayPort.left is 4, but // appUnitsToDevicePixels(appUnitsToDevicePixels.x) comes out as 3.9 because of rounding error. // That's bad, because the -0.1 error has caused it to floor to 3 instead of 4. (If it had errored // the other way and come out as 4.1, there's no problem). In this example, we need to increase the // "left" value by some amount so that the 3.9 actually comes out as >= 4, and it gets floored into // the expected value of 4. The delta values calculated below calculate that error amount (e.g. -0.1). let errorLeft = (geckoTransformX + appUnitsToDevicePixels(appUnitDisplayPort.x)) - aDisplayPort.left; let errorTop = (geckoTransformY + appUnitsToDevicePixels(appUnitDisplayPort.y)) - aDisplayPort.top; // If the error was negative, that means it will floor incorrectly, so we need to bump up the // original aDisplayPort.left and/or aDisplayPort.top values. The amount we bump it up by is // the error amount (increased by a small fudge factor to ensure it's sufficient), converted // backwards through the conversion process. if (errorLeft < 0) { aDisplayPort.left += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorLeft)); // After we modify the left value, we need to re-simulate some values to take that into account appUnitDisplayPort.x = cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX); appUnitDisplayPort.w = cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution); } if (errorTop < 0) { aDisplayPort.top += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorTop)); // After we modify the top value, we need to re-simulate some values to take that into account appUnitDisplayPort.y = cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY); appUnitDisplayPort.h = cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution); } // At this point, the aDisplayPort.left and aDisplayPort.top values have been corrected to account // for the error in conversion such that they end up where we want them. Now we need to also do the // same for the right/bottom values so that the width/height end up where we want them. // This is the final conversion that the displayport goes through before gecko spits it back to // us. Note that the width/height calculates are of the form "ceil(transform(right)) - floor(transform(left))" let scaledOutDevicePixels = { x: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)), y: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y)), w: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)), h: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y)) }; // The final "width" value as calculated in gecko is scaledOutDevicePixels.w. // In a perfect world, this would equal originalWidth. However, things are not perfect, and as before, // we need to calculate how much rounding error has been introduced. In this case the rounding error is causing // the Math.ceil call above to ceiling to the wrong final value. For example, 4 gets converted 4.1 and gets // ceiling'd to 5; in this case the error is 0.1. let errorRight = (appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w) - scaledOutDevicePixels.x) - originalWidth; let errorBottom = (appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h) - scaledOutDevicePixels.y) - originalHeight; // If the error was positive, that means it will ceiling incorrectly, so we need to bump down the // original aDisplayPort.right and/or aDisplayPort.bottom. Again, we back-convert the error amount // with a small fudge factor to figure out how much to adjust the original values. if (errorRight > 0) aDisplayPort.right -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorRight + EXTRA_FUDGE)); if (errorBottom > 0) aDisplayPort.bottom -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorBottom + EXTRA_FUDGE)); // Et voila! return aDisplayPort; }, setScrollClampingSize: function(zoom) { let viewportWidth = gScreenWidth / zoom; let viewportHeight = gScreenHeight / zoom; let screenWidth = gScreenWidth; let screenHeight = gScreenHeight; let [pageWidth, pageHeight] = this.getPageSize(this.browser.contentDocument, viewportWidth, viewportHeight); // Check if the page would fit into either of the viewport dimensions minus // the margins and shrink the screen size accordingly so that the aspect // ratio calculation below works correctly in these situations. // We take away the margin size over two to account for rounding errors, // as the browser size set in updateViewportSize doesn't allow for any // size between these two values (and thus anything between them is // attributable to rounding error). if ((pageHeight * zoom) < gScreenHeight - (gViewportMargins.top + gViewportMargins.bottom) / 2) { screenHeight = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom; viewportHeight = screenHeight / zoom; } if ((pageWidth * zoom) < gScreenWidth - (gViewportMargins.left + gViewportMargins.right) / 2) { screenWidth = gScreenWidth - gViewportMargins.left - gViewportMargins.right; viewportWidth = screenWidth / zoom; } // Make sure the aspect ratio of the screen is maintained when setting // the clamping scroll-port size. let factor = Math.min(viewportWidth / screenWidth, pageWidth / screenWidth, viewportHeight / screenHeight, pageHeight / screenHeight); let scrollPortWidth = screenWidth * factor; let scrollPortHeight = screenHeight * factor; let win = this.browser.contentWindow; win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils). setScrollPositionClampingScrollPortSize(scrollPortWidth, scrollPortHeight); }, setViewport: function(aViewport) { // Transform coordinates based on zoom let x = aViewport.x / aViewport.zoom; let y = aViewport.y / aViewport.zoom; this.setScrollClampingSize(aViewport.zoom); // Adjust the max line box width to be no more than the viewport width, but // only if the reflow-on-zoom preference is enabled. let isZooming = !fuzzyEquals(aViewport.zoom, this._zoom); if (BrowserApp.selectedTab.reflozPinchSeen && isZooming && aViewport.zoom < 1.0) { // In this case, we want to restore the max line box width, // because we are pinch-zooming to zoom out. BrowserEventHandler.resetMaxLineBoxWidth(); BrowserApp.selectedTab.reflozPinchSeen = false; } else if (BrowserApp.selectedTab.reflozPinchSeen && isZooming) { // In this case, the user pinch-zoomed in, so we don't want to // preserve position as we would with reflow-on-zoom. BrowserApp.selectedTab.probablyNeedRefloz = false; BrowserApp.selectedTab._mReflozPoint = null; } if (isZooming && BrowserEventHandler.mReflozPref && BrowserApp.selectedTab._mReflozPoint && BrowserApp.selectedTab.probablyNeedRefloz) { BrowserApp.selectedTab.performReflowOnZoom(aViewport); BrowserApp.selectedTab.probablyNeedRefloz = false; } let win = this.browser.contentWindow; win.scrollTo(x, y); this.userScrollPos.x = win.scrollX; this.userScrollPos.y = win.scrollY; this.setResolution(aViewport.zoom, false); if (aViewport.displayPort) this.setDisplayPort(aViewport.displayPort); // Store fixed margins for later retrieval in getViewport. this._fixedMarginLeft = aViewport.fixedMarginLeft; this._fixedMarginTop = aViewport.fixedMarginTop; this._fixedMarginRight = aViewport.fixedMarginRight; this._fixedMarginBottom = aViewport.fixedMarginBottom; let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); dwi.setContentDocumentFixedPositionMargins( aViewport.fixedMarginTop / aViewport.zoom, aViewport.fixedMarginRight / aViewport.zoom, aViewport.fixedMarginBottom / aViewport.zoom, aViewport.fixedMarginLeft / aViewport.zoom); Services.obs.notifyObservers(null, "after-viewport-change", ""); }, setResolution: function(aZoom, aForce) { // Set zoom level if (aForce || !fuzzyEquals(aZoom, this._zoom)) { this._zoom = aZoom; if (BrowserApp.selectedTab == this) { let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); this._drawZoom = aZoom; cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio); } } }, getPageSize: function(aDocument, aDefaultWidth, aDefaultHeight) { let body = aDocument.body || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight }; let html = aDocument.documentElement || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight }; return [Math.max(body.scrollWidth, html.scrollWidth), Math.max(body.scrollHeight, html.scrollHeight)]; }, getViewport: function() { let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right; let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom; let viewport = { width: screenW, height: screenH, cssWidth: screenW / this._zoom, cssHeight: screenH / this._zoom, pageLeft: 0, pageTop: 0, pageRight: screenW, pageBottom: screenH, // We make up matching css page dimensions cssPageLeft: 0, cssPageTop: 0, cssPageRight: screenW / this._zoom, cssPageBottom: screenH / this._zoom, fixedMarginLeft: this._fixedMarginLeft, fixedMarginTop: this._fixedMarginTop, fixedMarginRight: this._fixedMarginRight, fixedMarginBottom: this._fixedMarginBottom, zoom: this._zoom, }; // Set the viewport offset to current scroll offset viewport.cssX = this.browser.contentWindow.scrollX || 0; viewport.cssY = this.browser.contentWindow.scrollY || 0; // Transform coordinates based on zoom viewport.x = Math.round(viewport.cssX * viewport.zoom); viewport.y = Math.round(viewport.cssY * viewport.zoom); let doc = this.browser.contentDocument; if (doc != null) { let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); let cssPageRect = cwu.getRootBounds(); /* * Avoid sending page sizes of less than screen size before we hit DOMContentLoaded, because * this causes the page size to jump around wildly during page load. After the page is loaded, * send updates regardless of page size; we'll zoom to fit the content as needed. * * In the check below, we floor the viewport size because there might be slight rounding errors * introduced in the CSS page size due to the conversion to and from app units in Gecko. The * error should be no more than one app unit so doing the floor is overkill, but safe in the * sense that the extra page size updates that get sent as a result will be mostly harmless. */ let pageLargerThanScreen = (cssPageRect.width >= Math.floor(viewport.cssWidth)) && (cssPageRect.height >= Math.floor(viewport.cssHeight)); if (doc.readyState === 'complete' || pageLargerThanScreen) { viewport.cssPageLeft = cssPageRect.left; viewport.cssPageTop = cssPageRect.top; viewport.cssPageRight = cssPageRect.right; viewport.cssPageBottom = cssPageRect.bottom; /* Transform the page width and height based on the zoom factor. */ viewport.pageLeft = (viewport.cssPageLeft * viewport.zoom); viewport.pageTop = (viewport.cssPageTop * viewport.zoom); viewport.pageRight = (viewport.cssPageRight * viewport.zoom); viewport.pageBottom = (viewport.cssPageBottom * viewport.zoom); } } return viewport; }, sendViewportUpdate: function(aPageSizeUpdate) { let viewport = this.getViewport(); let displayPort = Services.androidBridge.getDisplayPort(aPageSizeUpdate, BrowserApp.isBrowserContentDocumentDisplayed(), this.id, viewport); if (displayPort != null) this.setDisplayPort(displayPort); }, updateViewportForPageSize: function() { let hasHorizontalMargins = gViewportMargins.left != 0 || gViewportMargins.right != 0; let hasVerticalMargins = gViewportMargins.top != 0 || gViewportMargins.bottom != 0; if (!hasHorizontalMargins && !hasVerticalMargins) { // If there are no margins, then we don't need to do any remeasuring return; } // If the page size has changed so that it might or might not fit on the // screen with the margins included, run updateViewportSize to resize the // browser accordingly. // A page will receive the smaller viewport when its page size fits // within the screen size, so remeasure when the page size remains within // the threshold of screen + margins, in case it's sizing itself relative // to the viewport. let viewport = this.getViewport(); let pageWidth = viewport.pageRight - viewport.pageLeft; let pageHeight = viewport.pageBottom - viewport.pageTop; let remeasureNeeded = false; if (hasHorizontalMargins) { let viewportShouldExcludeHorizontalMargins = (pageWidth <= gScreenWidth - 0.5); if (viewportShouldExcludeHorizontalMargins != this.viewportExcludesHorizontalMargins) { remeasureNeeded = true; } } if (hasVerticalMargins) { let viewportShouldExcludeVerticalMargins = (pageHeight <= gScreenHeight - 0.5); if (viewportShouldExcludeVerticalMargins != this.viewportExcludesVerticalMargins) { remeasureNeeded = true; } } if (remeasureNeeded) { if (!this.viewportMeasureCallback) { this.viewportMeasureCallback = setTimeout(function() { this.viewportMeasureCallback = null; // Re-fetch the viewport as it may have changed between setting the timeout // and running this callback let viewport = this.getViewport(); let pageWidth = viewport.pageRight - viewport.pageLeft; let pageHeight = viewport.pageBottom - viewport.pageTop; if (Math.abs(pageWidth - this.lastPageSizeAfterViewportRemeasure.width) >= 0.5 || Math.abs(pageHeight - this.lastPageSizeAfterViewportRemeasure.height) >= 0.5) { this.updateViewportSize(gScreenWidth); } }.bind(this), kViewportRemeasureThrottle); } } else if (this.viewportMeasureCallback) { // If the page changed size twice since we last measured the viewport and // the latest size change reveals we don't need to remeasure, cancel any // pending remeasure. clearTimeout(this.viewportMeasureCallback); this.viewportMeasureCallback = null; } }, handleEvent: function(aEvent) { switch (aEvent.type) { case "DOMContentLoaded": { let target = aEvent.originalTarget; LoginManagerContent.onContentLoaded(aEvent); // ignore on frames and other documents if (target != this.browser.contentDocument) return; // Sample the background color of the page and pass it along. (This is used to draw the // checkerboard.) Right now we don't detect changes in the background color after this // event fires; it's not clear that doing so is worth the effort. var backgroundColor = null; try { let { contentDocument, contentWindow } = this.browser; let computedStyle = contentWindow.getComputedStyle(contentDocument.body); backgroundColor = computedStyle.backgroundColor; } catch (e) { // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds. } let docURI = target.documentURI; let errorType = ""; if (docURI.startsWith("about:certerror")) errorType = "certerror"; else if (docURI.startsWith("about:blocked")) errorType = "blocked" else if (docURI.startsWith("about:neterror")) errorType = "neterror"; sendMessageToJava({ type: "DOMContentLoaded", tabID: this.id, bgColor: backgroundColor, errorType: errorType }); // Attach a listener to watch for "click" events bubbling up from error // pages and other similar page. This lets us fix bugs like 401575 which // require error page UI to do privileged things, without letting error // pages have any privilege themselves. if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) { this.browser.addEventListener("click", ErrorPageEventHandler, true); let listener = function() { this.browser.removeEventListener("click", ErrorPageEventHandler, true); this.browser.removeEventListener("pagehide", listener, true); }.bind(this); this.browser.addEventListener("pagehide", listener, true); } if (docURI.startsWith("about:reader")) { // During browser restart / recovery, duplicate "DOMContentLoaded" messages are received here // For the visible tab ... where more than one tab is being reloaded, the inital "DOMContentLoaded" // Message can be received before the document body is available ... so we avoid instantiating an // AboutReader object, expecting that an eventual valid message will follow. let contentDocument = this.browser.contentDocument; if (contentDocument.body) { new AboutReader(contentDocument, this.browser.contentWindow); } } break; } case "DOMFormHasPassword": { LoginManagerContent.onFormPassword(aEvent); break; } case "DOMLinkAdded": { let target = aEvent.originalTarget; if (!target.href || target.disabled) return; // Ignore on frames and other documents if (target.ownerDocument != this.browser.contentDocument) return; // Sanitize the rel string let list = []; if (target.rel) { list = target.rel.toLowerCase().split(/\s+/); let hash = {}; list.forEach(function(value) { hash[value] = true; }); list = []; for (let rel in hash) list.push("[" + rel + "]"); } if (list.indexOf("[icon]") != -1) { // We want to get the largest icon size possible for our UI. let maxSize = 0; // We use the sizes attribute if available // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon if (target.hasAttribute("sizes")) { let sizes = target.getAttribute("sizes").toLowerCase(); if (sizes == "any") { // Since Java expects an integer, use -1 to represent icons with sizes="any" maxSize = -1; } else { let tokens = sizes.split(" "); tokens.forEach(function(token) { // TODO: check for invalid tokens let [w, h] = token.split("x"); maxSize = Math.max(maxSize, Math.max(w, h)); }); } } let json = { type: "Link:Favicon", tabID: this.id, href: resolveGeckoURI(target.href), charset: target.ownerDocument.characterSet, title: target.title, rel: list.join(" "), size: maxSize }; sendMessageToJava(json); } else if (list.indexOf("[alternate]") != -1) { let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, ""); let isFeed = (type == "application/rss+xml" || type == "application/atom+xml"); if (!isFeed) return; try { // urlSecurityCeck will throw if things are not OK ContentAreaUtils.urlSecurityCheck(target.href, target.ownerDocument.nodePrincipal, Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); if (!this.browser.feeds) this.browser.feeds = []; this.browser.feeds.push({ href: target.href, title: target.title, type: type }); let json = { type: "Link:Feed", tabID: this.id }; sendMessageToJava(json); } catch (e) {} } else if (list.indexOf("[search]" != -1)) { let type = target.type && target.type.toLowerCase(); // Replace all starting or trailing spaces or spaces before "*;" globally w/ "". type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); // Check that type matches opensearch. let isOpenSearch = (type == "application/opensearchdescription+xml"); if (isOpenSearch && target.title && /^(?:https?|ftp):/i.test(target.href)) { let visibleEngines = Services.search.getVisibleEngines(); // NOTE: Engines are currently identified by name, but this can be changed // when Engines are identified by URL (see bug 335102). if (visibleEngines.some(function(e) { return e.name == target.title; })) { // This engine is already present, do nothing. return; } if (this.browser.engines) { // This engine has already been handled, do nothing. if (this.browser.engines.some(function(e) { return e.url == target.href; })) { return; } } else { this.browser.engines = []; } // Get favicon. let iconURL = target.ownerDocument.documentURIObject.prePath + "/favicon.ico"; let newEngine = { title: target.title, url: target.href, iconURL: iconURL }; this.browser.engines.push(newEngine); // Don't send a message to display engines if we've already handled an engine. if (this.browser.engines.length > 1) return; // Broadcast message that this tab contains search engines that should be visible. let newEngineMessage = { type: "Link:OpenSearch", tabID: this.id, visible: true }; sendMessageToJava(newEngineMessage); } } break; } case "DOMTitleChanged": { if (!aEvent.isTrusted) return; // ignore on frames and other documents if (aEvent.originalTarget != this.browser.contentDocument) return; sendMessageToJava({ type: "DOMTitleChanged", tabID: this.id, title: aEvent.target.title.substring(0, 255) }); break; } case "DOMWindowClose": { if (!aEvent.isTrusted) return; // Find the relevant tab, and close it from Java if (this.browser.contentWindow == aEvent.target) { aEvent.preventDefault(); sendMessageToJava({ type: "Tab:Close", tabID: this.id }); } break; } case "DOMWillOpenModalDialog": { if (!aEvent.isTrusted) return; // We're about to open a modal dialog, make sure the opening // tab is brought to the front. let tab = BrowserApp.getTabForWindow(aEvent.target.top); BrowserApp.selectTab(tab); break; } case "DOMAutoComplete": case "blur": { LoginManagerContent.onUsernameInput(aEvent); break; } case "scroll": { let win = this.browser.contentWindow; if (this.userScrollPos.x != win.scrollX || this.userScrollPos.y != win.scrollY) { this.sendViewportUpdate(); } break; } case "MozScrolledAreaChanged": { // This event is only fired for root scroll frames, and only when the // scrolled area has actually changed, so no need to check for that. // Just make sure it's the event for the correct root scroll frame. if (aEvent.originalTarget != this.browser.contentDocument) return; this.sendViewportUpdate(true); this.updateViewportForPageSize(); break; } case "PluginBindingAttached": { PluginHelper.handlePluginBindingAttached(this, aEvent); break; } case "MozApplicationManifest": { OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView); break; } case "pageshow": { // only send pageshow for the top-level document if (aEvent.originalTarget.defaultView != this.browser.contentWindow) return; sendMessageToJava({ type: "Content:PageShow", tabID: this.id }); if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) { if (!this._linkifier) this._linkifier = new Linkifier(); this._linkifier.linkifyNumbers(this.browser.contentWindow.document); } // Show page actions for helper apps. if (BrowserApp.selectedTab == this) ExternalApps.updatePageAction(this.browser.currentURI); if (!Reader.isEnabledForParseOnLoad) return; // Once document is fully loaded, parse it Reader.parseDocumentFromTab(this.id, function (article) { // Do nothing if there's no article or the page in this tab has // changed let tabURL = this.browser.currentURI.specIgnoringRef; if (article == null || (article.url != tabURL)) { // Don't clear the article for about:reader pages since we want to // use the article from the previous page if (!tabURL.startsWith("about:reader")) { this.savedArticle = null; this.readerEnabled = false; this.readerActive = false; } else { this.readerActive = true; } return; } this.savedArticle = article; sendMessageToJava({ type: "Content:ReaderEnabled", tabID: this.id }); if(this.readerActive) this.readerActive = false; if(!this.readerEnabled) this.readerEnabled = true; }.bind(this)); } } }, onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) { let contentWin = aWebProgress.DOMWindow; if (contentWin != contentWin.top) return; // Filter optimization: Only really send NETWORK state changes to Java listener if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) { // We may receive a document stop event while a document is still loading // (such as when doing URI fixup). Don't notify Java UI in these cases. return; } // Clear page-specific opensearch engines and feeds for a new request. if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) { this.browser.engines = null; // Send message to clear search engine option in context menu. let newEngineMessage = { type: "Link:OpenSearch", tabID: this.id, visible: false }; sendMessageToJava(newEngineMessage); this.browser.feeds = null; } // true if the page loaded successfully (i.e., no 404s or other errors) let success = false; let uri = ""; try { // Remember original URI for UA changes on redirected pages this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI; if (this.originalURI != null) uri = this.originalURI.spec; } catch (e) { } try { success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded; } catch (e) { } // Check to see if we restoring the content from a previous presentation (session) // since there should be no real network activity let restoring = aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING; let showProgress = restoring ? false : shouldShowProgress(uri); let message = { type: "Content:StateChange", tabID: this.id, uri: uri, state: aStateFlags, showProgress: showProgress, success: success }; sendMessageToJava(message); } }, onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) { let contentWin = aWebProgress.DOMWindow; // Browser webapps may load content inside iframes that can not reach across the app/frame boundary // i.e. even though the page is loaded in an iframe window.top != webapp // Make cure this window is a top level tab before moving on. if (BrowserApp.getBrowserForWindow(contentWin) == null) return; this._hostChanged = true; let fixedURI = aLocationURI; try { fixedURI = URIFixup.createExposableURI(aLocationURI); } catch (ex) { } let contentType = contentWin.document.contentType; // If fixedURI matches browser.lastURI, we assume this isn't a real location // change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883. // Note that we have to ensure fixedURI is not the same as aLocationURI so we // don't false-positive page reloads as spurious additions. let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 || ((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI)); this.browser.lastURI = fixedURI; // Reset state of click-to-play plugin notifications. clearTimeout(this.pluginDoorhangerTimeout); this.pluginDoorhangerTimeout = null; this.shouldShowPluginDoorhanger = true; this.clickToPlayPluginsActivated = false; // Borrowed from desktop Firefox: http://mxr.mozilla.org/mozilla-central/source/browser/base/content/urlbarBindings.xml#174 let documentURI = contentWin.document.documentURIObject.spec let matchedURL = documentURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/); let baseDomain = ""; if (matchedURL) { var domain = ""; [, , domain] = matchedURL; try { baseDomain = Services.eTLD.getBaseDomainFromHost(domain); if (!domain.endsWith(baseDomain)) { // getBaseDomainFromHost converts its resultant to ACE. let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService); baseDomain = IDNService.convertACEtoUTF8(baseDomain); } } catch (e) {} } let message = { type: "Content:LocationChange", tabID: this.id, uri: fixedURI.spec, userSearch: this.userSearch || "", baseDomain: baseDomain, aboutHomePage: this.aboutHomePage || "", contentType: (contentType ? contentType : ""), sameDocument: sameDocument }; sendMessageToJava(message); // The search term is only valid for this location change event, so reset it here. this.userSearch = ""; if (!sameDocument) { // XXX This code assumes that this is the earliest hook we have at which // browser.contentDocument is changed to the new document we're loading this.contentDocumentIsDisplayed = false; this.hasTouchListener = false; } else { this.sendViewportUpdate(); } }, // Properties used to cache security state used to update the UI _state: null, _hostChanged: false, // onLocationChange will flip this bit onSecurityChange: function(aWebProgress, aRequest, aState) { // Don't need to do anything if the data we use to update the UI hasn't changed if (this._state == aState && !this._hostChanged) return; this._state = aState; this._hostChanged = false; let identity = IdentityHandler.checkIdentity(aState, this.browser); let message = { type: "Content:SecurityChange", tabID: this.id, identity: identity }; sendMessageToJava(message); }, onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) { }, onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) { }, _sendHistoryEvent: function(aMessage, aParams) { let message = { type: "SessionHistory:" + aMessage, tabID: this.id, }; if (aParams) { if ("url" in aParams) message.url = aParams.url; if ("index" in aParams) message.index = aParams.index; if ("numEntries" in aParams) message.numEntries = aParams.numEntries; } sendMessageToJava(message); }, OnHistoryNewEntry: function(aUri) { this._sendHistoryEvent("New", { url: aUri.spec }); }, OnHistoryGoBack: function(aUri) { this._sendHistoryEvent("Back"); return true; }, OnHistoryGoForward: function(aUri) { this._sendHistoryEvent("Forward"); return true; }, OnHistoryReload: function(aUri, aFlags) { // we don't do anything with this, so don't propagate it // for now anyway return true; }, OnHistoryGotoIndex: function(aIndex, aUri) { this._sendHistoryEvent("Goto", { index: aIndex }); return true; }, OnHistoryPurge: function(aNumEntries) { this._sendHistoryEvent("Purge", { numEntries: aNumEntries }); return true; }, get metadata() { return ViewportHandler.getMetadataForDocument(this.browser.contentDocument); }, /** Update viewport when the metadata changes. */ updateViewportMetadata: function updateViewportMetadata(aMetadata, aInitialLoad) { if (Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) { aMetadata.allowZoom = true; aMetadata.minZoom = aMetadata.maxZoom = NaN; } let scaleRatio = window.devicePixelRatio; if (aMetadata.defaultZoom > 0) aMetadata.defaultZoom *= scaleRatio; if (aMetadata.minZoom > 0) aMetadata.minZoom *= scaleRatio; if (aMetadata.maxZoom > 0) aMetadata.maxZoom *= scaleRatio; aMetadata.isRTL = this.browser.contentDocument.documentElement.dir == "rtl"; ViewportHandler.setMetadataForDocument(this.browser.contentDocument, aMetadata); this.updateViewportSize(gScreenWidth, aInitialLoad); this.sendViewportMetadata(); }, /** Update viewport when the metadata or the window size changes. */ updateViewportSize: function updateViewportSize(aOldScreenWidth, aInitialLoad) { // When this function gets called on window resize, we must execute // this.sendViewportUpdate() so that refreshDisplayPort is called. // Ensure that when making changes to this function that code path // is not accidentally removed (the call to sendViewportUpdate() is // at the very end). if (this.viewportMeasureCallback) { clearTimeout(this.viewportMeasureCallback); this.viewportMeasureCallback = null; } let browser = this.browser; if (!browser) return; let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right; let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom; let viewportW, viewportH; this.viewportExcludesHorizontalMargins = true; this.viewportExcludesVerticalMargins = true; let metadata = this.metadata; if (metadata.autoSize) { viewportW = screenW / window.devicePixelRatio; viewportH = screenH / window.devicePixelRatio; } else { viewportW = metadata.width; viewportH = metadata.height; // If (scale * width) < device-width, increase the width (bug 561413). let maxInitialZoom = metadata.defaultZoom || metadata.maxZoom; if (maxInitialZoom && viewportW) { viewportW = Math.max(viewportW, screenW / maxInitialZoom); } let validW = viewportW > 0; let validH = viewportH > 0; if (!validW) viewportW = validH ? (viewportH * (screenW / screenH)) : BrowserApp.defaultBrowserWidth; if (!validH) viewportH = viewportW * (screenH / screenW); } // Make sure the viewport height is not shorter than the window when // the page is zoomed out to show its full width. Note that before // we set the viewport width, the "full width" of the page isn't properly // defined, so that's why we have to call setBrowserSize twice - once // to set the width, and the second time to figure out the height based // on the layout at that width. let oldBrowserWidth = this.browserWidth; this.setBrowserSize(viewportW, viewportH); // if this page has not been painted yet, then this must be getting run // because a meta-viewport element was added (via the DOMMetaAdded handler). // in this case, we should not do anything that forces a reflow (see bug 759678) // such as requesting the page size or sending a viewport update. this code // will get run again in the before-first-paint handler and that point we // will run though all of it. the reason we even bother executing up to this // point on the DOMMetaAdded handler is so that scripts that use window.innerWidth // before they are painted have a correct value (bug 771575). if (!this.contentDocumentIsDisplayed) { return; } let minScale = 1.0; if (this.browser.contentDocument) { // this may get run during a Viewport:Change message while the document // has not yet loaded, so need to guard against a null document. let [pageWidth, pageHeight] = this.getPageSize(this.browser.contentDocument, viewportW, viewportH); // In the situation the page size equals or exceeds the screen size, // lengthen the viewport on the corresponding axis to include the margins. // The '- 0.5' is to account for rounding errors. if (pageWidth * this._zoom > gScreenWidth - 0.5) { screenW = gScreenWidth; this.viewportExcludesHorizontalMargins = false; } if (pageHeight * this._zoom > gScreenHeight - 0.5) { screenH = gScreenHeight; this.viewportExcludesVerticalMargins = false; } minScale = screenW / pageWidth; } minScale = this.clampZoom(minScale); viewportH = Math.max(viewportH, screenH / minScale); this.setBrowserSize(viewportW, viewportH); // Avoid having the scroll position jump around after device rotation. let win = this.browser.contentWindow; this.userScrollPos.x = win.scrollX; this.userScrollPos.y = win.scrollY; // This change to the zoom accounts for all types of changes I can conceive: // 1. screen size changes, CSS viewport does not (pages with no meta viewport // or a fixed size viewport) // 2. screen size changes, CSS viewport also does (pages with a device-width // viewport) // 3. screen size remains constant, but CSS viewport changes (meta viewport // tag is added or removed) // 4. neither screen size nor CSS viewport changes // // In all of these cases, we maintain how much actual content is visible // within the screen width. Note that "actual content" may be different // with respect to CSS pixels because of the CSS viewport size changing. let zoomScale = (screenW * oldBrowserWidth) / (aOldScreenWidth * viewportW); let zoom = (aInitialLoad && metadata.defaultZoom) ? metadata.defaultZoom : this.clampZoom(this._zoom * zoomScale); this.setResolution(zoom, false); this.setScrollClampingSize(zoom); this.sendViewportUpdate(); // Store the page size that was used to calculate the viewport so that we // can verify it's changed when we consider remeasuring in updateViewportForPageSize let viewport = this.getViewport(); this.lastPageSizeAfterViewportRemeasure = { width: viewport.pageRight - viewport.pageLeft, height: viewport.pageBottom - viewport.pageTop }; }, sendViewportMetadata: function sendViewportMetadata() { let metadata = this.metadata; sendMessageToJava({ type: "Tab:ViewportMetadata", allowZoom: metadata.allowZoom, defaultZoom: metadata.defaultZoom || window.devicePixelRatio, minZoom: metadata.minZoom || 0, maxZoom: metadata.maxZoom || 0, isRTL: metadata.isRTL, tabID: this.id }); }, setBrowserSize: function(aWidth, aHeight) { if (fuzzyEquals(this.browserWidth, aWidth) && fuzzyEquals(this.browserHeight, aHeight)) { return; } this.browserWidth = aWidth; this.browserHeight = aHeight; if (!this.browser.contentWindow) return; let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); cwu.setCSSViewport(aWidth, aHeight); }, /** Takes a scale and restricts it based on this tab's zoom limits. */ clampZoom: function clampZoom(aZoom) { let zoom = ViewportHandler.clamp(aZoom, kViewportMinScale, kViewportMaxScale); let md = this.metadata; if (!md.allowZoom) return md.defaultZoom || zoom; if (md && md.minZoom) zoom = Math.max(zoom, md.minZoom); if (md && md.maxZoom) zoom = Math.min(zoom, md.maxZoom); return zoom; }, observe: function(aSubject, aTopic, aData) { switch (aTopic) { case "before-first-paint": // Is it on the top level? let contentDocument = aSubject; if (contentDocument == this.browser.contentDocument) { if (BrowserApp.selectedTab == this) { BrowserApp.contentDocumentChanged(); } this.contentDocumentIsDisplayed = true; // reset CSS viewport and zoom to default on new page, and then calculate // them properly using the actual metadata from the page. note that the // updateMetadata call takes into account the existing CSS viewport size // and zoom when calculating the new ones, so we need to reset these // things here before calling updateMetadata. this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight); this.setResolution(gScreenWidth / this.browserWidth, false); ViewportHandler.updateMetadata(this, true); // Note that if we draw without a display-port, things can go wrong. By the // time we execute this, it's almost certain a display-port has been set via // the MozScrolledAreaChanged event. If that didn't happen, the updateMetadata // call above does so at the end of the updateViewportSize function. As long // as that is happening, we don't need to do it again here. if (contentDocument.mozSyntheticDocument) { // for images, scale to fit width. this needs to happen *after* the call // to updateMetadata above, because that call sets the CSS viewport which // will affect the page size (i.e. contentDocument.body.scroll*) that we // use in this calculation. also we call sendViewportUpdate after changing // the resolution so that the display port gets recalculated appropriately. let fitZoom = Math.min(gScreenWidth / contentDocument.body.scrollWidth, gScreenHeight / contentDocument.body.scrollHeight); this.setResolution(fitZoom, false); this.sendViewportUpdate(); } } // If the reflow-text-on-page-load pref is enabled, and reflow-on-zoom // is enabled, and our defaultZoom level is set, then we need to get // the default zoom and reflow the text according to the defaultZoom // level. let rzEnabled = BrowserEventHandler.mReflozPref; let rzPl = Services.prefs.getBoolPref("browser.zoom.reflowZoom.reflowTextOnPageLoad"); if (rzEnabled && rzPl) { // Retrieve the viewport width and adjust the max line box width // accordingly. let vp = BrowserApp.selectedTab.getViewport(); BrowserApp.selectedTab.performReflowOnZoom(vp); } break; case "nsPref:changed": if (aData == "browser.ui.zoom.force-user-scalable") ViewportHandler.updateMetadata(this, false); break; } }, set readerEnabled(isReaderEnabled) { this._readerEnabled = isReaderEnabled; if (this.getActive()) Reader.updatePageAction(this); }, get readerEnabled() { return this._readerEnabled; }, set readerActive(isReaderActive) { this._readerActive = isReaderActive; if (this.getActive()) Reader.updatePageAction(this); }, get readerActive() { return this._readerActive; }, // nsIBrowserTab get window() { if (!this.browser) return null; return this.browser.contentWindow; }, get scale() { return this._zoom; }, QueryInterface: XPCOMUtils.generateQI([ Ci.nsIWebProgressListener, Ci.nsISHistoryListener, Ci.nsIObserver, Ci.nsISupportsWeakReference, Ci.nsIBrowserTab ]) }; var BrowserEventHandler = { init: function init() { Services.obs.addObserver(this, "Gesture:SingleTap", false); Services.obs.addObserver(this, "Gesture:CancelTouch", false); Services.obs.addObserver(this, "Gesture:DoubleTap", false); Services.obs.addObserver(this, "Gesture:Scroll", false); Services.obs.addObserver(this, "dom-touch-listener-added", false); BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false); BrowserApp.deck.addEventListener("touchstart", this, true); BrowserApp.deck.addEventListener("click", InputWidgetHelper, true); BrowserApp.deck.addEventListener("click", SelectHelper, true); SpatialNavigation.init(BrowserApp.deck, null); document.addEventListener("MozMagnifyGesture", this, true); Services.prefs.addObserver("browser.zoom.reflowOnZoom", this, false); this.updateReflozPref(); }, resetMaxLineBoxWidth: function() { BrowserApp.selectedTab.probablyNeedRefloz = false; if (gReflowPending) { clearTimeout(gReflowPending); } let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout"); gReflowPending = setTimeout(doChangeMaxLineBoxWidth, reflozTimeout, 0); }, updateReflozPref: function() { this.mReflozPref = Services.prefs.getBoolPref("browser.zoom.reflowOnZoom"); }, handleEvent: function(aEvent) { switch (aEvent.type) { case 'touchstart': this._handleTouchStart(aEvent); break; case 'MozMagnifyGesture': this.observe(this, aEvent.type, JSON.stringify({x: aEvent.screenX, y: aEvent.screenY, zoomDelta: aEvent.delta})); break; } }, _handleTouchStart: function(aEvent) { if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.touches.length > 1 || aEvent.defaultPrevented) return; let closest = aEvent.target; if (closest) { // If we've pressed a scrollable element, let Java know that we may // want to override the scroll behaviour (for document sub-frames) this._scrollableElement = this._findScrollableElement(closest, true); this._firstScrollEvent = true; if (this._scrollableElement != null) { // Discard if it's the top-level scrollable, we let Java handle this let doc = BrowserApp.selectedBrowser.contentDocument; if (this._scrollableElement != doc.body && this._scrollableElement != doc.documentElement) sendMessageToJava({ type: "Panning:Override" }); } } if (!ElementTouchHelper.isElementClickable(closest, null, false)) closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX, aEvent.changedTouches[0].screenY); if (!closest) closest = aEvent.target; if (closest) { let uri = this._getLinkURI(closest); if (uri) { try { Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null); } catch (e) {} } this._doTapHighlight(closest); } }, _getLinkURI: function(aElement) { if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) || (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) { try { return Services.io.newURI(aElement.href, null, null); } catch (e) {} } return null; }, observe: function(aSubject, aTopic, aData) { if (aTopic == "dom-touch-listener-added") { let tab = BrowserApp.getTabForWindow(aSubject.top); if (!tab || tab.hasTouchListener) return; tab.hasTouchListener = true; sendMessageToJava({ type: "Tab:HasTouchListener", tabID: tab.id }); return; } else if (aTopic == "nsPref:changed") { if (aData == "browser.zoom.reflowOnZoom") { this.updateReflozPref(); } return; } // the remaining events are all dependent on the browser content document being the // same as the browser displayed document. if they are not the same, we should ignore // the event. if (BrowserApp.isBrowserContentDocumentDisplayed()) { this.handleUserEvent(aTopic, aData); } }, handleUserEvent: function(aTopic, aData) { switch (aTopic) { case "Gesture:Scroll": { // If we've lost our scrollable element, return. Don't cancel the // override, as we probably don't want Java to handle panning until the // user releases their finger. if (this._scrollableElement == null) return; // If this is the first scroll event and we can't scroll in the direction // the user wanted, and neither can any non-root sub-frame, cancel the // override so that Java can handle panning the main document. let data = JSON.parse(aData); // round the scroll amounts because they come in as floats and might be // subject to minor rounding errors because of zoom values. I've seen values // like 0.99 come in here and get truncated to 0; this avoids that problem. let zoom = BrowserApp.selectedTab._zoom; let x = Math.round(data.x / zoom); let y = Math.round(data.y / zoom); if (this._firstScrollEvent) { while (this._scrollableElement != null && !this._elementCanScroll(this._scrollableElement, x, y)) this._scrollableElement = this._findScrollableElement(this._scrollableElement, false); let doc = BrowserApp.selectedBrowser.contentDocument; if (this._scrollableElement == null || this._scrollableElement == doc.body || this._scrollableElement == doc.documentElement) { sendMessageToJava({ type: "Panning:CancelOverride" }); return; } this._firstScrollEvent = false; } // Scroll the scrollable element if (this._elementCanScroll(this._scrollableElement, x, y)) { this._scrollElementBy(this._scrollableElement, x, y); sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: true }); SelectionHandler.subdocumentScrolled(this._scrollableElement); } else { sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: false }); } break; } case "Gesture:CancelTouch": this._cancelTapHighlight(); break; case "Gesture:SingleTap": { let element = this._highlightElement; if (element) { try { let data = JSON.parse(aData); let [x, y] = [data.x, data.y]; if (ElementTouchHelper.isElementClickable(element)) { [x, y] = this._moveClickPoint(element, x, y); element = ElementTouchHelper.anyElementFromPoint(x, y); } // Was the element already focused before it was clicked? let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser)); this._sendMouseEvent("mousemove", element, x, y); this._sendMouseEvent("mousedown", element, x, y); this._sendMouseEvent("mouseup", element, x, y); // If the element was previously focused, show the caret attached to it. if (isFocused) SelectionHandler.attachCaret(element); // scrollToFocusedInput does its own checks to find out if an element should be zoomed into BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser); } catch(e) { Cu.reportError(e); } } this._cancelTapHighlight(); break; } case"Gesture:DoubleTap": this._cancelTapHighlight(); this.onDoubleTap(aData); break; case "MozMagnifyGesture": this.onPinchFinish(aData); break; default: dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"'); break; } }, _zoomOut: function() { BrowserEventHandler.resetMaxLineBoxWidth(); sendMessageToJava({ type: "Browser:ZoomToPageWidth" }); }, _isRectZoomedIn: function(aRect, aViewport) { // This function checks to see if the area of the rect visible in the // viewport (i.e. the "overlapArea" variable below) is approximately // the max area of the rect we can show. It also checks that the rect // is actually on-screen by testing the left and right edges of the rect. // In effect, this tells us whether or not zooming in to this rect // will significantly change what the user is seeing. const minDifference = -20; const maxDifference = 20; const maxZoomAllowed = 4; // keep this in sync with mobile/android/base/ui/PanZoomController.MAX_ZOOM let vRect = new Rect(aViewport.cssX, aViewport.cssY, aViewport.cssWidth, aViewport.cssHeight); let overlap = vRect.intersect(aRect); let overlapArea = overlap.width * overlap.height; let availHeight = Math.min(aRect.width * vRect.height / vRect.width, aRect.height); let showing = overlapArea / (aRect.width * availHeight); let dw = (aRect.width - vRect.width); let dx = (aRect.x - vRect.x); if (fuzzyEquals(aViewport.zoom, maxZoomAllowed) && overlap.width / aRect.width > 0.9) { // we're already at the max zoom and the block is not spilling off the side of the screen so that even // if the block isn't taking up most of the viewport we can't pan/zoom in any more. return true so that we zoom out return true; } return (showing > 0.9 && dx > minDifference && dx < maxDifference && dw > minDifference && dw < maxDifference); }, onDoubleTap: function(aData) { let data = JSON.parse(aData); let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y); // We only want to do this if reflow-on-zoom is enabled, we don't already // have a reflow-on-zoom event pending, and the element upon which the user // double-tapped isn't of a type we want to avoid reflow-on-zoom. if (BrowserEventHandler.mReflozPref && !BrowserApp.selectedTab._mReflozPoint && !this._shouldSuppressReflowOnZoom(element)) { let data = JSON.parse(aData); let zoomPointX = data.x; let zoomPointY = data.y; BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY, range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) }; BrowserApp.selectedTab.probablyNeedRefloz = true; } if (!element) { this._zoomOut(); return; } while (element && !this._shouldZoomToElement(element)) element = element.parentNode; if (!element) { this._zoomOut(); } else { this._zoomToElement(element, data.y); } }, /** * Determine if reflow-on-zoom functionality should be suppressed, given a * particular element. Double-tapping on the following elements suppresses * reflow-on-zoom: * *