1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 ChromeUtils.defineESModuleGetters(lazy, {
8 AboutReaderParent: "resource:///actors/AboutReaderParent.sys.mjs",
9 BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
10 EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
12 "resource:///modules/asrouter/FeatureCalloutBroker.sys.mjs",
13 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
14 clearTimeout: "resource://gre/modules/Timer.sys.mjs",
15 setTimeout: "resource://gre/modules/Timer.sys.mjs",
18 ChromeUtils.defineLazyGetter(lazy, "log", () => {
19 const { Logger } = ChromeUtils.importESModule(
20 "resource://messaging-system/lib/Logger.sys.mjs"
22 return new Logger("ASRouterTriggerListeners");
25 const FEW_MINUTES = 15 * 60 * 1000; // 15 mins
27 function isPrivateWindow(win) {
29 !(win instanceof Ci.nsIDOMWindow) ||
31 lazy.PrivateBrowsingUtils.isWindowPrivate(win)
36 * Check current location against the list of allowed hosts
37 * Additionally verify for redirects and check original request URL against
40 * @returns {object} - {host, url} pair that matched the list of allowed hosts
42 function checkURLMatch(aLocationURI, { hosts, matchPatternSet }, aRequest) {
43 // If checks pass we return a match
46 match = { host: aLocationURI.host, url: aLocationURI.spec };
48 // nsIURI.host can throw for non-nsStandardURL nsIURIs
52 // Check current location against allowed hosts
53 if (hosts.has(match.host)) {
57 if (matchPatternSet) {
58 if (matchPatternSet.matches(match.url)) {
63 // Nothing else to check, return early
68 // The original URL at the start of the request
69 const originalLocation = aRequest.QueryInterface(Ci.nsIChannel).originalURI;
70 // We have been redirected
71 if (originalLocation.spec !== aLocationURI.spec) {
73 hosts.has(originalLocation.host) && {
74 host: originalLocation.host,
75 url: originalLocation.spec,
83 function createMatchPatternSet(patterns, flags) {
85 return new MatchPatternSet(new Set(patterns), flags);
89 return new MatchPatternSet([]);
93 * A Map from trigger IDs to singleton trigger listeners. Each listener must
94 * have idempotent `init` and `uninit` methods.
96 export const ASRouterTriggerListeners = new Map([
100 id: "openArticleURL",
102 _triggerHandler: null,
104 _matchPatternSet: null,
105 readerModeEvent: "Reader:UpdateReaderButton",
107 init(triggerHandler, hosts, patterns) {
108 if (!this._initialized) {
109 this.receiveMessage = this.receiveMessage.bind(this);
110 lazy.AboutReaderParent.addMessageListener(this.readerModeEvent, this);
111 this._triggerHandler = triggerHandler;
112 this._initialized = true;
115 this._matchPatternSet = createMatchPatternSet([
116 ...(this._matchPatternSet ? this._matchPatternSet.patterns : []),
121 hosts.forEach(h => this._hosts.add(h));
125 receiveMessage({ data, target }) {
126 if (data && data.isArticle) {
127 const match = checkURLMatch(target.currentURI, {
129 matchPatternSet: this._matchPatternSet,
132 this._triggerHandler(target, { id: this.id, param: match });
138 if (this._initialized) {
139 lazy.AboutReaderParent.removeMessageListener(
140 this.readerModeEvent,
143 this._initialized = false;
144 this._triggerHandler = null;
145 this._hosts = new Set();
146 this._matchPatternSet = null;
154 id: "openBookmarkedURL",
156 _triggerHandler: null,
158 bookmarkEvent: "bookmark-icon-updated",
160 init(triggerHandler) {
161 if (!this._initialized) {
162 Services.obs.addObserver(this, this.bookmarkEvent);
163 this._triggerHandler = triggerHandler;
164 this._initialized = true;
168 observe(subject, topic, data) {
169 if (topic === this.bookmarkEvent && data === "starred") {
170 const browser = Services.wm.getMostRecentBrowserWindow();
172 this._triggerHandler(browser.gBrowser.selectedBrowser, {
180 if (this._initialized) {
181 Services.obs.removeObserver(this, this.bookmarkEvent);
182 this._initialized = false;
183 this._triggerHandler = null;
184 this._hosts = new Set();
192 id: "frequentVisits",
194 _triggerHandler: null,
196 _matchPatternSet: null,
199 init(triggerHandler, hosts = [], patterns) {
200 if (!this._initialized) {
201 this.onTabSwitch = this.onTabSwitch.bind(this);
202 lazy.EveryWindow.registerCallback(
205 if (!isPrivateWindow(win)) {
206 win.addEventListener("TabSelect", this.onTabSwitch);
207 win.gBrowser.addTabsProgressListener(this);
211 if (!isPrivateWindow(win)) {
212 win.removeEventListener("TabSelect", this.onTabSwitch);
213 win.gBrowser.removeTabsProgressListener(this);
217 this._visits = new Map();
218 this._initialized = true;
220 this._triggerHandler = triggerHandler;
222 this._matchPatternSet = createMatchPatternSet([
223 ...(this._matchPatternSet ? this._matchPatternSet.patterns : []),
228 hosts.forEach(h => this._hosts.add(h));
230 this._hosts = new Set(hosts); // Clone the hosts to avoid unexpected behaviour
234 /* _updateVisits - Record visit timestamps for websites that match `this._hosts` and only
235 * if it's been more than FEW_MINUTES since the last visit.
236 * @param {string} host - Location host of current selected tab
237 * @returns {boolean} - If the new visit has been recorded
239 _updateVisits(host) {
240 const visits = this._visits.get(host);
242 if (visits && Date.now() - visits[0] > FEW_MINUTES) {
243 this._visits.set(host, [Date.now(), ...visits]);
247 this._visits.set(host, [Date.now()]);
255 if (!event.target.ownerGlobal.gBrowser) {
259 const { gBrowser } = event.target.ownerGlobal;
260 const match = checkURLMatch(gBrowser.currentURI, {
262 matchPatternSet: this._matchPatternSet,
265 this.triggerHandler(gBrowser.selectedBrowser, match);
269 triggerHandler(aBrowser, match) {
270 const updated = this._updateVisits(match.host);
272 // If the previous visit happend less than FEW_MINUTES ago
273 // no updates were made, no need to trigger the handler
278 this._triggerHandler(aBrowser, {
282 // Remapped to {host, timestamp} because JEXL operators can only
283 // filter over collections (arrays of objects)
284 recentVisits: this._visits
286 .map(timestamp => ({ host: match.host, timestamp })),
291 onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) {
292 // Some websites trigger redirect events after they finish loading even
293 // though the location remains the same. This results in onLocationChange
294 // events to be fired twice.
295 const isSameDocument = !!(
296 aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
298 if (aWebProgress.isTopLevel && !isSameDocument) {
299 const match = checkURLMatch(
301 { hosts: this._hosts, matchPatternSet: this._matchPatternSet },
305 this.triggerHandler(aBrowser, match);
311 if (this._initialized) {
312 lazy.EveryWindow.unregisterCallback(this.id);
314 this._initialized = false;
315 this._triggerHandler = null;
317 this._matchPatternSet = null;
325 * Attach listeners to every browser window to detect location changes, and
326 * notify the trigger handler whenever we navigate to a URL with a hostname
334 _triggerHandler: null,
336 _matchPatternSet: null,
340 * If the listener is already initialised, `init` will replace the trigger
341 * handler and add any new hosts to `this._hosts`.
343 init(triggerHandler, hosts = [], patterns) {
344 if (!this._initialized) {
345 this.onLocationChange = this.onLocationChange.bind(this);
346 lazy.EveryWindow.registerCallback(
349 if (!isPrivateWindow(win)) {
350 win.gBrowser.addTabsProgressListener(this);
354 if (!isPrivateWindow(win)) {
355 win.gBrowser.removeTabsProgressListener(this);
360 this._visits = new Map();
361 this._initialized = true;
363 this._triggerHandler = triggerHandler;
365 this._matchPatternSet = createMatchPatternSet([
366 ...(this._matchPatternSet ? this._matchPatternSet.patterns : []),
371 hosts.forEach(h => this._hosts.add(h));
373 this._hosts = new Set(hosts); // Clone the hosts to avoid unexpected behaviour
378 if (this._initialized) {
379 lazy.EveryWindow.unregisterCallback(this.id);
381 this._initialized = false;
382 this._triggerHandler = null;
384 this._matchPatternSet = null;
389 onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI, aFlags) {
390 // Some websites trigger redirect events after they finish loading even
391 // though the location remains the same. This results in onLocationChange
392 // events to be fired twice.
393 const isSameDocument = !!(
394 aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
396 if (aWebProgress.isTopLevel && !isSameDocument) {
397 const match = checkURLMatch(
399 { hosts: this._hosts, matchPatternSet: this._matchPatternSet },
403 let visitsCount = (this._visits.get(match.url) || 0) + 1;
404 this._visits.set(match.url, visitsCount);
405 this._triggerHandler(aBrowser, {
408 context: { visitsCount },
417 * Add an observer notification to notify the trigger handler whenever the user
418 * saves or updates a login via the login capture doorhanger.
424 _triggerHandler: null,
427 * If the listener is already initialised, `init` will replace the trigger
430 init(triggerHandler) {
431 if (!this._initialized) {
432 Services.obs.addObserver(this, "LoginStats:NewSavedPassword");
433 Services.obs.addObserver(this, "LoginStats:LoginUpdateSaved");
434 this._initialized = true;
436 this._triggerHandler = triggerHandler;
440 if (this._initialized) {
441 Services.obs.removeObserver(this, "LoginStats:NewSavedPassword");
442 Services.obs.removeObserver(this, "LoginStats:LoginUpdateSaved");
444 this._initialized = false;
445 this._triggerHandler = null;
449 observe(aSubject, aTopic, aData) {
450 if (aSubject.currentURI.asciiHost === "accounts.firefox.com") {
451 // Don't notify about saved logins on the FxA login origin since this
452 // trigger is used to promote login Sync and getting a recommendation
453 // to enable Sync during the sign up process is a bad UX.
458 case "LoginStats:NewSavedPassword": {
459 this._triggerHandler(aSubject, {
461 context: { type: "save" },
465 case "LoginStats:LoginUpdateSaved": {
466 this._triggerHandler(aSubject, {
468 context: { type: "update" },
473 throw new Error(`Unexpected observer notification: ${aTopic}`);
484 _triggerHandler: null,
485 _triggerDelay: 10000, // 10 second delay before triggering
486 _topic: "formautofill-storage-changed",
487 _events: ["add", "update", "notifyUsed"] /** @see AutofillRecords */,
488 _collections: ["addresses", "creditCards"] /** @see AutofillRecords */,
490 init(triggerHandler) {
491 if (!this._initialized) {
492 Services.obs.addObserver(this, this._topic);
493 this._initialized = true;
495 this._triggerHandler = triggerHandler;
499 if (this._initialized) {
500 Services.obs.removeObserver(this, this._topic);
501 this._initialized = false;
502 this._triggerHandler = null;
506 observe(subject, topic, data) {
508 Services.wm.getMostRecentBrowserWindow()?.gBrowser.selectedBrowser;
511 topic !== this._topic ||
512 !subject.wrappedJSObject ||
513 // Ignore changes caused by manual edits in the credit card/address
514 // managers in about:preferences.
515 browser.contentWindow?.gSubDialog?.dialogs.length
519 let { sourceSync, collectionName } = subject.wrappedJSObject;
520 // Ignore changes from sync and changes to untracked collections.
521 if (sourceSync || !this._collections.includes(collectionName)) {
524 if (this._events.includes(data)) {
526 let type = collectionName;
527 if (event === "notifyUsed") {
530 if (type === "creditCards") {
533 if (type === "addresses") {
536 lazy.setTimeout(() => {
539 // Make sure the browser still exists and is still selected.
540 browser.isConnectedAndReady &&
542 Services.wm.getMostRecentBrowserWindow()?.gBrowser
545 this._triggerHandler(browser, {
547 context: { event, type },
550 }, this._triggerDelay);
560 _triggerHandler: null,
563 onLocationChange: null,
565 init(triggerHandler, params, patterns) {
566 params.forEach(p => this._events.push(p));
568 if (!this._initialized) {
569 Services.obs.addObserver(this, "SiteProtection:ContentBlockingEvent");
570 Services.obs.addObserver(
572 "SiteProtection:ContentBlockingMilestone"
574 this.onLocationChange = this._onLocationChange.bind(this);
575 lazy.EveryWindow.registerCallback(
578 if (!isPrivateWindow(win)) {
579 win.gBrowser.addTabsProgressListener(this);
583 if (!isPrivateWindow(win)) {
584 win.gBrowser.removeTabsProgressListener(this);
589 this._initialized = true;
591 this._triggerHandler = triggerHandler;
595 if (this._initialized) {
596 Services.obs.removeObserver(
598 "SiteProtection:ContentBlockingEvent"
600 Services.obs.removeObserver(
602 "SiteProtection:ContentBlockingMilestone"
604 lazy.EveryWindow.unregisterCallback(this.id);
605 this.onLocationChange = null;
606 this._initialized = false;
608 this._triggerHandler = null;
610 this._sessionPageLoad = 0;
613 observe(aSubject, aTopic, aData) {
615 case "SiteProtection:ContentBlockingEvent":
616 const { browser, host, event } = aSubject.wrappedJSObject;
617 if (this._events.filter(e => (e & event) === e).length) {
618 this._triggerHandler(browser, {
619 id: "contentBlocking",
625 pageLoad: this._sessionPageLoad,
630 case "SiteProtection:ContentBlockingMilestone":
631 if (this._events.includes(aSubject.wrappedJSObject.event)) {
632 this._triggerHandler(
633 Services.wm.getMostRecentBrowserWindow().gBrowser
636 id: "contentBlocking",
638 pageLoad: this._sessionPageLoad,
641 type: aSubject.wrappedJSObject.event,
657 // Some websites trigger redirect events after they finish loading even
658 // though the location remains the same. This results in onLocationChange
659 // events to be fired twice.
660 const isSameDocument = !!(
661 aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT
664 ["http", "https"].includes(aLocationURI.scheme) &&
665 aWebProgress.isTopLevel &&
668 this._sessionPageLoad += 1;
675 "captivePortalLogin",
677 id: "captivePortalLogin",
679 _triggerHandler: null,
681 _shouldShowCaptivePortalVPNPromo() {
682 return lazy.BrowserUtils.shouldShowVPNPromo();
685 init(triggerHandler) {
686 if (!this._initialized) {
687 Services.obs.addObserver(this, "captive-portal-login-success");
688 this._initialized = true;
690 this._triggerHandler = triggerHandler;
693 observe(aSubject, aTopic, aData) {
695 case "captive-portal-login-success":
696 const browser = Services.wm.getMostRecentBrowserWindow();
697 // The check is here rather than in init because some
698 // folks leave their browsers running for a long time,
699 // eg from before leaving on a plane trip to after landing
700 // in the new destination, and the current region may have
701 // changed since init time.
702 if (browser && this._shouldShowCaptivePortalVPNPromo()) {
703 this._triggerHandler(browser.gBrowser.selectedBrowser, {
712 if (this._initialized) {
713 this._triggerHandler = null;
714 this._initialized = false;
715 Services.obs.removeObserver(this, "captive-portal-login-success");
722 "preferenceObserver",
724 id: "preferenceObserver",
726 _triggerHandler: null,
729 init(triggerHandler, prefs) {
730 if (!this._initialized) {
731 this._triggerHandler = triggerHandler;
732 this._initialized = true;
734 prefs.forEach(pref => {
735 this._observedPrefs.push(pref);
736 Services.prefs.addObserver(pref, this);
740 observe(aSubject, aTopic, aData) {
742 case "nsPref:changed":
743 const browser = Services.wm.getMostRecentBrowserWindow();
744 if (browser && this._observedPrefs.includes(aData)) {
745 this._triggerHandler(browser.gBrowser.selectedBrowser, {
757 if (this._initialized) {
758 this._observedPrefs.forEach(pref =>
759 Services.prefs.removeObserver(pref, this)
761 this._initialized = false;
762 this._triggerHandler = null;
763 this._observedPrefs = [];
773 _triggerHandler: null,
774 // Number of tabs the user closed this session
777 init(triggerHandler) {
778 this._triggerHandler = triggerHandler;
779 if (!this._initialized) {
780 lazy.EveryWindow.registerCallback(
783 win.addEventListener("TabClose", this);
786 win.removeEventListener("TabClose", this);
789 this._initialized = true;
793 if (this._initialized) {
794 if (!event.target.ownerGlobal.gBrowser) {
797 const { gBrowser } = event.target.ownerGlobal;
799 this._triggerHandler(gBrowser.selectedBrowser, {
801 context: { tabsClosedCount: this._closedTabs },
806 if (this._initialized) {
807 lazy.EveryWindow.unregisterCallback(this.id);
808 this._initialized = false;
809 this._triggerHandler = null;
810 this._closedTabs = 0;
818 id: "activityAfterIdle",
820 _triggerHandler: null,
822 // Optimization - only report idle state after one minute of idle time.
823 // This represents a minimum idleForMilliseconds of 60000.
827 _awaitingVisibilityChange: false,
828 // Fire the trigger 2 seconds after activity resumes to ensure user is
829 // actively using the browser when it fires.
831 _triggerTimeout: null,
832 // We may get an idle notification immediately after waking from sleep.
833 // The idle time in such a case will be the amount of time since the last
834 // user interaction, which was before the computer went to sleep. We want
835 // to ignore them in that case, so we ignore idle notifications that
836 // happen within 1 second of the last wake notification.
839 _listenedEvents: ["visibilitychange", "TabClose", "TabAttrModified"],
840 // When the OS goes to sleep or the process is suspended, we want to drop
841 // the idle time, since the time between sleep and wake is expected to be
842 // very long (e.g. overnight). Otherwise, this would trigger on the first
843 // activity after waking/resuming, counting sleep as idle time. This
844 // basically means each session starts with a fresh idle time.
846 "sleep_notification",
847 "suspend_process_notification",
849 "resume_process_notification",
854 return [...Services.wm.getEnumerator("navigator:browser")].some(
855 win => !win.closed && !win.document?.hidden
858 get _soundPlaying() {
859 return [...Services.wm.getEnumerator("navigator:browser")].some(win =>
860 win.gBrowser?.tabs.some(tab => !tab.closing && tab.soundPlaying)
863 init(triggerHandler) {
864 this._triggerHandler = triggerHandler;
865 // Instantiate this here instead of with a lazy service getter so we can
866 // stub it in tests (otherwise we'd have to wait up to 6 minutes for an
867 // idle notification in certain test environments).
868 if (!this._idleService) {
869 this._idleService = Cc[
870 "@mozilla.org/widget/useridleservice;1"
871 ].getService(Ci.nsIUserIdleService);
874 !this._initialized &&
875 !lazy.PrivateBrowsingUtils.permanentPrivateBrowsing
877 this._idleService.addIdleObserver(this, this._idleThreshold);
878 for (let topic of this._observedTopics) {
879 Services.obs.addObserver(this, topic);
881 lazy.EveryWindow.registerCallback(
884 for (let ev of this._listenedEvents) {
885 win.addEventListener(ev, this);
889 for (let ev of this._listenedEvents) {
890 win.removeEventListener(ev, this);
894 if (!this._soundPlaying) {
895 this._quietSince = Date.now();
897 this._initialized = true;
898 this.log("Initialized: ", {
899 idleTime: this._idleService.idleTime,
900 quietSince: this._quietSince,
904 observe(subject, topic, data) {
905 if (this._initialized) {
906 this.log("Heard observer notification: ", {
910 idleTime: this._idleService.idleTime,
911 idleSince: this._idleSince,
912 quietSince: this._quietSince,
913 lastWakeTime: this._lastWakeTime,
917 const now = Date.now();
918 // If the idle notification is within 1 second of the last wake
919 // notification, ignore it. We do this to avoid counting time the
920 // computer spent asleep as "idle time"
921 const isImmediatelyAfterWake =
922 this._lastWakeTime &&
923 now - this._lastWakeTime < this._wakeDelay;
924 if (!isImmediatelyAfterWake) {
925 this._idleSince = now - subject.idleTime;
929 // Trigger when user returns from being idle.
930 if (this._isVisible) {
932 this._idleSince = null;
933 this._lastWakeTime = null;
934 } else if (this._idleSince) {
935 // If the window is not visible, we want to wait until it is
936 // visible before triggering.
937 this._awaitingVisibilityChange = true;
940 // OS/process notifications
941 case "wake_notification":
942 case "resume_process_notification":
943 case "mac_app_activate":
944 this._lastWakeTime = Date.now();
945 // Fall through to reset idle time.
947 this._idleSince = null;
952 if (this._initialized) {
953 switch (event.type) {
954 case "visibilitychange":
955 if (this._awaitingVisibilityChange && this._isVisible) {
957 this._idleSince = null;
958 this._lastWakeTime = null;
959 this._awaitingVisibilityChange = false;
962 case "TabAttrModified":
963 // Listen for DOMAudioPlayback* events.
964 if (!event.detail?.changed?.includes("soundplaying")) {
969 this.log("Tab sound changed: ", {
971 idleTime: this._idleService.idleTime,
972 idleSince: this._idleSince,
973 quietSince: this._quietSince,
975 // Maybe update time if a tab closes with sound playing.
976 if (this._soundPlaying) {
977 this._quietSince = null;
978 } else if (!this._quietSince) {
979 this._quietSince = Date.now();
985 this.log("User is active: ", {
986 idleTime: this._idleService.idleTime,
987 idleSince: this._idleSince,
988 quietSince: this._quietSince,
989 lastWakeTime: this._lastWakeTime,
991 if (this._idleSince && this._quietSince) {
992 const win = Services.wm.getMostRecentBrowserWindow();
993 if (win && !isPrivateWindow(win) && !this._triggerTimeout) {
994 // Time since the most recent user interaction/audio playback,
995 // reported as the number of milliseconds the user has been idle.
996 const idleForMilliseconds =
997 Date.now() - Math.max(this._idleSince, this._quietSince);
998 this._triggerTimeout = lazy.setTimeout(() => {
999 this._triggerHandler(win.gBrowser.selectedBrowser, {
1001 context: { idleForMilliseconds },
1003 this._triggerTimeout = null;
1004 }, this._triggerDelay);
1009 if (this._initialized) {
1010 this._idleService.removeIdleObserver(this, this._idleThreshold);
1011 for (let topic of this._observedTopics) {
1012 Services.obs.removeObserver(this, topic);
1014 lazy.EveryWindow.unregisterCallback(this.id);
1015 lazy.clearTimeout(this._triggerTimeout);
1016 this._triggerTimeout = null;
1017 this._initialized = false;
1018 this._triggerHandler = null;
1019 this._idleSince = null;
1020 this._quietSince = null;
1021 this._lastWakeTime = null;
1022 this._awaitingVisibilityChange = false;
1023 this.log("Uninitialized");
1027 lazy.log.debug("Idle trigger :>>", ...args);
1030 QueryInterface: ChromeUtils.generateQI([
1032 "nsISupportsWeakReference",
1037 "cookieBannerDetected",
1039 id: "cookieBannerDetected",
1040 _initialized: false,
1041 _triggerHandler: null,
1043 init(triggerHandler) {
1044 this._triggerHandler = triggerHandler;
1045 if (!this._initialized) {
1046 lazy.EveryWindow.registerCallback(
1049 win.addEventListener("cookiebannerdetected", this);
1052 win.removeEventListener("cookiebannerdetected", this);
1055 this._initialized = true;
1058 handleEvent(event) {
1059 if (this._initialized) {
1060 const win = event.target || Services.wm.getMostRecentBrowserWindow();
1064 this._triggerHandler(win.gBrowser.selectedBrowser, {
1070 if (this._initialized) {
1071 lazy.EveryWindow.unregisterCallback(this.id);
1072 this._initialized = false;
1073 this._triggerHandler = null;
1079 "cookieBannerHandled",
1081 id: "cookieBannerHandled",
1082 _initialized: false,
1083 _triggerHandler: null,
1085 init(triggerHandler) {
1086 this._triggerHandler = triggerHandler;
1087 if (!this._initialized) {
1088 lazy.EveryWindow.registerCallback(
1091 win.addEventListener("cookiebannerhandled", this);
1094 win.removeEventListener("cookiebannerhandled", this);
1097 this._initialized = true;
1100 handleEvent(event) {
1101 if (this._initialized) {
1103 event.detail.windowContext.rootFrameLoader?.ownerElement;
1104 const win = browser?.ownerGlobal;
1105 // We only want to show messages in the active browser window.
1107 win === Services.wm.getMostRecentBrowserWindow() &&
1108 browser === win.gBrowser.selectedBrowser
1110 this._triggerHandler(browser, { id: this.id });
1115 if (this._initialized) {
1116 lazy.EveryWindow.unregisterCallback(this.id);
1117 this._initialized = false;
1118 this._triggerHandler = null;
1124 "pdfJsFeatureCalloutCheck",
1126 id: "pdfJsFeatureCalloutCheck",
1127 _initialized: false,
1128 _triggerHandler: null,
1129 _callouts: new WeakMap(),
1131 init(triggerHandler) {
1132 if (!this._initialized) {
1133 this.onLocationChange = this.onLocationChange.bind(this);
1134 this.onStateChange = this.onLocationChange;
1135 lazy.EveryWindow.registerCallback(
1138 this.onBrowserWindow(win);
1139 win.addEventListener("TabSelect", this);
1140 win.addEventListener("TabClose", this);
1141 win.addEventListener("SSTabRestored", this);
1142 win.gBrowser.addTabsProgressListener(this);
1145 win.removeEventListener("TabSelect", this);
1146 win.removeEventListener("TabClose", this);
1147 win.removeEventListener("SSTabRestored", this);
1148 win.gBrowser.removeTabsProgressListener(this);
1151 this._initialized = true;
1153 this._triggerHandler = triggerHandler;
1157 if (this._initialized) {
1158 lazy.EveryWindow.unregisterCallback(this.id);
1159 this._initialized = false;
1160 this._triggerHandler = null;
1161 for (let key of ChromeUtils.nondeterministicGetWeakMapKeys(
1164 const item = this._callouts.get(key);
1166 item.callout.endTour(true);
1168 this._callouts.delete(key);
1174 async showFeatureCalloutTour(win, browser, panelId, context) {
1175 const result = await this._triggerHandler(browser, {
1176 id: "pdfJsFeatureCalloutCheck",
1179 if (result.message.trigger) {
1180 const callout = lazy.FeatureCalloutBroker.showCustomFeatureCallout(
1186 result.message.content?.tour_pref_name ??
1187 "browser.pdfjs.feature-tour",
1188 defaultValue: result.message.content?.tour_pref_default_value,
1191 theme: { preset: "pdfjs", simulateContent: true },
1193 this._callouts.delete(win);
1199 callout.panelId = panelId;
1200 this._callouts.set(win, callout);
1205 onLocationChange(browser) {
1206 const tabbrowser = browser.getTabBrowser();
1207 if (browser !== tabbrowser.selectedBrowser) {
1210 const win = tabbrowser.ownerGlobal;
1211 const tab = tabbrowser.selectedTab;
1212 const existingCallout = this._callouts.get(win);
1214 browser.contentPrincipal.originNoSuffix === "resource://pdf.js";
1217 (existingCallout.panelId !== tab.linkedPanel || !isPDFJS)
1219 existingCallout.callout.endTour(true);
1220 existingCallout.cleanup();
1222 if (!this._callouts.has(win) && isPDFJS) {
1223 this.showFeatureCalloutTour(win, browser, tab.linkedPanel, {
1229 handleEvent(event) {
1230 const tab = event.target;
1231 const win = tab.ownerGlobal;
1232 const { gBrowser } = win;
1236 switch (event.type) {
1237 case "SSTabRestored":
1238 if (tab !== gBrowser.selectedTab) {
1243 const browser = gBrowser.getBrowserForTab(tab);
1244 const existingCallout = this._callouts.get(win);
1246 browser.contentPrincipal.originNoSuffix === "resource://pdf.js";
1249 (existingCallout.panelId !== tab.linkedPanel || !isPDFJS)
1251 existingCallout.callout.endTour(true);
1252 existingCallout.cleanup();
1254 if (!this._callouts.has(win) && isPDFJS) {
1255 this.showFeatureCalloutTour(win, browser, tab.linkedPanel, {
1262 const existingCallout = this._callouts.get(win);
1265 existingCallout.panelId === tab.linkedPanel
1267 existingCallout.callout.endTour(true);
1268 existingCallout.cleanup();
1275 onBrowserWindow(win) {
1276 this.onLocationChange(win.gBrowser.selectedBrowser);
1281 "newtabFeatureCalloutCheck",
1283 id: "newtabFeatureCalloutCheck",
1284 _initialized: false,
1285 _triggerHandler: null,
1286 _callouts: new WeakMap(),
1288 init(triggerHandler) {
1289 if (!this._initialized) {
1290 this.onLocationChange = this.onLocationChange.bind(this);
1291 this.onStateChange = this.onLocationChange;
1292 lazy.EveryWindow.registerCallback(
1295 this.onBrowserWindow(win);
1296 win.addEventListener("TabSelect", this);
1297 win.addEventListener("TabClose", this);
1298 win.addEventListener("SSTabRestored", this);
1299 win.gBrowser.addTabsProgressListener(this);
1302 win.removeEventListener("TabSelect", this);
1303 win.removeEventListener("TabClose", this);
1304 win.removeEventListener("SSTabRestored", this);
1305 win.gBrowser.removeTabsProgressListener(this);
1308 this._initialized = true;
1310 this._triggerHandler = triggerHandler;
1314 if (this._initialized) {
1315 lazy.EveryWindow.unregisterCallback(this.id);
1316 this._initialized = false;
1317 this._triggerHandler = null;
1318 for (let key of ChromeUtils.nondeterministicGetWeakMapKeys(
1321 const item = this._callouts.get(key);
1323 item.callout.endTour(true);
1325 this._callouts.delete(key);
1331 async showFeatureCalloutTour(win, browser, panelId, context) {
1332 const result = await this._triggerHandler(browser, {
1333 id: "newtabFeatureCalloutCheck",
1336 if (result.message.trigger) {
1337 const callout = lazy.FeatureCalloutBroker.showCustomFeatureCallout(
1343 result.message.content?.tour_pref_name ??
1344 "browser.newtab.feature-tour",
1345 defaultValue: result.message.content?.tour_pref_default_value,
1348 theme: { preset: "newtab", simulateContent: true },
1350 this._callouts.delete(win);
1356 callout.panelId = panelId;
1357 this._callouts.set(win, callout);
1362 onLocationChange(browser) {
1363 const tabbrowser = browser.getTabBrowser();
1364 if (browser !== tabbrowser.selectedBrowser) {
1367 const win = tabbrowser.ownerGlobal;
1368 const tab = tabbrowser.selectedTab;
1369 const existingCallout = this._callouts.get(win);
1370 const isNewtabOrHome =
1371 browser.currentURI.spec.startsWith("about:home") ||
1372 browser.currentURI.spec.startsWith("about:newtab");
1375 (existingCallout.panelId !== tab.linkedPanel || !isNewtabOrHome)
1377 existingCallout.callout.endTour(true);
1378 existingCallout.cleanup();
1380 if (!this._callouts.has(win) && isNewtabOrHome && tab.linkedPanel) {
1381 this.showFeatureCalloutTour(win, browser, tab.linkedPanel, {
1387 handleEvent(event) {
1388 const tab = event.target;
1389 const win = tab.ownerGlobal;
1390 const { gBrowser } = win;
1394 switch (event.type) {
1395 case "SSTabRestored":
1396 if (tab !== gBrowser.selectedTab) {
1401 const browser = gBrowser.getBrowserForTab(tab);
1402 const existingCallout = this._callouts.get(win);
1403 const isNewtabOrHome =
1404 browser.currentURI.spec.startsWith("about:home") ||
1405 browser.currentURI.spec.startsWith("about:newtab");
1408 (existingCallout.panelId !== tab.linkedPanel || !isNewtabOrHome)
1410 existingCallout.callout.endTour(true);
1411 existingCallout.cleanup();
1413 if (!this._callouts.has(win) && isNewtabOrHome) {
1414 this.showFeatureCalloutTour(win, browser, tab.linkedPanel, {
1421 const existingCallout = this._callouts.get(win);
1424 existingCallout.panelId === tab.linkedPanel
1426 existingCallout.callout.endTour(true);
1427 existingCallout.cleanup();
1434 onBrowserWindow(win) {
1435 this.onLocationChange(win.gBrowser.selectedBrowser);