AboutNewTab.sys.mjs (8934B)
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/. */ 4 5 import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 const lazy = {}; 9 10 ChromeUtils.defineESModuleGetters(lazy, { 11 AboutNewTabResourceMapping: 12 "resource:///modules/AboutNewTabResourceMapping.sys.mjs", 13 ActivityStream: "resource://newtab/lib/ActivityStream.sys.mjs", 14 ObjectUtils: "resource://gre/modules/ObjectUtils.sys.mjs", 15 TelemetryReportingPolicy: 16 "resource://gre/modules/TelemetryReportingPolicy.sys.mjs", 17 }); 18 19 const ABOUT_URL = "about:newtab"; 20 const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug"; 21 // AboutHomeStartupCache needs us in "quit-application", so stay alive longer. 22 // TODO: We could better have a shared async shutdown blocker? 23 const TOPIC_APP_QUIT = "profile-before-change"; 24 25 export const AboutNewTab = { 26 QueryInterface: ChromeUtils.generateQI([ 27 "nsIObserver", 28 "nsISupportsWeakReference", 29 ]), 30 31 // AboutNewTab 32 initialized: false, 33 34 willNotifyUser: false, 35 36 _activityStreamEnabled: false, 37 activityStream: null, 38 activityStreamDebug: false, 39 40 _cachedTopSites: null, 41 42 _newTabURL: ABOUT_URL, 43 _newTabURLOverridden: false, 44 45 /** 46 * init - Initializes an instance of Activity Stream if one doesn't exist already. 47 */ 48 init() { 49 if (this.initialized) { 50 return; 51 } 52 53 Services.obs.addObserver(this, TOPIC_APP_QUIT); 54 if (!AppConstants.RELEASE_OR_BETA) { 55 XPCOMUtils.defineLazyPreferenceGetter( 56 this, 57 "activityStreamDebug", 58 PREF_ACTIVITY_STREAM_DEBUG, 59 false, 60 () => { 61 this.notifyChange(); 62 } 63 ); 64 } 65 66 XPCOMUtils.defineLazyPreferenceGetter( 67 this, 68 "privilegedAboutProcessEnabled", 69 "browser.tabs.remote.separatePrivilegedContentProcess", 70 false, 71 () => { 72 this.notifyChange(); 73 } 74 ); 75 76 // Make sure to register newtab resource mapping as early as possible 77 // on startup. 78 if (AppConstants.BROWSER_NEWTAB_AS_ADDON) { 79 lazy.AboutNewTabResourceMapping.init(); 80 } 81 82 // More initialization happens here 83 this.toggleActivityStream(true); 84 this.initialized = true; 85 86 Services.obs.addObserver( 87 this, 88 lazy.TelemetryReportingPolicy.TELEMETRY_TOU_ACCEPTED_OR_INELIGIBLE 89 ); 90 }, 91 92 /** 93 * React to changes to the activity stream being enabled or not. 94 * 95 * This will only act if there is a change of state and if not overridden. 96 * 97 * @returns {boolean} Returns if there has been a state change 98 * 99 * @param {boolean} stateEnabled activity stream enabled state to set to 100 * @param {boolean} forceState force state change 101 */ 102 toggleActivityStream(stateEnabled, forceState = false) { 103 if ( 104 !forceState && 105 (this._newTabURLOverridden || 106 stateEnabled === this._activityStreamEnabled) 107 ) { 108 // exit there is no change of state 109 return false; 110 } 111 if (stateEnabled) { 112 this._activityStreamEnabled = true; 113 } else { 114 this._activityStreamEnabled = false; 115 } 116 117 this._newTabURL = ABOUT_URL; 118 return true; 119 }, 120 121 get newTabURL() { 122 return this._newTabURL; 123 }, 124 125 set newTabURL(aNewTabURL) { 126 let newTabURL = aNewTabURL.trim(); 127 if (newTabURL === ABOUT_URL) { 128 // avoid infinite redirects in case one sets the URL to about:newtab 129 this.resetNewTabURL(); 130 return; 131 } else if (newTabURL === "") { 132 newTabURL = "about:blank"; 133 } 134 135 this.toggleActivityStream(false); 136 this._newTabURL = newTabURL; 137 this._newTabURLOverridden = true; 138 this.notifyChange(); 139 }, 140 141 get newTabURLOverridden() { 142 return this._newTabURLOverridden; 143 }, 144 145 get activityStreamEnabled() { 146 return this._activityStreamEnabled; 147 }, 148 149 resetNewTabURL() { 150 this._newTabURLOverridden = false; 151 this._newTabURL = ABOUT_URL; 152 this.toggleActivityStream(true, true); 153 this.notifyChange(); 154 }, 155 156 notifyChange() { 157 Services.obs.notifyObservers(null, "newtab-url-changed", this._newTabURL); 158 }, 159 160 /** 161 * onBrowserReady - Continues the initialization of Activity Stream after browser is ready. 162 */ 163 async onBrowserReady() { 164 if (AppConstants.BASE_BROWSER_VERSION) { 165 // Do not initialise ActivityStream, which we do not want and is not 166 // available. tor-browser#43886. 167 return; 168 } 169 if (this.activityStream && this.activityStream.initialized) { 170 return; 171 } 172 173 if (AppConstants.BROWSER_NEWTAB_AS_ADDON) { 174 // Wait until the built-in addon has reported that it has finished 175 // initializing. 176 let redirector = Cc[ 177 "@mozilla.org/network/protocol/about;1?what=newtab" 178 ].getService(Ci.nsIAboutModule).wrappedJSObject; 179 180 await redirector.promiseBuiltInAddonInitialized; 181 lazy.AboutNewTabResourceMapping.scheduleUpdateTrainhopAddonState(); 182 } else { 183 // We may have had the built-in addon installed in the past. Since the 184 // flag is false, let's go ahead and remove it. We don't need to await on 185 // this since the extension should be inert if the build flag is false. 186 lazy.AboutNewTabResourceMapping.uninstallAddon(); 187 } 188 189 try { 190 this.activityStream = new lazy.ActivityStream(); 191 Glean.newtab.activityStreamCtorSuccess.set(true); 192 } catch (error) { 193 // Send Activity Stream loading failure telemetry 194 // This probe will help to monitor if ActivityStream failure has crossed 195 // a threshold and send alert. See Bug 1965278 196 Glean.newtab.activityStreamCtorSuccess.set(false); 197 console.error(error); 198 throw error; 199 } 200 201 try { 202 this.activityStream.init(); 203 this._subscribeToActivityStream(); 204 } catch (e) { 205 console.error(e); 206 } 207 }, 208 209 _subscribeToActivityStream() { 210 let unsubscribe = this.activityStream.store.subscribe(() => { 211 // If the top sites changed, broadcast "newtab-top-sites-changed". We 212 // ignore changes to the `screenshot` property in each site because 213 // screenshots are generated at times that are hard to predict and it ends 214 // up interfering with tests that rely on "newtab-top-sites-changed". 215 // Observers likely don't care about screenshots anyway. 216 let topSites = this.activityStream.store 217 .getState() 218 .TopSites.rows.map(site => { 219 site = { ...site }; 220 delete site.screenshot; 221 return site; 222 }); 223 if (!lazy.ObjectUtils.deepEqual(topSites, this._cachedTopSites)) { 224 this._cachedTopSites = topSites; 225 Services.obs.notifyObservers(null, "newtab-top-sites-changed"); 226 } 227 }); 228 this._unsubscribeFromActivityStream = () => { 229 try { 230 unsubscribe(); 231 } catch (e) { 232 console.error(e); 233 } 234 }; 235 }, 236 237 /** 238 * uninit - Uninitializes Activity Stream if it exists. 239 */ 240 uninit() { 241 if (this.activityStream) { 242 this._unsubscribeFromActivityStream?.(); 243 this.activityStream.uninit(); 244 this.activityStream = null; 245 } 246 try { 247 Services.obs.removeObserver(this, TOPIC_APP_QUIT); 248 Services.obs.removeObserver( 249 this, 250 lazy.TelemetryReportingPolicy.TELEMETRY_TOU_ACCEPTED_OR_INELIGIBLE 251 ); 252 } catch (e) { 253 // If init failed before registering these observers, removeObserver may throw. 254 // Safe to ignore during shutdown. 255 } 256 257 this.initialized = false; 258 }, 259 260 getTopSites() { 261 return this.activityStream 262 ? this.activityStream.store.getState().TopSites.rows 263 : []; 264 }, 265 266 _alreadyRecordedTopsitesPainted: false, 267 _nonDefaultStartup: false, 268 269 noteNonDefaultStartup() { 270 this._nonDefaultStartup = true; 271 }, 272 273 maybeRecordTopsitesPainted(timestamp) { 274 if (this._alreadyRecordedTopsitesPainted || this._nonDefaultStartup) { 275 return; 276 } 277 278 let startupInfo = Services.startup.getStartupInfo(); 279 let processStartTs = startupInfo.process.getTime(); 280 let delta = Math.round(timestamp - processStartTs); 281 Glean.timestamps.aboutHomeTopsitesFirstPaint.set(delta); 282 ChromeUtils.addProfilerMarker("aboutHomeTopsitesFirstPaint"); 283 this._alreadyRecordedTopsitesPainted = true; 284 }, 285 286 // nsIObserver implementation 287 288 observe(subject, topic) { 289 switch (topic) { 290 case TOPIC_APP_QUIT: { 291 this.uninit(); 292 break; 293 } 294 case lazy.TelemetryReportingPolicy.TELEMETRY_TOU_ACCEPTED_OR_INELIGIBLE: { 295 Services.obs.removeObserver( 296 this, 297 lazy.TelemetryReportingPolicy.TELEMETRY_TOU_ACCEPTED_OR_INELIGIBLE 298 ); 299 300 // Avoid running synchronously during this event that's used for timing 301 Services.tm.dispatchToMainThread(() => this.onBrowserReady()); 302 break; 303 } 304 } 305 }, 306 };