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