diff -r 000000000000 -r 6474c204b198 mobile/android/chrome/content/browser.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/chrome/content/browser.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,8558 @@ +#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/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, "sendMessageToJava", + "resource://gre/modules/Messaging.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, "SSLExceptions", + "resource://gre/modules/SSLExceptions.jsm"); + +XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", + "resource://gre/modules/FormHistory.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery", + "resource://gre/modules/SimpleServiceDiscovery.jsm"); + +#ifdef NIGHTLY_BUILD +XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils", + "resource://shumway/ShumwayUtils.jsm"); +#endif + +#ifdef MOZ_ANDROID_SYNTHAPKS +XPCOMUtils.defineLazyModuleGetter(this, "WebappManager", + "resource://gre/modules/WebappManager.jsm"); +#endif + +XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", + "resource://gre/modules/CharsetMenu.jsm"); + +// Lazily-loaded browser scripts: +[ + ["SelectHelper", "chrome://browser/content/SelectHelper.js"], + ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"], + ["AboutReader", "chrome://browser/content/aboutReader.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"], + ["ZoomHelper", "chrome://browser/content/ZoomHelper.js"], + ["CastingApps", "chrome://browser/content/CastingApps.js"], +].forEach(function (aScript) { + let [name, script] = aScript; + XPCOMUtils.defineLazyGetter(window, name, function() { + let sandbox = {}; + Services.scriptloader.loadSubScript(script, sandbox); + return sandbox[name]; + }); +}); + +[ +#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) { + Services.obs.addObserver(function(s, t, d) { + window[name].observe(s, t, d) + }, aNotification, false); + }); +}); + +// Lazily-loaded JS modules that use observer notifications +[ + ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView", + "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"], +].forEach(module => { + let [name, notifications, resource] = module; + XPCOMUtils.defineLazyModuleGetter(this, name, resource); + notifications.forEach(notification => { + Services.obs.addObserver((s,t,d) => { + this[name].observe(s,t,d) + }, notification, false); + }); +}); + +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 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; + } + + try { + docViewer.pausePainting(); + docViewer.changeMaxLineBoxWidth(aWidth); + + if (range) { + ZoomHelper.zoomInAndSnapToRange(range); + } else { + // In this case, we actually didn't zoom into a specific range. It + // probably happened from a page load reflow-on-zoom event, so we + // need to make sure painting is re-enabled. + BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); + } + } finally { + docViewer.resumePainting(); + } +} + +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; +} + +/** + * Cache of commonly used string bundles. + */ +var Strings = {}; +[ + ["brand", "chrome://branding/locale/brand.properties"], + ["browser", "chrome://browser/locale/browser.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"); + this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() { + try { + BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false); + Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); + sendMessageToJava({ type: "Gecko:DelayedStartup" }); + } catch(ex) { console.log(ex); } + }, false); + + 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); +#ifdef MOZ_ANDROID_SYNTHAPKS + Services.obs.addObserver(this, "webapps-runtime-install", false); + Services.obs.addObserver(this, "webapps-runtime-install-package", false); + Services.obs.addObserver(this, "webapps-ask-install", false); + Services.obs.addObserver(this, "webapps-launch", false); + Services.obs.addObserver(this, "webapps-uninstall", false); + Services.obs.addObserver(this, "Webapps:AutoInstall", false); + Services.obs.addObserver(this, "Webapps:Load", false); + Services.obs.addObserver(this, "Webapps:AutoUninstall", false); +#endif + 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(); +#ifdef MOZ_ANDROID_SYNTHAPKS + // TODO: replace with Android implementation of WebappOSUtils.isLaunchable. + Cu.import("resource://gre/modules/Webapps.jsm"); + DOMApplicationRegistry.allAppsLaunchable = true; +#else + WebappsUI.init(); +#endif + RemoteDebugger.init(); + Reader.init(); + UserAgentOverrides.init(); + DesktopUserAgent.init(); + CastingApps.init(); + Distribution.init(); + Tabs.init(); +#ifdef ACCESSIBILITY + AccessFu.attach(window); +#endif +#ifdef NIGHTLY_BUILD + ShumwayUtils.init(); +#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]; + } + + if (pinned) { + this._initRuntime(this._startupStatus, url, aUrl => this.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 (this._startupStatus) + 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 + }, + + get _startupStatus() { + delete this._startupStatus; + + let savedMilestone = null; + try { + savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone"); + } catch (e) { + } +#expand let ourMilestone = "__MOZ_APP_VERSION__"; + this._startupStatus = ""; + if (ourMilestone != savedMilestone) { + Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone); + this._startupStatus = savedMilestone ? "upgrade" : "new"; + } + + return this._startupStatus; + }, + + /** + * Pass this a locale string, such as "fr" or "es_ES". + */ + setLocale: function (locale) { + console.log("browser.js: requesting locale set: " + locale); + sendMessageToJava({ type: "Locale:Set", locale: locale }); + }, + + _initRuntime: function(status, url, callback) { + let sandbox = {}; + Services.scriptloader.loadSubScript("chrome://browser/content/WebappRT.js", sandbox); + window.WebappRT = sandbox.WebappRT; + WebappRT.init(status, url, callback); + }, + + 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) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab"); + UITelemetry.addEvent("loadurl.1", "contextmenu", null); + + let url = NativeWindow.contextmenus._getLinkURL(aTarget); + ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal); + 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) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_private_tab"); + UITelemetry.addEvent("loadurl.1", "contextmenu", null); + + let url = NativeWindow.contextmenus._getLinkURL(aTarget); + ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal); + 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) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_link"); + + let url = NativeWindow.contextmenus._getLinkURL(aTarget); + NativeWindow.contextmenus._copyStringToDefaultClipboard(url); + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyEmailAddress"), + NativeWindow.contextmenus.emailLinkContext, + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_email"); + + let url = NativeWindow.contextmenus._getLinkURL(aTarget); + let emailAddr = NativeWindow.contextmenus._stripScheme(url); + NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr); + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyPhoneNumber"), + NativeWindow.contextmenus.phoneNumberLinkContext, + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_phone"); + + let url = NativeWindow.contextmenus._getLinkURL(aTarget); + let phoneNumber = NativeWindow.contextmenus._stripScheme(url); + NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber); + }); + + NativeWindow.contextmenus.add({ + label: Strings.browser.GetStringFromName("contextmenu.shareLink"), + order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items + selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkShareableContext), + showAsActions: function(aElement) { + return { + title: aElement.textContent.trim() || aElement.title.trim(), + uri: NativeWindow.contextmenus._getLinkURL(aElement), + }; + }, + icon: "drawable://ic_menu_share", + callback: function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link"); + } + }); + + NativeWindow.contextmenus.add({ + label: Strings.browser.GetStringFromName("contextmenu.shareEmailAddress"), + order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, + selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext), + showAsActions: function(aElement) { + let url = NativeWindow.contextmenus._getLinkURL(aElement); + let emailAddr = NativeWindow.contextmenus._stripScheme(url); + let title = aElement.textContent || aElement.title; + return { + title: title, + uri: emailAddr, + }; + }, + icon: "drawable://ic_menu_share", + callback: function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email"); + } + }); + + NativeWindow.contextmenus.add({ + label: Strings.browser.GetStringFromName("contextmenu.sharePhoneNumber"), + order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, + selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext), + showAsActions: function(aElement) { + let url = NativeWindow.contextmenus._getLinkURL(aElement); + let phoneNumber = NativeWindow.contextmenus._stripScheme(url); + let title = aElement.textContent || aElement.title; + return { + title: title, + uri: phoneNumber, + }; + }, + icon: "drawable://ic_menu_share", + callback: function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone"); + } + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"), + NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext), + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email"); + + 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) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone"); + + let url = NativeWindow.contextmenus._getLinkURL(aTarget); + sendMessageToJava({ + type: "Contact:Add", + phone: url + }); + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"), + NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkBookmarkableContext), + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark"); + + 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) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_play"); + aTarget.play(); + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.pauseMedia"), + NativeWindow.contextmenus.mediaContext("media-playing"), + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_pause"); + aTarget.pause(); + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.showControls2"), + NativeWindow.contextmenus.mediaContext("media-hidingcontrols"), + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media"); + aTarget.setAttribute("controls", true); + }); + + NativeWindow.contextmenus.add({ + label: Strings.browser.GetStringFromName("contextmenu.shareMedia"), + order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, + selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.SelectorContext("video")), + showAsActions: function(aElement) { + let url = (aElement.currentSrc || aElement.src); + let title = aElement.textContent || aElement.title; + return { + title: title, + uri: url, + type: "video/*", + }; + }, + icon: "drawable://ic_menu_share", + callback: function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media"); + } + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.fullScreen"), + NativeWindow.contextmenus.SelectorContext("video:not(:-moz-full-screen)"), + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen"); + aTarget.mozRequestFullScreen(); + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.mute"), + NativeWindow.contextmenus.mediaContext("media-unmuted"), + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute"); + aTarget.muted = true; + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"), + NativeWindow.contextmenus.mediaContext("media-muted"), + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute"); + aTarget.muted = false; + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyImageLocation"), + NativeWindow.contextmenus.imageLocationCopyableContext, + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image"); + + let url = aTarget.src; + NativeWindow.contextmenus._copyStringToDefaultClipboard(url); + }); + + NativeWindow.contextmenus.add({ + label: Strings.browser.GetStringFromName("contextmenu.shareImage"), + selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext), + order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items + showAsActions: function(aTarget) { + let doc = aTarget.ownerDocument; + let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools) + .getImgCacheForDocument(doc); + let props = imageCache.findEntryProperties(aTarget.currentURI, doc.characterSet); + let src = aTarget.src; + return { + title: src, + uri: src, + type: "image/*", + }; + }, + icon: "drawable://ic_menu_share", + menu: true, + callback: function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image"); + } + }); + + NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.saveImage"), + NativeWindow.contextmenus.imageSaveableContext, + function(aTarget) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image"); + + 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) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image"); + + 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) { + UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media"); + + let url = aTarget.currentSrc || aTarget.src; + let filePickerTitleKey = (aTarget instanceof HTMLVideoElement && + (aTarget.videoWidth != 0 && aTarget.videoHeight != 0)) + ? "SaveVideoTitle" : "SaveAudioTitle"; + // Skipped trying to pull MIME type out of cache for now + ContentAreaUtils.internalSave(url, null, null, null, null, false, + filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject, + aTarget.ownerDocument, true, null); + }); + }, + + 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(); +#ifndef MOZ_ANDROID_SYNTHAPKS + WebappsUI.uninit(); +#endif + RemoteDebugger.uninit(); + Reader.uninit(); + UserAgentOverrides.uninit(); + DesktopUserAgent.uninit(); + ExternalApps.uninit(); + CastingApps.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) { + this._selectedTab.setActive(false); + } + + this._selectedTab = aTab; + if (!aTab) + return; + + 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 ("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); + + 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); + }, + +#ifdef MOZ_ANDROID_SYNTHAPKS + _loadWebapp: function(aMessage) { + + this._initRuntime(this._startupStatus, aMessage.url, aUrl => { + this.manifestUrl = aMessage.url; + this.addTab(aUrl, { title: aMessage.name }); + }); + }, +#endif + + // 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 + } + + 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": + case "home.sync.updateMode": + 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": + case "home.sync.updateMode": + json.type = "int"; + json.value = parseInt(json.value); + break; + } + + 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 "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; + // ZoomHelper.zoomToElement will handle not sending any message if this input is already mostly filling the screen + ZoomHelper.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 | + Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; + 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), + 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; + +#ifdef MOZ_ANDROID_SYNTHAPKS + case "webapps-runtime-install": + WebappManager.install(JSON.parse(aData), aSubject); + break; + + case "webapps-runtime-install-package": + WebappManager.installPackage(JSON.parse(aData), aSubject); + break; + + case "webapps-ask-install": + WebappManager.askInstall(JSON.parse(aData)); + break; + + case "webapps-launch": { + WebappManager.launch(JSON.parse(aData)); + break; + } + + case "webapps-uninstall": { + WebappManager.uninstall(JSON.parse(aData)); + break; + } + + case "Webapps:AutoInstall": + WebappManager.autoInstall(JSON.parse(aData)); + break; + + case "Webapps:Load": + this._loadWebapp(JSON.parse(aData)); + break; + + case "Webapps:AutoUninstall": + WebappManager.autoUninstall(JSON.parse(aData)); + break; +#endif + + case "Locale:Changed": + // The value provided to Locale:Changed should be a BCP47 language tag + // understood by Gecko -- for example, "es-ES" or "de". + console.log("Locale:Changed: " + aData); + + // 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); + }, + + getUITelemetryObserver: function() { + return UITelemetry; + }, + + 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 + DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items + + init: function() { + Services.obs.addObserver(this, "Gesture:LongPress", false); + }, + + uninit: function() { + Services.obs.removeObserver(this, "Gesture:LongPress"); + }, + + add: function() { + let args; + if (arguments.length == 1) { + args = arguments[0]; + } else if (arguments.length == 3) { + args = { + label : arguments[0], + selector: arguments[1], + callback: arguments[2] + }; + } else { + throw "Incorrect number of parameters"; + } + + if (!args.label) + throw "Menu items must have a name"; + + let cmItem = new ContextMenuItem(args); + this.items[cmItem.id] = cmItem; + return cmItem.id; + }, + + remove: function(aId) { + delete this.items[aId]; + }, + + 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; + } + }, + + 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; + } + }; + }, + + /* Holds a WeakRef to the original target element this context menu was shown for. + * Most API's will have to walk up the tree from this node to find the correct element + * to act on + */ + get _target() { + if (this._targetRef) + return this._targetRef.get(); + return null; + }, + + set _target(aTarget) { + if (aTarget) + this._targetRef = Cu.getWeakReference(aTarget); + else this._targetRef = null; + }, + + get defaultContext() { + delete this.defaultContext; + return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default"); + }, + + /* Gets menuitems for an arbitrary node + * Parameters: + * element - The element to look at. If this element has a contextmenu attribute, the + * corresponding contextmenu will be used. + */ + _getHTMLContextMenuItemsForElement: function(element) { + let htmlMenu = element.contextMenu; + if (!htmlMenu) { + return []; + } + + htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu); + htmlMenu.sendShowEvent(); + + return this._getHTMLContextMenuItemsForMenu(htmlMenu, element); + }, + + /* Add a menuitem for an HTML