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"use strict";
6
7var EXPORTED_SYMBOLS = ["TelemetryEnvironment", "Policy"];
8
9const myScope = this;
10
11const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm");
12const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
13const { TelemetryUtils } = ChromeUtils.import(
14  "resource://gre/modules/TelemetryUtils.jsm"
15);
16const { ObjectUtils } = ChromeUtils.import(
17  "resource://gre/modules/ObjectUtils.jsm"
18);
19const { AppConstants } = ChromeUtils.import(
20  "resource://gre/modules/AppConstants.jsm"
21);
22
23const Utils = TelemetryUtils;
24
25const { AddonManager, AddonManagerPrivate } = ChromeUtils.import(
26  "resource://gre/modules/AddonManager.jsm"
27);
28
29ChromeUtils.defineModuleGetter(
30  this,
31  "AttributionCode",
32  "resource:///modules/AttributionCode.jsm"
33);
34ChromeUtils.defineModuleGetter(
35  this,
36  "ProfileAge",
37  "resource://gre/modules/ProfileAge.jsm"
38);
39ChromeUtils.defineModuleGetter(
40  this,
41  "WindowsRegistry",
42  "resource://gre/modules/WindowsRegistry.jsm"
43);
44ChromeUtils.defineModuleGetter(
45  this,
46  "UpdateUtils",
47  "resource://gre/modules/UpdateUtils.jsm"
48);
49ChromeUtils.defineModuleGetter(
50  this,
51  "fxAccounts",
52  "resource://gre/modules/FxAccounts.jsm"
53);
54ChromeUtils.defineModuleGetter(
55  this,
56  "WindowsVersionInfo",
57  "resource://gre/modules/components-utils/WindowsVersionInfo.jsm"
58);
59
60// The maximum length of a string (e.g. description) in the addons section.
61const MAX_ADDON_STRING_LENGTH = 100;
62// The maximum length of a string value in the settings.attribution object.
63const MAX_ATTRIBUTION_STRING_LENGTH = 100;
64// The maximum lengths for the experiment id and branch in the experiments section.
65const MAX_EXPERIMENT_ID_LENGTH = 100;
66const MAX_EXPERIMENT_BRANCH_LENGTH = 100;
67const MAX_EXPERIMENT_TYPE_LENGTH = 20;
68const MAX_EXPERIMENT_ENROLLMENT_ID_LENGTH = 40;
69
70/**
71 * This is a policy object used to override behavior for testing.
72 */
73// eslint-disable-next-line no-unused-vars
74var Policy = {
75  now: () => new Date(),
76  _intlLoaded: false,
77  _browserDelayedStartup() {
78    if (Policy._intlLoaded) {
79      return Promise.resolve();
80    }
81    return new Promise(resolve => {
82      let startupTopic = "browser-delayed-startup-finished";
83      Services.obs.addObserver(function observer(subject, topic) {
84        if (topic == startupTopic) {
85          Services.obs.removeObserver(observer, startupTopic);
86          resolve();
87        }
88      }, startupTopic);
89    });
90  },
91};
92
93// This is used to buffer calls to setExperimentActive and friends, so that we
94// don't prematurely initialize our environment if it is called early during
95// startup.
96var gActiveExperimentStartupBuffer = new Map();
97
98var gGlobalEnvironment;
99function getGlobal() {
100  if (!gGlobalEnvironment) {
101    gGlobalEnvironment = new EnvironmentCache();
102  }
103  return gGlobalEnvironment;
104}
105
106var TelemetryEnvironment = {
107  get currentEnvironment() {
108    return getGlobal().currentEnvironment;
109  },
110
111  onInitialized() {
112    return getGlobal().onInitialized();
113  },
114
115  delayedInit() {
116    return getGlobal().delayedInit();
117  },
118
119  registerChangeListener(name, listener) {
120    return getGlobal().registerChangeListener(name, listener);
121  },
122
123  unregisterChangeListener(name) {
124    return getGlobal().unregisterChangeListener(name);
125  },
126
127  /**
128   * Add an experiment annotation to the environment.
129   * If an annotation with the same id already exists, it will be overwritten.
130   * This triggers a new subsession, subject to throttling.
131   *
132   * @param {String} id The id of the active experiment.
133   * @param {String} branch The experiment branch.
134   * @param {Object} [options] Optional object with options.
135   * @param {String} [options.type=false] The specific experiment type.
136   * @param {String} [options.enrollmentId=undefined] The id of the enrollment.
137   */
138  setExperimentActive(id, branch, options = {}) {
139    if (gGlobalEnvironment) {
140      gGlobalEnvironment.setExperimentActive(id, branch, options);
141    } else {
142      gActiveExperimentStartupBuffer.set(id, { branch, options });
143    }
144  },
145
146  /**
147   * Remove an experiment annotation from the environment.
148   * If the annotation exists, a new subsession will triggered.
149   *
150   * @param {String} id The id of the active experiment.
151   */
152  setExperimentInactive(id) {
153    if (gGlobalEnvironment) {
154      gGlobalEnvironment.setExperimentInactive(id);
155    } else {
156      gActiveExperimentStartupBuffer.delete(id);
157    }
158  },
159
160  /**
161   * Returns an object containing the data for the active experiments.
162   *
163   * The returned object is of the format:
164   *
165   * {
166   *   "<experiment id>": { branch: "<branch>" },
167   *   // …
168   * }
169   */
170  getActiveExperiments() {
171    if (gGlobalEnvironment) {
172      return gGlobalEnvironment.getActiveExperiments();
173    }
174
175    const result = {};
176    for (const [id, { branch }] of gActiveExperimentStartupBuffer.entries()) {
177      result[id] = branch;
178    }
179    return result;
180  },
181
182  shutdown() {
183    return getGlobal().shutdown();
184  },
185
186  // Policy to use when saving preferences. Exported for using them in tests.
187  // Reports "<user-set>" if there is a value set on the user branch
188  RECORD_PREF_STATE: 1,
189
190  // Reports the value set on the user branch, if one is set
191  RECORD_PREF_VALUE: 2,
192
193  // Reports the active value (set on either the user or default branch)
194  // for this pref, if one is set
195  RECORD_DEFAULTPREF_VALUE: 3,
196
197  // Reports "<set>" if a value for this pref is defined on either the user
198  // or default branch
199  RECORD_DEFAULTPREF_STATE: 4,
200
201  // Testing method
202  async testWatchPreferences(prefMap) {
203    return getGlobal()._watchPreferences(prefMap);
204  },
205
206  /**
207   * Intended for use in tests only.
208   *
209   * In multiple tests we need a way to shut and re-start telemetry together
210   * with TelemetryEnvironment. This is problematic due to the fact that
211   * TelemetryEnvironment is a singleton. We, therefore, need this helper
212   * method to be able to re-set TelemetryEnvironment.
213   */
214  testReset() {
215    return getGlobal().reset();
216  },
217
218  /**
219   * Intended for use in tests only.
220   */
221  testCleanRestart() {
222    getGlobal().shutdown();
223    gGlobalEnvironment = null;
224    gActiveExperimentStartupBuffer = new Map();
225    return getGlobal();
226  },
227};
228
229const RECORD_PREF_STATE = TelemetryEnvironment.RECORD_PREF_STATE;
230const RECORD_PREF_VALUE = TelemetryEnvironment.RECORD_PREF_VALUE;
231const RECORD_DEFAULTPREF_VALUE = TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE;
232const RECORD_DEFAULTPREF_STATE = TelemetryEnvironment.RECORD_DEFAULTPREF_STATE;
233const DEFAULT_ENVIRONMENT_PREFS = new Map([
234  ["app.feedback.baseURL", { what: RECORD_PREF_VALUE }],
235  ["app.support.baseURL", { what: RECORD_PREF_VALUE }],
236  ["accessibility.browsewithcaret", { what: RECORD_PREF_VALUE }],
237  ["accessibility.force_disabled", { what: RECORD_PREF_VALUE }],
238  ["app.normandy.test-prefs.bool", { what: RECORD_PREF_VALUE }],
239  ["app.normandy.test-prefs.integer", { what: RECORD_PREF_VALUE }],
240  ["app.normandy.test-prefs.string", { what: RECORD_PREF_VALUE }],
241  ["app.shield.optoutstudies.enabled", { what: RECORD_PREF_VALUE }],
242  ["app.update.interval", { what: RECORD_PREF_VALUE }],
243  ["app.update.service.enabled", { what: RECORD_PREF_VALUE }],
244  ["app.update.silent", { what: RECORD_PREF_VALUE }],
245  ["browser.cache.disk.enable", { what: RECORD_PREF_VALUE }],
246  ["browser.cache.disk.capacity", { what: RECORD_PREF_VALUE }],
247  ["browser.cache.memory.enable", { what: RECORD_PREF_VALUE }],
248  ["browser.cache.offline.enable", { what: RECORD_PREF_VALUE }],
249  ["browser.formfill.enable", { what: RECORD_PREF_VALUE }],
250  ["browser.newtabpage.enabled", { what: RECORD_PREF_VALUE }],
251  ["browser.shell.checkDefaultBrowser", { what: RECORD_PREF_VALUE }],
252  ["browser.search.ignoredJAREngines", { what: RECORD_DEFAULTPREF_VALUE }],
253  ["browser.search.region", { what: RECORD_PREF_VALUE }],
254  ["browser.search.suggest.enabled", { what: RECORD_PREF_VALUE }],
255  ["browser.search.widget.inNavBar", { what: RECORD_DEFAULTPREF_VALUE }],
256  ["browser.startup.homepage", { what: RECORD_PREF_STATE }],
257  ["browser.startup.page", { what: RECORD_PREF_VALUE }],
258  ["browser.touchmode.auto", { what: RECORD_PREF_VALUE }],
259  ["browser.uidensity", { what: RECORD_PREF_VALUE }],
260  ["browser.urlbar.showSearchSuggestionsFirst", { what: RECORD_PREF_VALUE }],
261  ["browser.urlbar.suggest.searches", { what: RECORD_PREF_VALUE }],
262  ["devtools.chrome.enabled", { what: RECORD_PREF_VALUE }],
263  ["devtools.debugger.enabled", { what: RECORD_PREF_VALUE }],
264  ["devtools.debugger.remote-enabled", { what: RECORD_PREF_VALUE }],
265  ["dom.ipc.plugins.enabled", { what: RECORD_PREF_VALUE }],
266  ["dom.ipc.plugins.sandbox-level.flash", { what: RECORD_PREF_VALUE }],
267  ["dom.ipc.processCount", { what: RECORD_PREF_VALUE }],
268  ["dom.max_script_run_time", { what: RECORD_PREF_VALUE }],
269  ["extensions.autoDisableScopes", { what: RECORD_PREF_VALUE }],
270  ["extensions.enabledScopes", { what: RECORD_PREF_VALUE }],
271  ["extensions.blocklist.enabled", { what: RECORD_PREF_VALUE }],
272  ["extensions.formautofill.addresses.enabled", { what: RECORD_PREF_VALUE }],
273  [
274    "extensions.formautofill.addresses.capture.enabled",
275    { what: RECORD_PREF_VALUE },
276  ],
277  ["extensions.formautofill.creditCards.enabled", { what: RECORD_PREF_VALUE }],
278  [
279    "extensions.formautofill.creditCards.available",
280    { what: RECORD_PREF_VALUE },
281  ],
282  ["extensions.formautofill.creditCards.used", { what: RECORD_PREF_VALUE }],
283  ["extensions.strictCompatibility", { what: RECORD_PREF_VALUE }],
284  ["extensions.update.enabled", { what: RECORD_PREF_VALUE }],
285  ["extensions.update.url", { what: RECORD_PREF_VALUE }],
286  ["extensions.update.background.url", { what: RECORD_PREF_VALUE }],
287  ["extensions.screenshots.disabled", { what: RECORD_PREF_VALUE }],
288  ["general.config.filename", { what: RECORD_DEFAULTPREF_STATE }],
289  ["general.smoothScroll", { what: RECORD_PREF_VALUE }],
290  ["gfx.direct2d.disabled", { what: RECORD_PREF_VALUE }],
291  ["gfx.direct2d.force-enabled", { what: RECORD_PREF_VALUE }],
292  ["gfx.webrender.all", { what: RECORD_PREF_VALUE }],
293  ["gfx.webrender.all.qualified", { what: RECORD_PREF_VALUE }],
294  ["gfx.webrender.force-disabled", { what: RECORD_PREF_VALUE }],
295  ["layers.acceleration.disabled", { what: RECORD_PREF_VALUE }],
296  ["layers.acceleration.force-enabled", { what: RECORD_PREF_VALUE }],
297  ["layers.async-pan-zoom.enabled", { what: RECORD_PREF_VALUE }],
298  ["layers.async-video-oop.enabled", { what: RECORD_PREF_VALUE }],
299  ["layers.async-video.enabled", { what: RECORD_PREF_VALUE }],
300  ["layers.componentalpha.enabled", { what: RECORD_PREF_VALUE }],
301  ["layers.d3d11.disable-warp", { what: RECORD_PREF_VALUE }],
302  ["layers.d3d11.force-warp", { what: RECORD_PREF_VALUE }],
303  [
304    "layers.offmainthreadcomposition.force-disabled",
305    { what: RECORD_PREF_VALUE },
306  ],
307  ["layers.prefer-d3d9", { what: RECORD_PREF_VALUE }],
308  ["layers.prefer-opengl", { what: RECORD_PREF_VALUE }],
309  ["layout.css.devPixelsPerPx", { what: RECORD_PREF_VALUE }],
310  ["network.http.windows-sso.enabled", { what: RECORD_PREF_VALUE }],
311  ["network.proxy.autoconfig_url", { what: RECORD_PREF_STATE }],
312  ["network.proxy.http", { what: RECORD_PREF_STATE }],
313  ["network.proxy.ssl", { what: RECORD_PREF_STATE }],
314  ["network.trr.mode", { what: RECORD_PREF_VALUE }],
315  ["pdfjs.disabled", { what: RECORD_PREF_VALUE }],
316  ["places.history.enabled", { what: RECORD_PREF_VALUE }],
317  ["plugins.show_infobar", { what: RECORD_PREF_VALUE }],
318  ["privacy.fuzzyfox.enabled", { what: RECORD_PREF_VALUE }],
319  ["privacy.firstparty.isolate", { what: RECORD_PREF_VALUE }],
320  ["privacy.resistFingerprinting", { what: RECORD_PREF_VALUE }],
321  ["privacy.trackingprotection.enabled", { what: RECORD_PREF_VALUE }],
322  ["privacy.donottrackheader.enabled", { what: RECORD_PREF_VALUE }],
323  ["security.enterprise_roots.auto-enabled", { what: RECORD_PREF_VALUE }],
324  ["security.enterprise_roots.enabled", { what: RECORD_PREF_VALUE }],
325  ["security.pki.mitm_detected", { what: RECORD_PREF_VALUE }],
326  ["security.mixed_content.block_active_content", { what: RECORD_PREF_VALUE }],
327  ["security.mixed_content.block_display_content", { what: RECORD_PREF_VALUE }],
328  ["security.tls.version.enable-deprecated", { what: RECORD_PREF_VALUE }],
329  ["signon.management.page.breach-alerts.enabled", { what: RECORD_PREF_VALUE }],
330  ["signon.autofillForms", { what: RECORD_PREF_VALUE }],
331  ["signon.generation.enabled", { what: RECORD_PREF_VALUE }],
332  ["signon.rememberSignons", { what: RECORD_PREF_VALUE }],
333  ["toolkit.telemetry.pioneerId", { what: RECORD_PREF_STATE }],
334  ["widget.content.allow-gtk-dark-theme", { what: RECORD_DEFAULTPREF_VALUE }],
335  ["widget.content.gtk-theme-override", { what: RECORD_PREF_STATE }],
336  [
337    "widget.content.gtk-high-contrast.enabled",
338    { what: RECORD_DEFAULTPREF_VALUE },
339  ],
340  ["xpinstall.signatures.required", { what: RECORD_PREF_VALUE }],
341  ["nimbus.debug", { what: RECORD_PREF_VALUE }],
342]);
343
344const LOGGER_NAME = "Toolkit.Telemetry";
345
346const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
347const PREF_DISTRIBUTION_ID = "distribution.id";
348const PREF_DISTRIBUTION_VERSION = "distribution.version";
349const PREF_DISTRIBUTOR = "app.distributor";
350const PREF_DISTRIBUTOR_CHANNEL = "app.distributor.channel";
351const PREF_APP_PARTNER_BRANCH = "app.partner.";
352const PREF_PARTNER_ID = "mozilla.partner.id";
353
354const COMPOSITOR_CREATED_TOPIC = "compositor:created";
355const COMPOSITOR_PROCESS_ABORTED_TOPIC = "compositor:process-aborted";
356const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC =
357  "distribution-customization-complete";
358const GFX_FEATURES_READY_TOPIC = "gfx-features-ready";
359const SEARCH_ENGINE_MODIFIED_TOPIC = "browser-search-engine-modified";
360const SEARCH_SERVICE_TOPIC = "browser-search-service";
361const SESSIONSTORE_WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored";
362const PREF_CHANGED_TOPIC = "nsPref:changed";
363const BLOCKLIST_LOADED_TOPIC = "plugin-blocklist-loaded";
364const AUTO_UPDATE_PREF_CHANGE_TOPIC =
365  UpdateUtils.PER_INSTALLATION_PREFS["app.update.auto"].observerTopic;
366const BACKGROUND_UPDATE_PREF_CHANGE_TOPIC =
367  UpdateUtils.PER_INSTALLATION_PREFS["app.update.background.enabled"]
368    .observerTopic;
369const SERVICES_INFO_CHANGE_TOPIC = "sync-ui-state:update";
370
371/**
372 * Enforces the parameter to a boolean value.
373 * @param aValue The input value.
374 * @return {Boolean|Object} If aValue is a boolean or a number, returns its truthfulness
375 *         value. Otherwise, return null.
376 */
377function enforceBoolean(aValue) {
378  if (typeof aValue !== "number" && typeof aValue !== "boolean") {
379    return null;
380  }
381  return Boolean(aValue);
382}
383
384/**
385 * Get the current browser locale.
386 * @return a string with the locale or null on failure.
387 */
388function getBrowserLocale() {
389  try {
390    return Services.locale.appLocaleAsBCP47;
391  } catch (e) {
392    return null;
393  }
394}
395
396/**
397 * Get the current OS locale.
398 * @return a string with the OS locale or null on failure.
399 */
400function getSystemLocale() {
401  try {
402    return Cc["@mozilla.org/intl/ospreferences;1"].getService(
403      Ci.mozIOSPreferences
404    ).systemLocale;
405  } catch (e) {
406    return null;
407  }
408}
409
410/**
411 * Get the current OS locales.
412 * @return an array of strings with the OS locales or null on failure.
413 */
414function getSystemLocales() {
415  try {
416    return Cc["@mozilla.org/intl/ospreferences;1"].getService(
417      Ci.mozIOSPreferences
418    ).systemLocales;
419  } catch (e) {
420    return null;
421  }
422}
423
424/**
425 * Get the current OS regional preference locales.
426 * @return an array of strings with the OS regional preference locales or null on failure.
427 */
428function getRegionalPrefsLocales() {
429  try {
430    return Cc["@mozilla.org/intl/ospreferences;1"].getService(
431      Ci.mozIOSPreferences
432    ).regionalPrefsLocales;
433  } catch (e) {
434    return null;
435  }
436}
437
438function getIntlSettings() {
439  return {
440    requestedLocales: Services.locale.requestedLocales,
441    availableLocales: Services.locale.availableLocales,
442    appLocales: Services.locale.appLocalesAsBCP47,
443    systemLocales: getSystemLocales(),
444    regionalPrefsLocales: getRegionalPrefsLocales(),
445    acceptLanguages: Services.prefs
446      .getComplexValue("intl.accept_languages", Ci.nsIPrefLocalizedString)
447      .data.split(",")
448      .map(str => str.trim()),
449  };
450}
451
452/**
453 * Safely get a sysinfo property and return its value. If the property is not
454 * available, return aDefault.
455 *
456 * @param aPropertyName the property name to get.
457 * @param aDefault the value to return if aPropertyName is not available.
458 * @return The property value, if available, or aDefault.
459 */
460function getSysinfoProperty(aPropertyName, aDefault) {
461  try {
462    // |getProperty| may throw if |aPropertyName| does not exist.
463    return Services.sysinfo.getProperty(aPropertyName);
464  } catch (e) {}
465
466  return aDefault;
467}
468
469/**
470 * Safely get a gfxInfo field and return its value. If the field is not available, return
471 * aDefault.
472 *
473 * @param aPropertyName the property name to get.
474 * @param aDefault the value to return if aPropertyName is not available.
475 * @return The property value, if available, or aDefault.
476 */
477function getGfxField(aPropertyName, aDefault) {
478  let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
479
480  try {
481    // Accessing the field may throw if |aPropertyName| does not exist.
482    let gfxProp = gfxInfo[aPropertyName];
483    if (gfxProp !== undefined && gfxProp !== "") {
484      return gfxProp;
485    }
486  } catch (e) {}
487
488  return aDefault;
489}
490
491/**
492 * Returns a substring of the input string.
493 *
494 * @param {String} aString The input string.
495 * @param {Integer} aMaxLength The maximum length of the returned substring. If this is
496 *        greater than the length of the input string, we return the whole input string.
497 * @return {String} The substring or null if the input string is null.
498 */
499function limitStringToLength(aString, aMaxLength) {
500  if (typeof aString !== "string") {
501    return null;
502  }
503  return aString.substring(0, aMaxLength);
504}
505
506/**
507 * Force a value to be a string.
508 * Only if the value is null, null is returned instead.
509 */
510function forceToStringOrNull(aValue) {
511  if (aValue === null) {
512    return null;
513  }
514
515  return String(aValue);
516}
517
518/**
519 * Get the information about a graphic adapter.
520 *
521 * @param aSuffix A suffix to add to the properties names.
522 * @return An object containing the adapter properties.
523 */
524function getGfxAdapter(aSuffix = "") {
525  // Note that gfxInfo, and so getGfxField, might return "Unknown" for the RAM on failures,
526  // not null.
527  let memoryMB = parseInt(getGfxField("adapterRAM" + aSuffix, null), 10);
528  if (Number.isNaN(memoryMB)) {
529    memoryMB = null;
530  }
531
532  return {
533    description: getGfxField("adapterDescription" + aSuffix, null),
534    vendorID: getGfxField("adapterVendorID" + aSuffix, null),
535    deviceID: getGfxField("adapterDeviceID" + aSuffix, null),
536    subsysID: getGfxField("adapterSubsysID" + aSuffix, null),
537    RAM: memoryMB,
538    driver: getGfxField("adapterDriver" + aSuffix, null),
539    driverVendor: getGfxField("adapterDriverVendor" + aSuffix, null),
540    driverVersion: getGfxField("adapterDriverVersion" + aSuffix, null),
541    driverDate: getGfxField("adapterDriverDate" + aSuffix, null),
542  };
543}
544
545/**
546 * Encapsulates the asynchronous magic interfacing with the addon manager. The builder
547 * is owned by a parent environment object and is an addon listener.
548 */
549function EnvironmentAddonBuilder(environment) {
550  this._environment = environment;
551
552  // The pending task blocks addon manager shutdown. It can either be the initial load
553  // or a change load.
554  this._pendingTask = null;
555
556  // Have we added an observer to listen for blocklist changes that still needs to be
557  // removed:
558  this._blocklistObserverAdded = false;
559
560  // Set to true once initial load is complete and we're watching for changes.
561  this._loaded = false;
562
563  // The state reported by the shutdown blocker if we hang shutdown.
564  this._shutdownState = "Initial";
565}
566EnvironmentAddonBuilder.prototype = {
567  /**
568   * Get the initial set of addons.
569   * @returns Promise<void> when the initial load is complete.
570   */
571  async init() {
572    AddonManager.beforeShutdown.addBlocker(
573      "EnvironmentAddonBuilder",
574      () => this._shutdownBlocker(),
575      { fetchState: () => this._shutdownState }
576    );
577
578    this._pendingTask = (async () => {
579      try {
580        this._shutdownState = "Awaiting _updateAddons";
581        // Gather initial addons details
582        await this._updateAddons(true);
583
584        if (!this._environment._addonsAreFull) {
585          // The addon database has not been loaded, wait for it to
586          // initialize and gather full data as soon as it does.
587          this._shutdownState = "Awaiting AddonManagerPrivate.databaseReady";
588          await AddonManagerPrivate.databaseReady;
589
590          // Now gather complete addons details.
591          this._shutdownState = "Awaiting second _updateAddons";
592          await this._updateAddons();
593        }
594      } catch (err) {
595        this._environment._log.error("init - Exception in _updateAddons", err);
596      } finally {
597        this._pendingTask = null;
598        this._shutdownState = "_pendingTask init complete. No longer blocking.";
599      }
600    })();
601
602    return this._pendingTask;
603  },
604
605  /**
606   * Register an addon listener and watch for changes.
607   */
608  watchForChanges() {
609    this._loaded = true;
610    AddonManager.addAddonListener(this);
611  },
612
613  // AddonListener
614  onEnabled(addon) {
615    this._onAddonChange(addon);
616  },
617  onDisabled(addon) {
618    this._onAddonChange(addon);
619  },
620  onInstalled(addon) {
621    this._onAddonChange(addon);
622  },
623  onUninstalling(addon) {
624    this._onAddonChange(addon);
625  },
626  onUninstalled(addon) {
627    this._onAddonChange(addon);
628  },
629
630  _onAddonChange(addon) {
631    if (addon && addon.isBuiltin && !addon.isSystem) {
632      return;
633    }
634    this._environment._log.trace("_onAddonChange");
635    this._checkForChanges("addons-changed");
636  },
637
638  // nsIObserver
639  observe(aSubject, aTopic, aData) {
640    this._environment._log.trace("observe - Topic " + aTopic);
641    if (aTopic == BLOCKLIST_LOADED_TOPIC) {
642      Services.obs.removeObserver(this, BLOCKLIST_LOADED_TOPIC);
643      this._blocklistObserverAdded = false;
644      let gmpPluginsPromise = this._getActiveGMPlugins();
645      gmpPluginsPromise.then(
646        gmpPlugins => {
647          let { addons } = this._environment._currentEnvironment;
648          addons.activeGMPlugins = gmpPlugins;
649        },
650        err => {
651          this._environment._log.error(
652            "blocklist observe: Error collecting plugins",
653            err
654          );
655        }
656      );
657    }
658  },
659
660  _checkForChanges(changeReason) {
661    if (this._pendingTask) {
662      this._environment._log.trace(
663        "_checkForChanges - task already pending, dropping change with reason " +
664          changeReason
665      );
666      return;
667    }
668
669    this._shutdownState = "_checkForChanges awaiting _updateAddons";
670    this._pendingTask = this._updateAddons().then(
671      result => {
672        this._pendingTask = null;
673        this._shutdownState = "No longer blocking, _updateAddons resolved";
674        if (result.changed) {
675          this._environment._onEnvironmentChange(
676            changeReason,
677            result.oldEnvironment
678          );
679        }
680      },
681      err => {
682        this._pendingTask = null;
683        this._shutdownState = "No longer blocking, _updateAddons rejected";
684        this._environment._log.error(
685          "_checkForChanges: Error collecting addons",
686          err
687        );
688      }
689    );
690  },
691
692  _shutdownBlocker() {
693    if (this._loaded) {
694      AddonManager.removeAddonListener(this);
695      if (this._blocklistObserverAdded) {
696        Services.obs.removeObserver(this, BLOCKLIST_LOADED_TOPIC);
697      }
698    }
699
700    // At startup, _pendingTask is set to a Promise that does not resolve
701    // until the addons database has been read so complete details about
702    // addons are available.  Returning it here will cause it to block
703    // profileBeforeChange, guranteeing that full information will be
704    // available by the time profileBeforeChangeTelemetry is fired.
705    return this._pendingTask;
706  },
707
708  /**
709   * Collect the addon data for the environment.
710   *
711   * This should only be called from _pendingTask; otherwise we risk
712   * running this during addon manager shutdown.
713   *
714   * @param {boolean} [atStartup]
715   *        True if this is the first check we're performing at startup. In that
716   *        situation, we defer some more expensive initialization.
717   *
718   * @returns Promise<Object> This returns a Promise resolved with a status object with the following members:
719   *   changed - Whether the environment changed.
720   *   oldEnvironment - Only set if a change occured, contains the environment data before the change.
721   */
722  async _updateAddons(atStartup) {
723    this._environment._log.trace("_updateAddons");
724
725    let addons = {
726      activeAddons: await this._getActiveAddons(),
727      theme: await this._getActiveTheme(),
728      activeGMPlugins: await this._getActiveGMPlugins(atStartup),
729    };
730
731    let result = {
732      changed:
733        !this._environment._currentEnvironment.addons ||
734        !ObjectUtils.deepEqual(
735          addons.activeAddons,
736          this._environment._currentEnvironment.addons.activeAddons
737        ),
738    };
739
740    if (result.changed) {
741      this._environment._log.trace("_updateAddons: addons differ");
742      result.oldEnvironment = Cu.cloneInto(
743        this._environment._currentEnvironment,
744        myScope
745      );
746    }
747    this._environment._currentEnvironment.addons = addons;
748
749    return result;
750  },
751
752  /**
753   * Get the addon data in object form.
754   * @return Promise<object> containing the addon data.
755   */
756  async _getActiveAddons() {
757    // Request addons, asynchronously.
758    let { addons: allAddons, fullData } = await AddonManager.getActiveAddons([
759      "extension",
760      "service",
761    ]);
762
763    this._environment._addonsAreFull = fullData;
764    let activeAddons = {};
765    for (let addon of allAddons) {
766      // Don't collect any information about the new built-in search webextensions
767      if (addon.isBuiltin && !addon.isSystem) {
768        continue;
769      }
770      // Weird addon data in the wild can lead to exceptions while collecting
771      // the data.
772      try {
773        // Make sure to have valid dates.
774        let updateDate = new Date(Math.max(0, addon.updateDate));
775
776        activeAddons[addon.id] = {
777          version: limitStringToLength(addon.version, MAX_ADDON_STRING_LENGTH),
778          scope: addon.scope,
779          type: addon.type,
780          updateDay: Utils.millisecondsToDays(updateDate.getTime()),
781          isSystem: addon.isSystem,
782          isWebExtension: addon.isWebExtension,
783          multiprocessCompatible: true,
784        };
785
786        // getActiveAddons() gives limited data during startup and full
787        // data after the addons database is loaded.
788        if (fullData) {
789          let installDate = new Date(Math.max(0, addon.installDate));
790          Object.assign(activeAddons[addon.id], {
791            blocklisted:
792              addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
793            description: limitStringToLength(
794              addon.description,
795              MAX_ADDON_STRING_LENGTH
796            ),
797            name: limitStringToLength(addon.name, MAX_ADDON_STRING_LENGTH),
798            userDisabled: enforceBoolean(addon.userDisabled),
799            appDisabled: addon.appDisabled,
800            foreignInstall: enforceBoolean(addon.foreignInstall),
801            hasBinaryComponents: false,
802            installDay: Utils.millisecondsToDays(installDate.getTime()),
803            signedState: addon.signedState,
804          });
805        }
806      } catch (ex) {
807        this._environment._log.error(
808          "_getActiveAddons - An addon was discarded due to an error",
809          ex
810        );
811        continue;
812      }
813    }
814
815    return activeAddons;
816  },
817
818  /**
819   * Get the currently active theme data in object form.
820   * @return Promise<object> containing the active theme data.
821   */
822  async _getActiveTheme() {
823    // Request themes, asynchronously.
824    let { addons: themes } = await AddonManager.getActiveAddons(["theme"]);
825
826    let activeTheme = {};
827    // We only store information about the active theme.
828    let theme = themes.find(theme => theme.isActive);
829    if (theme) {
830      // Make sure to have valid dates.
831      let installDate = new Date(Math.max(0, theme.installDate));
832      let updateDate = new Date(Math.max(0, theme.updateDate));
833
834      activeTheme = {
835        id: theme.id,
836        blocklisted:
837          theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
838        description: limitStringToLength(
839          theme.description,
840          MAX_ADDON_STRING_LENGTH
841        ),
842        name: limitStringToLength(theme.name, MAX_ADDON_STRING_LENGTH),
843        userDisabled: enforceBoolean(theme.userDisabled),
844        appDisabled: theme.appDisabled,
845        version: limitStringToLength(theme.version, MAX_ADDON_STRING_LENGTH),
846        scope: theme.scope,
847        foreignInstall: enforceBoolean(theme.foreignInstall),
848        hasBinaryComponents: false,
849        installDay: Utils.millisecondsToDays(installDate.getTime()),
850        updateDay: Utils.millisecondsToDays(updateDate.getTime()),
851      };
852    }
853
854    return activeTheme;
855  },
856
857  /**
858   * Get the GMPlugins data in object form.
859   *
860   * @param {boolean} [atStartup]
861   *        True if this is the first check we're performing at startup. In that
862   *        situation, we defer some more expensive initialization.
863   *
864   * @return Object containing the GMPlugins data.
865   *
866   * This should only be called from _pendingTask; otherwise we risk
867   * running this during addon manager shutdown.
868   */
869  async _getActiveGMPlugins(atStartup) {
870    // If we haven't yet loaded the blocklist, pass back dummy data for now,
871    // and add an observer to update this data as soon as we get it.
872    if (atStartup || !Services.blocklist.isLoaded) {
873      if (!this._blocklistObserverAdded) {
874        Services.obs.addObserver(this, BLOCKLIST_LOADED_TOPIC);
875        this._blocklistObserverAdded = true;
876      }
877      return {
878        "dummy-gmp": {
879          version: "0.1",
880          userDisabled: false,
881          applyBackgroundUpdates: 1,
882        },
883      };
884    }
885    // Request plugins, asynchronously.
886    let allPlugins = await AddonManager.getAddonsByTypes(["plugin"]);
887
888    let activeGMPlugins = {};
889    for (let plugin of allPlugins) {
890      // Only get info for active GMplugins.
891      if (!plugin.isGMPlugin || !plugin.isActive) {
892        continue;
893      }
894
895      try {
896        activeGMPlugins[plugin.id] = {
897          version: plugin.version,
898          userDisabled: enforceBoolean(plugin.userDisabled),
899          applyBackgroundUpdates: plugin.applyBackgroundUpdates,
900        };
901      } catch (ex) {
902        this._environment._log.error(
903          "_getActiveGMPlugins - A GMPlugin was discarded due to an error",
904          ex
905        );
906        continue;
907      }
908    }
909
910    return activeGMPlugins;
911  },
912};
913
914function EnvironmentCache() {
915  this._log = Log.repository.getLoggerWithMessagePrefix(
916    LOGGER_NAME,
917    "TelemetryEnvironment::"
918  );
919  this._log.trace("constructor");
920
921  this._shutdown = false;
922  // Don't allow querying the search service too early to prevent
923  // impacting the startup performance.
924  this._canQuerySearch = false;
925  // To guard against slowing down startup, defer gathering heavy environment
926  // entries until the session is restored.
927  this._sessionWasRestored = false;
928
929  // A map of listeners that will be called on environment changes.
930  this._changeListeners = new Map();
931
932  // A map of watched preferences which trigger an Environment change when
933  // modified. Every entry contains a recording policy (RECORD_PREF_*).
934  this._watchedPrefs = DEFAULT_ENVIRONMENT_PREFS;
935
936  this._currentEnvironment = {
937    build: this._getBuild(),
938    partner: this._getPartner(),
939    system: this._getSystem(),
940  };
941
942  this._addObservers();
943
944  // Build the remaining asynchronous parts of the environment. Don't register change listeners
945  // until the initial environment has been built.
946
947  let p = [this._updateSettings()];
948  this._addonBuilder = new EnvironmentAddonBuilder(this);
949  p.push(this._addonBuilder.init());
950
951  this._currentEnvironment.profile = {};
952  p.push(this._updateProfile());
953  if (AppConstants.MOZ_BUILD_APP == "browser") {
954    p.push(this._loadAttributionAsync());
955  }
956  p.push(this._loadAsyncUpdateSettings());
957  p.push(this._loadIntlData());
958
959  for (const [
960    id,
961    { branch, options },
962  ] of gActiveExperimentStartupBuffer.entries()) {
963    this.setExperimentActive(id, branch, options);
964  }
965  gActiveExperimentStartupBuffer = null;
966
967  let setup = () => {
968    this._initTask = null;
969    this._startWatchingPrefs();
970    this._addonBuilder.watchForChanges();
971    this._updateGraphicsFeatures();
972    return this.currentEnvironment;
973  };
974
975  this._initTask = Promise.all(p).then(
976    () => setup(),
977    err => {
978      // log errors but eat them for consumers
979      this._log.error("EnvironmentCache - error while initializing", err);
980      return setup();
981    }
982  );
983
984  // Addons may contain partial or full data depending on whether the Addons DB
985  // has had a chance to load. Do we have full data yet?
986  this._addonsAreFull = false;
987}
988EnvironmentCache.prototype = {
989  /**
990   * The current environment data. The returned data is cloned to avoid
991   * unexpected sharing or mutation.
992   * @returns object
993   */
994  get currentEnvironment() {
995    return Cu.cloneInto(this._currentEnvironment, myScope);
996  },
997
998  /**
999   * Wait for the current enviroment to be fully initialized.
1000   * @returns Promise<object>
1001   */
1002  onInitialized() {
1003    if (this._initTask) {
1004      return this._initTask;
1005    }
1006    return Promise.resolve(this.currentEnvironment);
1007  },
1008
1009  /**
1010   * This gets called when the delayed init completes.
1011   */
1012  async delayedInit() {
1013    this._processData = await Services.sysinfo.processInfo;
1014    let processData = await Services.sysinfo.processInfo;
1015    // Remove isWow64 and isWowARM64 from processData
1016    // to strip it down to just CPU info
1017    delete processData.isWow64;
1018    delete processData.isWowARM64;
1019
1020    let oldEnv = null;
1021    if (!this._initTask) {
1022      oldEnv = this.currentEnvironment;
1023    }
1024
1025    this._cpuData = this._getCPUData();
1026    // Augment the return value from the promises with cached values
1027    this._cpuData = { ...processData, ...this._cpuData };
1028
1029    this._currentEnvironment.system.cpu = this._getCPUData();
1030
1031    if (AppConstants.platform == "win") {
1032      this._hddData = await Services.sysinfo.diskInfo;
1033      let osData = await Services.sysinfo.osInfo;
1034
1035      if (!this._initTask) {
1036        // We've finished creating the initial env, so notify for the update
1037        // This is all a bit awkward because `currentEnvironment` clones
1038        // the object, which we need to pass to the notification, but we
1039        // should only notify once we've updated the current environment...
1040        // Ideally, _onEnvironmentChange should somehow deal with all this
1041        // instead of all the consumers.
1042        oldEnv = this.currentEnvironment;
1043      }
1044
1045      this._osData = this._getOSData();
1046
1047      // Augment the return values from the promises with cached values
1048      this._osData = Object.assign(osData, this._osData);
1049
1050      this._currentEnvironment.system.os = this._getOSData();
1051      this._currentEnvironment.system.hdd = this._getHDDData();
1052
1053      // Windows only values stored in processData
1054      this._currentEnvironment.system.isWow64 = this._getProcessData().isWow64;
1055      this._currentEnvironment.system.isWowARM64 = this._getProcessData().isWowARM64;
1056    }
1057
1058    if (!this._initTask) {
1059      this._onEnvironmentChange("system-info", oldEnv);
1060    }
1061  },
1062
1063  /**
1064   * Register a listener for environment changes.
1065   * @param name The name of the listener. If a new listener is registered
1066   *             with the same name, the old listener will be replaced.
1067   * @param listener function(reason, oldEnvironment) - Will receive a reason for
1068                     the change and the environment data before the change.
1069   */
1070  registerChangeListener(name, listener) {
1071    this._log.trace("registerChangeListener for " + name);
1072    if (this._shutdown) {
1073      this._log.warn("registerChangeListener - already shutdown");
1074      return;
1075    }
1076    this._changeListeners.set(name, listener);
1077  },
1078
1079  /**
1080   * Unregister from listening to environment changes.
1081   * It's fine to call this on an unitialized TelemetryEnvironment.
1082   * @param name The name of the listener to remove.
1083   */
1084  unregisterChangeListener(name) {
1085    this._log.trace("unregisterChangeListener for " + name);
1086    if (this._shutdown) {
1087      this._log.warn("registerChangeListener - already shutdown");
1088      return;
1089    }
1090    this._changeListeners.delete(name);
1091  },
1092
1093  setExperimentActive(id, branch, options) {
1094    this._log.trace(`setExperimentActive - id: ${id}, branch: ${branch}`);
1095    // Make sure both the id and the branch have sane lengths.
1096    const saneId = limitStringToLength(id, MAX_EXPERIMENT_ID_LENGTH);
1097    const saneBranch = limitStringToLength(
1098      branch,
1099      MAX_EXPERIMENT_BRANCH_LENGTH
1100    );
1101    if (!saneId || !saneBranch) {
1102      this._log.error(
1103        "setExperimentActive - the provided arguments are not strings."
1104      );
1105      return;
1106    }
1107
1108    // Warn the user about any content truncation.
1109    if (saneId.length != id.length || saneBranch.length != branch.length) {
1110      this._log.warn(
1111        "setExperimentActive - the experiment id or branch were truncated."
1112      );
1113    }
1114
1115    // Truncate the experiment type if present.
1116    if (options.hasOwnProperty("type")) {
1117      let type = limitStringToLength(options.type, MAX_EXPERIMENT_TYPE_LENGTH);
1118      if (type.length != options.type.length) {
1119        options.type = type;
1120        this._log.warn(
1121          "setExperimentActive - the experiment type was truncated."
1122        );
1123      }
1124    }
1125
1126    // Truncate the enrollment id if present.
1127    if (options.hasOwnProperty("enrollmentId")) {
1128      let enrollmentId = limitStringToLength(
1129        options.enrollmentId,
1130        MAX_EXPERIMENT_ENROLLMENT_ID_LENGTH
1131      );
1132      if (enrollmentId.length != options.enrollmentId.length) {
1133        options.enrollmentId = enrollmentId;
1134        this._log.warn(
1135          "setExperimentActive - the enrollment id was truncated."
1136        );
1137      }
1138    }
1139
1140    let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
1141    // Add the experiment annotation.
1142    let experiments = this._currentEnvironment.experiments || {};
1143    experiments[saneId] = { branch: saneBranch };
1144    if (options.hasOwnProperty("type")) {
1145      experiments[saneId].type = options.type;
1146    }
1147    if (options.hasOwnProperty("enrollmentId")) {
1148      experiments[saneId].enrollmentId = options.enrollmentId;
1149    }
1150    this._currentEnvironment.experiments = experiments;
1151    // Notify of the change.
1152    this._onEnvironmentChange("experiment-annotation-changed", oldEnvironment);
1153  },
1154
1155  setExperimentInactive(id) {
1156    this._log.trace("setExperimentInactive");
1157    let experiments = this._currentEnvironment.experiments || {};
1158    if (id in experiments) {
1159      // Only attempt to notify if a previous annotation was found and removed.
1160      let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
1161      // Remove the experiment annotation.
1162      delete this._currentEnvironment.experiments[id];
1163      // Notify of the change.
1164      this._onEnvironmentChange(
1165        "experiment-annotation-changed",
1166        oldEnvironment
1167      );
1168    }
1169  },
1170
1171  getActiveExperiments() {
1172    return Cu.cloneInto(this._currentEnvironment.experiments || {}, myScope);
1173  },
1174
1175  shutdown() {
1176    this._log.trace("shutdown");
1177    this._shutdown = true;
1178  },
1179
1180  /**
1181   * Only used in tests, set the preferences to watch.
1182   * @param aPreferences A map of preferences names and their recording policy.
1183   */
1184  async _watchPreferences(aPreferences) {
1185    this._stopWatchingPrefs();
1186    this._watchedPrefs = aPreferences;
1187    await this._updateSettings();
1188    this._startWatchingPrefs();
1189  },
1190
1191  /**
1192   * Get an object containing the values for the watched preferences. Depending on the
1193   * policy, the value for a preference or whether it was changed by user is reported.
1194   *
1195   * @return An object containing the preferences values.
1196   */
1197  _getPrefData() {
1198    let prefData = {};
1199    for (let [pref, policy] of this._watchedPrefs.entries()) {
1200      let prefValue = this._getPrefValue(pref, policy.what);
1201
1202      if (prefValue === undefined) {
1203        continue;
1204      }
1205
1206      prefData[pref] = prefValue;
1207    }
1208    return prefData;
1209  },
1210
1211  /**
1212   * Get the value of a preference given the preference name and the policy.
1213   * @param pref Name of the preference.
1214   * @param what Policy of the preference.
1215   *
1216   * @returns The value we need to store for this preference. It can be undefined
1217   *          or null if the preference is invalid or has a value set by the user.
1218   */
1219  _getPrefValue(pref, what) {
1220    // Check the policy for the preference and decide if we need to store its value
1221    // or whether it changed from the default value.
1222    let prefType = Services.prefs.getPrefType(pref);
1223
1224    if (
1225      what == TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE ||
1226      what == TelemetryEnvironment.RECORD_DEFAULTPREF_STATE
1227    ) {
1228      // For default prefs, make sure they exist
1229      if (prefType == Ci.nsIPrefBranch.PREF_INVALID) {
1230        return undefined;
1231      }
1232    } else if (!Services.prefs.prefHasUserValue(pref)) {
1233      // For user prefs, make sure they are set
1234      return undefined;
1235    }
1236
1237    if (what == TelemetryEnvironment.RECORD_DEFAULTPREF_STATE) {
1238      return "<set>";
1239    } else if (what == TelemetryEnvironment.RECORD_PREF_STATE) {
1240      return "<user-set>";
1241    } else if (prefType == Ci.nsIPrefBranch.PREF_STRING) {
1242      return Services.prefs.getStringPref(pref);
1243    } else if (prefType == Ci.nsIPrefBranch.PREF_BOOL) {
1244      return Services.prefs.getBoolPref(pref);
1245    } else if (prefType == Ci.nsIPrefBranch.PREF_INT) {
1246      return Services.prefs.getIntPref(pref);
1247    } else if (prefType == Ci.nsIPrefBranch.PREF_INVALID) {
1248      return null;
1249    }
1250    throw new Error(
1251      `Unexpected preference type ("${prefType}") for "${pref}".`
1252    );
1253  },
1254
1255  QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
1256
1257  /**
1258   * Start watching the preferences.
1259   */
1260  _startWatchingPrefs() {
1261    this._log.trace("_startWatchingPrefs - " + this._watchedPrefs);
1262
1263    Services.prefs.addObserver("", this, true);
1264  },
1265
1266  _onPrefChanged(aData) {
1267    this._log.trace("_onPrefChanged");
1268    let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
1269    this._currentEnvironment.settings.userPrefs[aData] = this._getPrefValue(
1270      aData,
1271      this._watchedPrefs.get(aData).what
1272    );
1273    this._onEnvironmentChange("pref-changed", oldEnvironment);
1274  },
1275
1276  /**
1277   * Do not receive any more change notifications for the preferences.
1278   */
1279  _stopWatchingPrefs() {
1280    this._log.trace("_stopWatchingPrefs");
1281
1282    Services.prefs.removeObserver("", this);
1283  },
1284
1285  _addObservers() {
1286    // Watch the search engine change and service topics.
1287    Services.obs.addObserver(this, SESSIONSTORE_WINDOWS_RESTORED_TOPIC);
1288    Services.obs.addObserver(this, COMPOSITOR_CREATED_TOPIC);
1289    Services.obs.addObserver(this, COMPOSITOR_PROCESS_ABORTED_TOPIC);
1290    Services.obs.addObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
1291    Services.obs.addObserver(this, GFX_FEATURES_READY_TOPIC);
1292    Services.obs.addObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC);
1293    Services.obs.addObserver(this, SEARCH_SERVICE_TOPIC);
1294    Services.obs.addObserver(this, AUTO_UPDATE_PREF_CHANGE_TOPIC);
1295    Services.obs.addObserver(this, BACKGROUND_UPDATE_PREF_CHANGE_TOPIC);
1296    Services.obs.addObserver(this, SERVICES_INFO_CHANGE_TOPIC);
1297  },
1298
1299  _removeObservers() {
1300    Services.obs.removeObserver(this, SESSIONSTORE_WINDOWS_RESTORED_TOPIC);
1301    Services.obs.removeObserver(this, COMPOSITOR_CREATED_TOPIC);
1302    Services.obs.removeObserver(this, COMPOSITOR_PROCESS_ABORTED_TOPIC);
1303    try {
1304      Services.obs.removeObserver(
1305        this,
1306        DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC
1307      );
1308    } catch (ex) {}
1309    Services.obs.removeObserver(this, GFX_FEATURES_READY_TOPIC);
1310    Services.obs.removeObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC);
1311    Services.obs.removeObserver(this, SEARCH_SERVICE_TOPIC);
1312    Services.obs.removeObserver(this, AUTO_UPDATE_PREF_CHANGE_TOPIC);
1313    Services.obs.removeObserver(this, BACKGROUND_UPDATE_PREF_CHANGE_TOPIC);
1314    Services.obs.removeObserver(this, SERVICES_INFO_CHANGE_TOPIC);
1315  },
1316
1317  observe(aSubject, aTopic, aData) {
1318    this._log.trace("observe - aTopic: " + aTopic + ", aData: " + aData);
1319    switch (aTopic) {
1320      case SEARCH_ENGINE_MODIFIED_TOPIC:
1321        if (
1322          aData != "engine-default" &&
1323          aData != "engine-default-private" &&
1324          aData != "engine-changed"
1325        ) {
1326          return;
1327        }
1328        if (
1329          aData == "engine-changed" &&
1330          aSubject.QueryInterface(Ci.nsISearchEngine) &&
1331          Services.search.defaultEngine != aSubject
1332        ) {
1333          return;
1334        }
1335        // Record the new default search choice and send the change notification.
1336        this._onSearchEngineChange();
1337        break;
1338      case SEARCH_SERVICE_TOPIC:
1339        if (aData != "init-complete") {
1340          return;
1341        }
1342        // Now that the search engine init is complete, record the default search choice.
1343        this._canQuerySearch = true;
1344        this._updateSearchEngine();
1345        break;
1346      case GFX_FEATURES_READY_TOPIC:
1347      case COMPOSITOR_CREATED_TOPIC:
1348        // Full graphics information is not available until we have created at
1349        // least one off-main-thread-composited window. Thus we wait for the
1350        // first compositor to be created and then query nsIGfxInfo again.
1351        this._updateGraphicsFeatures();
1352        break;
1353      case COMPOSITOR_PROCESS_ABORTED_TOPIC:
1354        // Our compositor process has been killed for whatever reason, so refresh
1355        // our reported graphics features and trigger an environment change.
1356        this._onCompositorProcessAborted();
1357        break;
1358      case DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC:
1359        // Distribution customizations are applied after final-ui-startup. query
1360        // partner prefs again when they are ready.
1361        this._updatePartner();
1362        Services.obs.removeObserver(this, aTopic);
1363        break;
1364      case SESSIONSTORE_WINDOWS_RESTORED_TOPIC:
1365        this._sessionWasRestored = true;
1366        // Make sure to initialize the search service once we've done restoring
1367        // the windows, so that we don't risk loosing search data.
1368        Services.search.init();
1369        // The default browser check could take some time, so just call it after
1370        // the session was restored.
1371        this._updateDefaultBrowser();
1372        break;
1373      case PREF_CHANGED_TOPIC:
1374        let options = this._watchedPrefs.get(aData);
1375        if (options && !options.requiresRestart) {
1376          this._onPrefChanged(aData);
1377        }
1378        break;
1379      case AUTO_UPDATE_PREF_CHANGE_TOPIC:
1380        this._currentEnvironment.settings.update.autoDownload = aData == "true";
1381        break;
1382      case BACKGROUND_UPDATE_PREF_CHANGE_TOPIC:
1383        this._currentEnvironment.settings.update.background = aData == "true";
1384        break;
1385      case SERVICES_INFO_CHANGE_TOPIC:
1386        this._updateServicesInfo();
1387        break;
1388    }
1389  },
1390
1391  /**
1392   * Update the default search engine value.
1393   */
1394  async _updateSearchEngine() {
1395    if (!this._canQuerySearch) {
1396      this._log.trace("_updateSearchEngine - ignoring early call");
1397      return;
1398    }
1399
1400    this._log.trace(
1401      "_updateSearchEngine - isInitialized: " + Services.search.isInitialized
1402    );
1403    if (!Services.search.isInitialized) {
1404      return;
1405    }
1406
1407    // Make sure we have a settings section.
1408    this._currentEnvironment.settings = this._currentEnvironment.settings || {};
1409
1410    // Update the search engine entry in the current environment.
1411    const defaultEngineInfo = await Services.search.getDefaultEngineInfo();
1412    this._currentEnvironment.settings.defaultSearchEngine =
1413      defaultEngineInfo.defaultSearchEngine;
1414    this._currentEnvironment.settings.defaultSearchEngineData = {
1415      ...defaultEngineInfo.defaultSearchEngineData,
1416    };
1417    if ("defaultPrivateSearchEngine" in defaultEngineInfo) {
1418      this._currentEnvironment.settings.defaultPrivateSearchEngine =
1419        defaultEngineInfo.defaultPrivateSearchEngine;
1420    }
1421    if ("defaultPrivateSearchEngineData" in defaultEngineInfo) {
1422      this._currentEnvironment.settings.defaultPrivateSearchEngineData = {
1423        ...defaultEngineInfo.defaultPrivateSearchEngineData,
1424      };
1425    }
1426  },
1427
1428  /**
1429   * Update the default search engine value and trigger the environment change.
1430   */
1431  async _onSearchEngineChange() {
1432    this._log.trace("_onSearchEngineChange");
1433
1434    // Finally trigger the environment change notification.
1435    let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
1436    await this._updateSearchEngine();
1437    this._onEnvironmentChange("search-engine-changed", oldEnvironment);
1438  },
1439
1440  /**
1441   * Refresh the Telemetry environment and trigger an environment change due to
1442   * a change in compositor process (normally this will mean we've fallen back
1443   * from out-of-process to in-process compositing).
1444   */
1445  _onCompositorProcessAborted() {
1446    this._log.trace("_onCompositorProcessAborted");
1447
1448    // Trigger the environment change notification.
1449    let oldEnvironment = Cu.cloneInto(this._currentEnvironment, myScope);
1450    this._updateGraphicsFeatures();
1451    this._onEnvironmentChange("gfx-features-changed", oldEnvironment);
1452  },
1453
1454  /**
1455   * Update the graphics features object.
1456   */
1457  _updateGraphicsFeatures() {
1458    let gfxData = this._currentEnvironment.system.gfx;
1459    try {
1460      let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
1461      gfxData.features = gfxInfo.getFeatures();
1462    } catch (e) {
1463      this._log.error("nsIGfxInfo.getFeatures() caught error", e);
1464    }
1465  },
1466
1467  /**
1468   * Update the partner prefs.
1469   */
1470  _updatePartner() {
1471    this._currentEnvironment.partner = this._getPartner();
1472  },
1473
1474  /**
1475   * Get the build data in object form.
1476   * @return Object containing the build data.
1477   */
1478  _getBuild() {
1479    let buildData = {
1480      applicationId: Services.appinfo.ID || null,
1481      applicationName: Services.appinfo.name || null,
1482      architecture: Services.sysinfo.get("arch"),
1483      buildId: Services.appinfo.appBuildID || null,
1484      version: Services.appinfo.version || null,
1485      vendor: Services.appinfo.vendor || null,
1486      displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY || null,
1487      platformVersion: Services.appinfo.platformVersion || null,
1488      xpcomAbi: Services.appinfo.XPCOMABI,
1489      updaterAvailable: AppConstants.MOZ_UPDATER,
1490    };
1491
1492    return buildData;
1493  },
1494
1495  /**
1496   * Determine if we're the default browser.
1497   * @returns null on error, true if we are the default browser, or false otherwise.
1498   */
1499  _isDefaultBrowser() {
1500    let isDefault = (service, ...args) => {
1501      try {
1502        return !!service.isDefaultBrowser(...args);
1503      } catch (ex) {
1504        this._log.error(
1505          "_isDefaultBrowser - Could not determine if default browser",
1506          ex
1507        );
1508        return null;
1509      }
1510    };
1511
1512    if (!("@mozilla.org/browser/shell-service;1" in Cc)) {
1513      this._log.info(
1514        "_isDefaultBrowser - Could not obtain browser shell service"
1515      );
1516      return null;
1517    }
1518
1519    try {
1520      let { ShellService } = ChromeUtils.import(
1521        "resource:///modules/ShellService.jsm"
1522      );
1523      // This uses the same set of flags used by the pref pane.
1524      return isDefault(ShellService, false, true);
1525    } catch (ex) {
1526      this._log.error("_isDefaultBrowser - Could not obtain shell service JSM");
1527    }
1528
1529    try {
1530      let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService(
1531        Ci.nsIShellService
1532      );
1533      // This uses the same set of flags used by the pref pane.
1534      return isDefault(shellService, true);
1535    } catch (ex) {
1536      this._log.error("_isDefaultBrowser - Could not obtain shell service", ex);
1537      return null;
1538    }
1539  },
1540
1541  _updateDefaultBrowser() {
1542    if (AppConstants.platform === "android") {
1543      return;
1544    }
1545    // Make sure to have a settings section.
1546    this._currentEnvironment.settings = this._currentEnvironment.settings || {};
1547    this._currentEnvironment.settings.isDefaultBrowser = this
1548      ._sessionWasRestored
1549      ? this._isDefaultBrowser()
1550      : null;
1551  },
1552
1553  /**
1554   * Update the cached settings data.
1555   */
1556  async _updateSettings() {
1557    let updateChannel = null;
1558    try {
1559      updateChannel = Utils.getUpdateChannel();
1560    } catch (e) {}
1561
1562    this._currentEnvironment.settings = {
1563      blocklistEnabled: Services.prefs.getBoolPref(
1564        PREF_BLOCKLIST_ENABLED,
1565        true
1566      ),
1567      e10sEnabled: Services.appinfo.browserTabsRemoteAutostart,
1568      e10sMultiProcesses: Services.appinfo.maxWebProcessCount,
1569      fissionEnabled: Services.appinfo.fissionAutostart,
1570      telemetryEnabled: Utils.isTelemetryEnabled,
1571      locale: getBrowserLocale(),
1572      // We need to wait for browser-delayed-startup-finished to ensure that the locales
1573      // have settled, once that's happened we can get the intl data directly.
1574      intl: Policy._intlLoaded ? getIntlSettings() : {},
1575      update: {
1576        channel: updateChannel,
1577        enabled: !Services.policies || Services.policies.isAllowed("appUpdate"),
1578      },
1579      userPrefs: this._getPrefData(),
1580      sandbox: this._getSandboxData(),
1581    };
1582
1583    // Services.appinfo.launcherProcessState is not available in all build
1584    // configurations, in which case an exception may be thrown.
1585    try {
1586      this._currentEnvironment.settings.launcherProcessState =
1587        Services.appinfo.launcherProcessState;
1588    } catch (e) {}
1589
1590    this._currentEnvironment.settings.addonCompatibilityCheckEnabled =
1591      AddonManager.checkCompatibility;
1592
1593    this._updateAttribution();
1594    this._updateDefaultBrowser();
1595    await this._updateSearchEngine();
1596    this._loadAsyncUpdateSettingsFromCache();
1597  },
1598
1599  _getSandboxData() {
1600    let effectiveContentProcessLevel = null;
1601    let contentWin32kLockdownState = null;
1602    try {
1603      let sandboxSettings = Cc[
1604        "@mozilla.org/sandbox/sandbox-settings;1"
1605      ].getService(Ci.mozISandboxSettings);
1606      effectiveContentProcessLevel =
1607        sandboxSettings.effectiveContentSandboxLevel;
1608
1609      // See `ContentWin32kLockdownState` in
1610      // <security/sandbox/common/SandboxSettings.h>
1611      //
1612      // Values:
1613      // 1 = LockdownEnabled
1614      // 2 = MissingWebRender
1615      // 3 = OperatingSystemNotSupported
1616      // 4 = PrefNotSet
1617      contentWin32kLockdownState = sandboxSettings.contentWin32kLockdownState;
1618    } catch (e) {}
1619    return {
1620      effectiveContentProcessLevel,
1621      contentWin32kLockdownState,
1622    };
1623  },
1624
1625  /**
1626   * Update the cached profile data.
1627   * @returns Promise<> resolved when the I/O is complete.
1628   */
1629  async _updateProfile() {
1630    let profileAccessor = await ProfileAge();
1631
1632    let creationDate = await profileAccessor.created;
1633    let resetDate = await profileAccessor.reset;
1634    let firstUseDate = await profileAccessor.firstUse;
1635
1636    this._currentEnvironment.profile.creationDate = Utils.millisecondsToDays(
1637      creationDate
1638    );
1639    if (resetDate) {
1640      this._currentEnvironment.profile.resetDate = Utils.millisecondsToDays(
1641        resetDate
1642      );
1643    }
1644    if (firstUseDate) {
1645      this._currentEnvironment.profile.firstUseDate = Utils.millisecondsToDays(
1646        firstUseDate
1647      );
1648    }
1649  },
1650
1651  /**
1652   * Load the attribution data object and updates the environment.
1653   * @returns Promise<> resolved when the I/O is complete.
1654   */
1655  async _loadAttributionAsync() {
1656    try {
1657      await AttributionCode.getAttrDataAsync();
1658    } catch (e) {
1659      // The AttributionCode.jsm module might not be always available
1660      // (e.g. tests). Gracefully handle this.
1661      return;
1662    }
1663    this._updateAttribution();
1664  },
1665
1666  /**
1667   * Update the environment with the cached attribution data.
1668   */
1669  _updateAttribution() {
1670    let data = null;
1671    try {
1672      data = AttributionCode.getCachedAttributionData();
1673    } catch (e) {
1674      // The AttributionCode.jsm module might not be always available
1675      // (e.g. tests). Gracefully handle this.
1676    }
1677
1678    if (!data || !Object.keys(data).length) {
1679      return;
1680    }
1681
1682    let attributionData = {};
1683    for (let key in data) {
1684      attributionData[key] = limitStringToLength(
1685        data[key],
1686        MAX_ATTRIBUTION_STRING_LENGTH
1687      );
1688    }
1689    this._currentEnvironment.settings.attribution = attributionData;
1690  },
1691
1692  /**
1693   * Load the per-installation update settings, cache them, and add them to the
1694   * environment.
1695   */
1696  async _loadAsyncUpdateSettings() {
1697    if (AppConstants.MOZ_UPDATER) {
1698      this._updateAutoDownloadCache = await UpdateUtils.getAppUpdateAutoEnabled();
1699      this._updateBackgroundCache = await UpdateUtils.readUpdateConfigSetting(
1700        "app.update.background.enabled"
1701      );
1702    } else {
1703      this._updateAutoDownloadCache = false;
1704      this._updateBackgroundCache = false;
1705    }
1706    this._loadAsyncUpdateSettingsFromCache();
1707  },
1708
1709  /**
1710   * Update the environment with the cached values for per-installation update
1711   * settings.
1712   */
1713  _loadAsyncUpdateSettingsFromCache() {
1714    if (this._updateAutoDownloadCache !== undefined) {
1715      this._currentEnvironment.settings.update.autoDownload = this._updateAutoDownloadCache;
1716    }
1717    if (this._updateBackgroundCache !== undefined) {
1718      this._currentEnvironment.settings.update.background = this._updateBackgroundCache;
1719    }
1720  },
1721
1722  /**
1723   * Get i18n data about the system.
1724   * @return A promise of completion.
1725   */
1726  async _loadIntlData() {
1727    // Wait for the startup topic.
1728    await Policy._browserDelayedStartup();
1729    this._currentEnvironment.settings.intl = getIntlSettings();
1730    Policy._intlLoaded = true;
1731  },
1732  // This exists as a separate function for testing.
1733  async _getFxaSignedInUser() {
1734    return fxAccounts.getSignedInUser();
1735  },
1736
1737  async _updateServicesInfo() {
1738    let syncEnabled = false;
1739    let accountEnabled = false;
1740    let weaveService = Cc["@mozilla.org/weave/service;1"].getService()
1741      .wrappedJSObject;
1742    syncEnabled = weaveService && weaveService.enabled;
1743    if (syncEnabled) {
1744      // All sync users are account users, definitely.
1745      accountEnabled = true;
1746    } else {
1747      // Not all account users are sync users. See if they're signed into FxA.
1748      try {
1749        let user = await this._getFxaSignedInUser();
1750        if (user) {
1751          accountEnabled = true;
1752        }
1753      } catch (e) {
1754        // We don't know. This might be a transient issue which will clear
1755        // itself up later, but the information in telemetry is quite possibly stale
1756        // (this is called from a change listener), so clear it out to avoid
1757        // reporting data which might be wrong until we can figure it out.
1758        delete this._currentEnvironment.services;
1759        this._log.error("_updateServicesInfo() caught error", e);
1760        return;
1761      }
1762    }
1763    this._currentEnvironment.services = {
1764      accountEnabled,
1765      syncEnabled,
1766    };
1767  },
1768
1769  /**
1770   * Get the partner data in object form.
1771   * @return Object containing the partner data.
1772   */
1773  _getPartner() {
1774    let partnerData = {
1775      distributionId: Services.prefs.getStringPref(PREF_DISTRIBUTION_ID, null),
1776      distributionVersion: Services.prefs.getStringPref(
1777        PREF_DISTRIBUTION_VERSION,
1778        null
1779      ),
1780      partnerId: Services.prefs.getStringPref(PREF_PARTNER_ID, null),
1781      distributor: Services.prefs.getStringPref(PREF_DISTRIBUTOR, null),
1782      distributorChannel: Services.prefs.getStringPref(
1783        PREF_DISTRIBUTOR_CHANNEL,
1784        null
1785      ),
1786    };
1787
1788    // Get the PREF_APP_PARTNER_BRANCH branch and append its children to partner data.
1789    let partnerBranch = Services.prefs.getBranch(PREF_APP_PARTNER_BRANCH);
1790    partnerData.partnerNames = partnerBranch.getChildList("");
1791
1792    return partnerData;
1793  },
1794
1795  _cpuData: null,
1796  /**
1797   * Get the CPU information.
1798   * @return Object containing the CPU information data.
1799   */
1800  _getCPUData() {
1801    if (this._cpuData) {
1802      return this._cpuData;
1803    }
1804
1805    this._cpuData = {};
1806
1807    const CPU_EXTENSIONS = [
1808      "hasMMX",
1809      "hasSSE",
1810      "hasSSE2",
1811      "hasSSE3",
1812      "hasSSSE3",
1813      "hasSSE4A",
1814      "hasSSE4_1",
1815      "hasSSE4_2",
1816      "hasAVX",
1817      "hasAVX2",
1818      "hasAES",
1819      "hasEDSP",
1820      "hasARMv6",
1821      "hasARMv7",
1822      "hasNEON",
1823      "hasUserCET",
1824    ];
1825
1826    // Enumerate the available CPU extensions.
1827    let availableExts = [];
1828    for (let ext of CPU_EXTENSIONS) {
1829      if (getSysinfoProperty(ext, false)) {
1830        availableExts.push(ext);
1831      }
1832    }
1833
1834    this._cpuData.extensions = availableExts;
1835
1836    return this._cpuData;
1837  },
1838
1839  _processData: null,
1840  /**
1841   * Get the process information.
1842   * @return Object containing the process information data.
1843   */
1844  _getProcessData() {
1845    if (this._processData) {
1846      return this._processData;
1847    }
1848    return {};
1849  },
1850
1851  /**
1852   * Get the device information, if we are on a portable device.
1853   * @return Object containing the device information data, or null if
1854   * not a portable device.
1855   */
1856  _getDeviceData() {
1857    if (AppConstants.platform !== "android") {
1858      return null;
1859    }
1860
1861    return {
1862      model: getSysinfoProperty("device", null),
1863      manufacturer: getSysinfoProperty("manufacturer", null),
1864      hardware: getSysinfoProperty("hardware", null),
1865      isTablet: getSysinfoProperty("tablet", null),
1866    };
1867  },
1868
1869  _osData: null,
1870  /**
1871   * Get the OS information.
1872   * @return Object containing the OS data.
1873   */
1874  _getOSData() {
1875    if (this._osData) {
1876      return this._osData;
1877    }
1878    this._osData = {
1879      name: forceToStringOrNull(getSysinfoProperty("name", null)),
1880      version: forceToStringOrNull(getSysinfoProperty("version", null)),
1881      locale: forceToStringOrNull(getSystemLocale()),
1882    };
1883
1884    if (AppConstants.platform == "android") {
1885      this._osData.kernelVersion = forceToStringOrNull(
1886        getSysinfoProperty("kernel_version", null)
1887      );
1888    } else if (AppConstants.platform === "win") {
1889      // The path to the "UBR" key, queried to get additional version details on Windows.
1890      const WINDOWS_UBR_KEY_PATH =
1891        "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
1892
1893      let versionInfo = WindowsVersionInfo.get({ throwOnError: false });
1894      this._osData.servicePackMajor = versionInfo.servicePackMajor;
1895      this._osData.servicePackMinor = versionInfo.servicePackMinor;
1896      this._osData.windowsBuildNumber = versionInfo.buildNumber;
1897      // We only need the UBR if we're at or above Windows 10.
1898      if (
1899        typeof this._osData.version === "string" &&
1900        Services.vc.compare(this._osData.version, "10") >= 0
1901      ) {
1902        // Query the UBR key and only add it to the environment if it's available.
1903        // |readRegKey| doesn't throw, but rather returns 'undefined' on error.
1904        let ubr = WindowsRegistry.readRegKey(
1905          Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
1906          WINDOWS_UBR_KEY_PATH,
1907          "UBR",
1908          Ci.nsIWindowsRegKey.WOW64_64
1909        );
1910        this._osData.windowsUBR = ubr !== undefined ? ubr : null;
1911      }
1912    }
1913
1914    return this._osData;
1915  },
1916
1917  _hddData: null,
1918  /**
1919   * Get the HDD information.
1920   * @return Object containing the HDD data.
1921   */
1922  _getHDDData() {
1923    if (this._hddData) {
1924      return this._hddData;
1925    }
1926    let nullData = { model: null, revision: null, type: null };
1927    return { profile: nullData, binary: nullData, system: nullData };
1928  },
1929
1930  /**
1931   * Get registered security product information.
1932   * @return Object containing the security product data
1933   */
1934  _getSecurityAppData() {
1935    const maxStringLength = 256;
1936
1937    const keys = [
1938      ["registeredAntiVirus", "antivirus"],
1939      ["registeredAntiSpyware", "antispyware"],
1940      ["registeredFirewall", "firewall"],
1941    ];
1942
1943    let result = {};
1944
1945    for (let [inKey, outKey] of keys) {
1946      let prop = getSysinfoProperty(inKey, null);
1947      if (prop) {
1948        prop = limitStringToLength(prop, maxStringLength).split(";");
1949      }
1950
1951      result[outKey] = prop;
1952    }
1953
1954    return result;
1955  },
1956
1957  /**
1958   * Get the GFX information.
1959   * @return Object containing the GFX data.
1960   */
1961  _getGFXData() {
1962    let gfxData = {
1963      D2DEnabled: getGfxField("D2DEnabled", null),
1964      DWriteEnabled: getGfxField("DWriteEnabled", null),
1965      ContentBackend: getGfxField("ContentBackend", null),
1966      Headless: getGfxField("isHeadless", null),
1967      EmbeddedInFirefoxReality: getGfxField("EmbeddedInFirefoxReality", null),
1968      // The following line is disabled due to main thread jank and will be enabled
1969      // again as part of bug 1154500.
1970      // DWriteVersion: getGfxField("DWriteVersion", null),
1971      adapters: [],
1972      monitors: [],
1973      features: {},
1974    };
1975
1976    if (AppConstants.platform !== "android") {
1977      let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
1978      try {
1979        gfxData.monitors = gfxInfo.getMonitors();
1980      } catch (e) {
1981        this._log.error("nsIGfxInfo.getMonitors() caught error", e);
1982      }
1983    }
1984
1985    try {
1986      let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
1987      gfxData.features = gfxInfo.getFeatures();
1988    } catch (e) {
1989      this._log.error("nsIGfxInfo.getFeatures() caught error", e);
1990    }
1991
1992    // GfxInfo does not yet expose a way to iterate through all the adapters.
1993    gfxData.adapters.push(getGfxAdapter(""));
1994    gfxData.adapters[0].GPUActive = true;
1995
1996    // If we have a second adapter add it to the gfxData.adapters section.
1997    let hasGPU2 = getGfxField("adapterDeviceID2", null) !== null;
1998    if (!hasGPU2) {
1999      this._log.trace("_getGFXData - Only one display adapter detected.");
2000      return gfxData;
2001    }
2002
2003    this._log.trace("_getGFXData - Two display adapters detected.");
2004
2005    gfxData.adapters.push(getGfxAdapter("2"));
2006    gfxData.adapters[1].GPUActive = getGfxField("isGPU2Active", null);
2007
2008    return gfxData;
2009  },
2010
2011  /**
2012   * Get the system data in object form.
2013   * @return Object containing the system data.
2014   */
2015  _getSystem() {
2016    let memoryMB = getSysinfoProperty("memsize", null);
2017    if (memoryMB) {
2018      // Send RAM size in megabytes. Rounding because sysinfo doesn't
2019      // always provide RAM in multiples of 1024.
2020      memoryMB = Math.round(memoryMB / 1024 / 1024);
2021    }
2022
2023    let virtualMB = getSysinfoProperty("virtualmemsize", null);
2024    if (virtualMB) {
2025      // Send the total virtual memory size in megabytes. Rounding because
2026      // sysinfo doesn't always provide RAM in multiples of 1024.
2027      virtualMB = Math.round(virtualMB / 1024 / 1024);
2028    }
2029
2030    let data = {
2031      memoryMB,
2032      virtualMaxMB: virtualMB,
2033      cpu: this._getCPUData(),
2034      os: this._getOSData(),
2035      hdd: this._getHDDData(),
2036      gfx: this._getGFXData(),
2037      appleModelId: getSysinfoProperty("appleModelId", null),
2038    };
2039
2040    if (AppConstants.platform === "win") {
2041      data = { ...this._getProcessData(), ...data };
2042    } else if (AppConstants.platform == "android") {
2043      data.device = this._getDeviceData();
2044    }
2045
2046    // Windows 8+
2047    if (AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) {
2048      data.sec = this._getSecurityAppData();
2049    }
2050
2051    return data;
2052  },
2053
2054  _onEnvironmentChange(what, oldEnvironment) {
2055    this._log.trace("_onEnvironmentChange for " + what);
2056
2057    // We are already skipping change events in _checkChanges if there is a pending change task running.
2058    if (this._shutdown) {
2059      this._log.trace("_onEnvironmentChange - Already shut down.");
2060      return;
2061    }
2062
2063    if (ObjectUtils.deepEqual(this._currentEnvironment, oldEnvironment)) {
2064      this._log.trace("_onEnvironmentChange - Environment didn't change");
2065      return;
2066    }
2067
2068    for (let [name, listener] of this._changeListeners) {
2069      try {
2070        this._log.debug("_onEnvironmentChange - calling " + name);
2071        listener(what, oldEnvironment);
2072      } catch (e) {
2073        this._log.error(
2074          "_onEnvironmentChange - listener " + name + " caught error",
2075          e
2076        );
2077      }
2078    }
2079  },
2080
2081  reset() {
2082    this._shutdown = false;
2083  },
2084};
2085