Merge autoland to mozilla-central. a=merge
[gecko.git] / browser / modules / AboutNewTab.sys.mjs
blob979c3adf1252124854ef50adf4713a6e253bfe4f
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/. */
5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
8 const lazy = {};
10 ChromeUtils.defineESModuleGetters(lazy, {
11   ActivityStream: "resource://activity-stream/lib/ActivityStream.sys.mjs",
12   ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs",
13 });
15 const ABOUT_URL = "about:newtab";
16 const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
17 const TOPIC_APP_QUIT = "quit-application-granted";
18 const BROWSER_READY_NOTIFICATION = "sessionstore-windows-restored";
20 export const AboutNewTab = {
21   QueryInterface: ChromeUtils.generateQI([
22     "nsIObserver",
23     "nsISupportsWeakReference",
24   ]),
26   // AboutNewTab
27   initialized: false,
29   willNotifyUser: false,
31   _activityStreamEnabled: false,
32   activityStream: null,
33   activityStreamDebug: false,
35   _cachedTopSites: null,
37   _newTabURL: ABOUT_URL,
38   _newTabURLOverridden: false,
40   /**
41    * init - Initializes an instance of Activity Stream if one doesn't exist already.
42    */
43   init() {
44     Services.obs.addObserver(this, TOPIC_APP_QUIT);
45     if (!AppConstants.RELEASE_OR_BETA) {
46       XPCOMUtils.defineLazyPreferenceGetter(
47         this,
48         "activityStreamDebug",
49         PREF_ACTIVITY_STREAM_DEBUG,
50         false,
51         () => {
52           this.notifyChange();
53         }
54       );
55     }
57     XPCOMUtils.defineLazyPreferenceGetter(
58       this,
59       "privilegedAboutProcessEnabled",
60       "browser.tabs.remote.separatePrivilegedContentProcess",
61       false,
62       () => {
63         this.notifyChange();
64       }
65     );
67     // More initialization happens here
68     this.toggleActivityStream(true);
69     this.initialized = true;
71     Services.obs.addObserver(this, BROWSER_READY_NOTIFICATION);
72   },
74   /**
75    * React to changes to the activity stream being enabled or not.
76    *
77    * This will only act if there is a change of state and if not overridden.
78    *
79    * @returns {Boolean} Returns if there has been a state change
80    *
81    * @param {Boolean}   stateEnabled    activity stream enabled state to set to
82    * @param {Boolean}   forceState      force state change
83    */
84   toggleActivityStream(stateEnabled, forceState = false) {
85     if (
86       !forceState &&
87       (this._newTabURLOverridden ||
88         stateEnabled === this._activityStreamEnabled)
89     ) {
90       // exit there is no change of state
91       return false;
92     }
93     if (stateEnabled) {
94       this._activityStreamEnabled = true;
95     } else {
96       this._activityStreamEnabled = false;
97     }
99     this._newTabURL = ABOUT_URL;
100     return true;
101   },
103   get newTabURL() {
104     return this._newTabURL;
105   },
107   set newTabURL(aNewTabURL) {
108     let newTabURL = aNewTabURL.trim();
109     if (newTabURL === ABOUT_URL) {
110       // avoid infinite redirects in case one sets the URL to about:newtab
111       this.resetNewTabURL();
112       return;
113     } else if (newTabURL === "") {
114       newTabURL = "about:blank";
115     }
117     this.toggleActivityStream(false);
118     this._newTabURL = newTabURL;
119     this._newTabURLOverridden = true;
120     this.notifyChange();
121   },
123   get newTabURLOverridden() {
124     return this._newTabURLOverridden;
125   },
127   get activityStreamEnabled() {
128     return this._activityStreamEnabled;
129   },
131   resetNewTabURL() {
132     this._newTabURLOverridden = false;
133     this._newTabURL = ABOUT_URL;
134     this.toggleActivityStream(true, true);
135     this.notifyChange();
136   },
138   notifyChange() {
139     Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL);
140   },
142   /**
143    * onBrowserReady - Continues the initialization of Activity Stream after browser is ready.
144    */
145   onBrowserReady() {
146     if (this.activityStream && this.activityStream.initialized) {
147       return;
148     }
150     this.activityStream = new lazy.ActivityStream();
151     try {
152       this.activityStream.init();
153       this._subscribeToActivityStream();
154     } catch (e) {
155       console.error(e);
156     }
157   },
159   _subscribeToActivityStream() {
160     let unsubscribe = this.activityStream.store.subscribe(() => {
161       // If the top sites changed, broadcast "newtab-top-sites-changed". We
162       // ignore changes to the `screenshot` property in each site because
163       // screenshots are generated at times that are hard to predict and it ends
164       // up interfering with tests that rely on "newtab-top-sites-changed".
165       // Observers likely don't care about screenshots anyway.
166       let topSites = this.activityStream.store
167         .getState()
168         .TopSites.rows.map(site => {
169           site = { ...site };
170           delete site.screenshot;
171           return site;
172         });
173       if (!lazy.ObjectUtils.deepEqual(topSites, this._cachedTopSites)) {
174         this._cachedTopSites = topSites;
175         Services.obs.notifyObservers(null, "newtab-top-sites-changed");
176       }
177     });
178     this._unsubscribeFromActivityStream = () => {
179       try {
180         unsubscribe();
181       } catch (e) {
182         console.error(e);
183       }
184     };
185   },
187   /**
188    * uninit - Uninitializes Activity Stream if it exists.
189    */
190   uninit() {
191     if (this.activityStream) {
192       this._unsubscribeFromActivityStream?.();
193       this.activityStream.uninit();
194       this.activityStream = null;
195     }
197     this.initialized = false;
198   },
200   getTopSites() {
201     return this.activityStream
202       ? this.activityStream.store.getState().TopSites.rows
203       : [];
204   },
206   _alreadyRecordedTopsitesPainted: false,
207   _nonDefaultStartup: false,
209   noteNonDefaultStartup() {
210     this._nonDefaultStartup = true;
211   },
213   maybeRecordTopsitesPainted(timestamp) {
214     if (this._alreadyRecordedTopsitesPainted || this._nonDefaultStartup) {
215       return;
216     }
218     const SCALAR_KEY = "timestamps.about_home_topsites_first_paint";
220     let startupInfo = Services.startup.getStartupInfo();
221     let processStartTs = startupInfo.process.getTime();
222     let delta = Math.round(timestamp - processStartTs);
223     Services.telemetry.scalarSet(SCALAR_KEY, delta);
224     ChromeUtils.addProfilerMarker("aboutHomeTopsitesFirstPaint");
225     this._alreadyRecordedTopsitesPainted = true;
226   },
228   // nsIObserver implementation
230   observe(subject, topic) {
231     switch (topic) {
232       case TOPIC_APP_QUIT: {
233         // We defer to this to the next tick of the event loop since the
234         // AboutHomeStartupCache might want to read from the ActivityStream
235         // store during TOPIC_APP_QUIT.
236         Services.tm.dispatchToMainThread(() => this.uninit());
237         break;
238       }
239       case BROWSER_READY_NOTIFICATION: {
240         Services.obs.removeObserver(this, BROWSER_READY_NOTIFICATION);
241         // Avoid running synchronously during this event that's used for timing
242         Services.tm.dispatchToMainThread(() => this.onBrowserReady());
243         break;
244       }
245     }
246   },