tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

firefox-view-tabs-setup-manager.sys.mjs (21188B)


      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 /**
      6 * This module exports the TabsSetupFlowManager singleton, which manages the state and
      7 * diverse inputs which drive the Firefox View synced tabs setup flow
      8 */
      9 
     10 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
     11 
     12 const lazy = {};
     13 
     14 ChromeUtils.defineESModuleGetters(lazy, {
     15  Log: "resource://gre/modules/Log.sys.mjs",
     16  SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
     17  SyncedTabsErrorHandler:
     18    "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs",
     19  UIState: "resource://services-sync/UIState.sys.mjs",
     20 });
     21 
     22 ChromeUtils.defineLazyGetter(lazy, "syncUtils", () => {
     23  return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs")
     24    .Utils;
     25 });
     26 
     27 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
     28  return ChromeUtils.importESModule(
     29    "resource://gre/modules/FxAccounts.sys.mjs"
     30  ).getFxAccountsSingleton();
     31 });
     32 
     33 const SYNC_TABS_PREF = "services.sync.engine.tabs";
     34 const TOPIC_TABS_CHANGED = "services.sync.tabs.changed";
     35 const LOGGING_PREF = "browser.tabs.firefox-view.logLevel";
     36 const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
     37 const TOPIC_DEVICESTATE_CHANGED = "firefox-view.devicestate.changed";
     38 const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated";
     39 const NETWORK_STATUS_CHANGED = "network:offline-status-changed";
     40 const SYNC_SERVICE_ERROR = "weave:service:sync:error";
     41 const FXA_DEVICE_CONNECTED = "fxaccounts:device_connected";
     42 const FXA_DEVICE_DISCONNECTED = "fxaccounts:device_disconnected";
     43 const SYNC_SERVICE_FINISHED = "weave:service:sync:finish";
     44 const PRIMARY_PASSWORD_UNLOCKED = "passwordmgr-crypto-login";
     45 
     46 function openTabInWindow(window, url) {
     47  // Null checks as the passed window might be closing, particularly in tests.
     48  const ownerGlobal = window.docShell?.chromeEventHandler?.ownerGlobal;
     49  ownerGlobal?.switchToTabHavingURI(url, true, {});
     50 }
     51 
     52 export const TabsSetupFlowManager = new (class {
     53  constructor() {
     54    this.QueryInterface = ChromeUtils.generateQI(["nsIObserver"]);
     55 
     56    this.setupState = new Map();
     57    this.resetInternalState();
     58    this._currentSetupStateName = "";
     59    this.syncIsConnected = lazy.UIState.get().syncEnabled;
     60    this.didFxaTabOpen = false;
     61 
     62    this.registerSetupState({
     63      uiStateIndex: 0,
     64      name: "error-state",
     65      exitConditions: () => {
     66        return lazy.SyncedTabsErrorHandler.isSyncReady();
     67      },
     68    });
     69    this.registerSetupState({
     70      uiStateIndex: 1,
     71      name: "not-signed-in",
     72      exitConditions: () => {
     73        return this.fxaSignedIn;
     74      },
     75    });
     76    this.registerSetupState({
     77      uiStateIndex: 2,
     78      name: "connect-secondary-device",
     79      exitConditions: () => {
     80        return this.secondaryDeviceConnected;
     81      },
     82    });
     83    this.registerSetupState({
     84      uiStateIndex: 3,
     85      name: "disabled-tab-sync",
     86      exitConditions: () => {
     87        return this.syncTabsPrefEnabled;
     88      },
     89    });
     90    this.registerSetupState({
     91      uiStateIndex: 4,
     92      name: "synced-tabs-loaded",
     93      exitConditions: () => {
     94        // This is the end state
     95        return false;
     96      },
     97    });
     98 
     99    Services.obs.addObserver(this, lazy.UIState.ON_UPDATE);
    100    Services.obs.addObserver(this, TOPIC_DEVICELIST_UPDATED);
    101    Services.obs.addObserver(this, NETWORK_STATUS_CHANGED);
    102    Services.obs.addObserver(this, SYNC_SERVICE_ERROR);
    103    Services.obs.addObserver(this, SYNC_SERVICE_FINISHED);
    104    Services.obs.addObserver(this, TOPIC_TABS_CHANGED);
    105    Services.obs.addObserver(this, PRIMARY_PASSWORD_UNLOCKED);
    106    Services.obs.addObserver(this, FXA_DEVICE_CONNECTED);
    107    Services.obs.addObserver(this, FXA_DEVICE_DISCONNECTED);
    108 
    109    // this.syncTabsPrefEnabled will track the value of the tabs pref
    110    XPCOMUtils.defineLazyPreferenceGetter(
    111      this,
    112      "syncTabsPrefEnabled",
    113      SYNC_TABS_PREF,
    114      false,
    115      () => {
    116        this.maybeUpdateUI(true);
    117      }
    118    );
    119 
    120    this._lastFxASignedIn = this.fxaSignedIn;
    121    this.logger.debug(
    122      "TabsSetupFlowManager constructor, fxaSignedIn:",
    123      this._lastFxASignedIn
    124    );
    125    this.onSignedInChange();
    126  }
    127 
    128  resetInternalState() {
    129    // assign initial values for all the managed internal properties
    130    delete this._lastFxASignedIn;
    131    this._currentSetupStateName = "not-signed-in";
    132    this._shouldShowSuccessConfirmation = false;
    133    this._didShowMobilePromo = false;
    134    this.abortWaitingForTabs();
    135 
    136    Services.obs.notifyObservers(null, TOPIC_DEVICESTATE_CHANGED);
    137 
    138    // keep track of what is connected so we can respond to changes
    139    this._deviceStateSnapshot = {
    140      mobileDeviceConnected: this.mobileDeviceConnected,
    141      secondaryDeviceConnected: this.secondaryDeviceConnected,
    142    };
    143    // keep track of tab-pickup-container instance visibilities
    144    this._viewVisibilityStates = new Map();
    145  }
    146 
    147  get isPrimaryPasswordLocked() {
    148    return lazy.syncUtils.mpLocked();
    149  }
    150 
    151  uninit() {
    152    Services.obs.removeObserver(this, lazy.UIState.ON_UPDATE);
    153    Services.obs.removeObserver(this, TOPIC_DEVICELIST_UPDATED);
    154    Services.obs.removeObserver(this, NETWORK_STATUS_CHANGED);
    155    Services.obs.removeObserver(this, SYNC_SERVICE_ERROR);
    156    Services.obs.removeObserver(this, SYNC_SERVICE_FINISHED);
    157    Services.obs.removeObserver(this, TOPIC_TABS_CHANGED);
    158    Services.obs.removeObserver(this, PRIMARY_PASSWORD_UNLOCKED);
    159    Services.obs.removeObserver(this, FXA_DEVICE_CONNECTED);
    160    Services.obs.removeObserver(this, FXA_DEVICE_DISCONNECTED);
    161  }
    162  get hasVisibleViews() {
    163    return Array.from(this._viewVisibilityStates.values()).reduce(
    164      (hasVisible, visibility) => {
    165        return hasVisible || visibility == "visible";
    166      },
    167      false
    168    );
    169  }
    170  get currentSetupState() {
    171    return this.setupState.get(this._currentSetupStateName);
    172  }
    173  get isTabSyncSetupComplete() {
    174    return this.currentSetupState.uiStateIndex >= 4;
    175  }
    176  get uiStateIndex() {
    177    return this.currentSetupState.uiStateIndex;
    178  }
    179  get fxaSignedIn() {
    180    let { UIState } = lazy;
    181    let syncState = UIState.get();
    182    return (
    183      UIState.isReady() &&
    184      syncState.status === UIState.STATUS_SIGNED_IN &&
    185      // syncEnabled just checks the "services.sync.username" pref has a value
    186      syncState.syncEnabled
    187    );
    188  }
    189 
    190  get secondaryDeviceConnected() {
    191    if (!this.fxaSignedIn) {
    192      return false;
    193    }
    194    let recentDevices = lazy.fxAccounts.device?.recentDeviceList?.length;
    195    return recentDevices > 1;
    196  }
    197  get mobileDeviceConnected() {
    198    if (!this.fxaSignedIn) {
    199      return false;
    200    }
    201    let mobileClients = lazy.fxAccounts.device.recentDeviceList?.filter(
    202      device => device.type == "mobile" || device.type == "tablet"
    203    );
    204    return mobileClients?.length > 0;
    205  }
    206  get shouldShowMobilePromo() {
    207    return (
    208      this.syncIsConnected &&
    209      this.fxaSignedIn &&
    210      this.currentSetupState.uiStateIndex >= 4 &&
    211      !this.mobileDeviceConnected &&
    212      !this.mobilePromoDismissedPref
    213    );
    214  }
    215  get shouldShowMobileConnectedSuccess() {
    216    return (
    217      this.currentSetupState.uiStateIndex >= 3 &&
    218      this._shouldShowSuccessConfirmation &&
    219      this.mobileDeviceConnected
    220    );
    221  }
    222  get logger() {
    223    if (!this._log) {
    224      let setupLog = lazy.Log.repository.getLogger("FirefoxView.TabsSetup");
    225      setupLog.manageLevelFromPref(LOGGING_PREF);
    226      setupLog.addAppender(
    227        new lazy.Log.ConsoleAppender(new lazy.Log.BasicFormatter())
    228      );
    229      this._log = setupLog;
    230    }
    231    return this._log;
    232  }
    233 
    234  registerSetupState(state) {
    235    this.setupState.set(state.name, state);
    236  }
    237 
    238  async observe(subject, topic, data) {
    239    switch (topic) {
    240      case lazy.UIState.ON_UPDATE:
    241        this.logger.debug("Handling UIState update");
    242        this.syncIsConnected = lazy.UIState.get().syncEnabled;
    243        if (this._lastFxASignedIn !== this.fxaSignedIn) {
    244          this.onSignedInChange();
    245        } else {
    246          await this.maybeUpdateUI();
    247        }
    248        this._lastFxASignedIn = this.fxaSignedIn;
    249        break;
    250      case TOPIC_DEVICELIST_UPDATED: {
    251        this.logger.debug("Handling observer notification:", topic, data);
    252        const { deviceStateChanged, deviceAdded } = await this.refreshDevices();
    253        if (deviceStateChanged) {
    254          await this.maybeUpdateUI(true);
    255        }
    256        if (deviceAdded && this.secondaryDeviceConnected) {
    257          this.logger.debug("device was added");
    258          this._deviceAddedResultsNeverSeen = true;
    259          if (this.hasVisibleViews) {
    260            this.startWaitingForNewDeviceTabs();
    261          }
    262        }
    263        break;
    264      }
    265      case FXA_DEVICE_CONNECTED:
    266      case FXA_DEVICE_DISCONNECTED:
    267        await lazy.fxAccounts.device.refreshDeviceList({ ignoreCached: true });
    268        await this.maybeUpdateUI(true);
    269        break;
    270      case SYNC_SERVICE_ERROR:
    271        this.logger.debug(`Handling ${SYNC_SERVICE_ERROR}`);
    272        if (lazy.UIState.get().status == lazy.UIState.STATUS_SIGNED_IN) {
    273          this.abortWaitingForTabs();
    274          await this.maybeUpdateUI(true);
    275        }
    276        break;
    277      case NETWORK_STATUS_CHANGED:
    278        this.abortWaitingForTabs();
    279        await this.maybeUpdateUI(true);
    280        break;
    281      case SYNC_SERVICE_FINISHED:
    282        this.logger.debug(`Handling ${SYNC_SERVICE_FINISHED}`);
    283        // We intentionally leave any empty-tabs timestamp
    284        // as we may be still waiting for a sync that delivers some tabs
    285        this._waitingForNextTabSync = false;
    286        await this.maybeUpdateUI(true);
    287        break;
    288      case TOPIC_TABS_CHANGED:
    289        this.stopWaitingForTabs();
    290        break;
    291      case PRIMARY_PASSWORD_UNLOCKED:
    292        this.logger.debug(`Handling ${PRIMARY_PASSWORD_UNLOCKED}`);
    293        this.tryToClearError();
    294        break;
    295    }
    296  }
    297 
    298  updateViewVisibility(instanceId, visibility) {
    299    const wasVisible = this.hasVisibleViews;
    300    this.logger.debug(
    301      `updateViewVisibility for instance: ${instanceId}, visibility: ${visibility}`
    302    );
    303    if (visibility == "unloaded") {
    304      this._viewVisibilityStates.delete(instanceId);
    305    } else {
    306      this._viewVisibilityStates.set(instanceId, visibility);
    307    }
    308    const isVisible = this.hasVisibleViews;
    309    if (isVisible && !wasVisible) {
    310      // If we're already timing waiting for tabs from a newly-added device
    311      // we might be able to stop
    312      if (this._noTabsVisibleFromAddedDeviceTimestamp) {
    313        return this.stopWaitingForNewDeviceTabs();
    314      }
    315      if (this._deviceAddedResultsNeverSeen) {
    316        // If this is the first time a view has been visible since a device was added
    317        // we may want to start the empty-tabs visible timer
    318        return this.startWaitingForNewDeviceTabs();
    319      }
    320    }
    321    if (!isVisible) {
    322      this.logger.debug(
    323        "Resetting timestamp and tabs pending flags as there are no visible views"
    324      );
    325      // if there's no view visible, we're not really waiting anymore
    326      this.abortWaitingForTabs();
    327    }
    328    return null;
    329  }
    330 
    331  get waitingForTabs() {
    332    return (
    333      // signed in & at least 1 other device is syncing indicates there's something to wait for
    334      this.secondaryDeviceConnected && this._waitingForNextTabSync
    335    );
    336  }
    337 
    338  abortWaitingForTabs() {
    339    this._waitingForNextTabSync = false;
    340    // also clear out the device-added / tabs pending flags
    341    this._noTabsVisibleFromAddedDeviceTimestamp = 0;
    342    this._deviceAddedResultsNeverSeen = false;
    343  }
    344 
    345  startWaitingForTabs() {
    346    if (!this._waitingForNextTabSync) {
    347      this._waitingForNextTabSync = true;
    348      Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED);
    349    }
    350  }
    351 
    352  async stopWaitingForTabs() {
    353    const wasWaiting = this.waitingForTabs;
    354    if (this.hasVisibleViews && this._deviceAddedResultsNeverSeen) {
    355      await this.stopWaitingForNewDeviceTabs();
    356    }
    357    this._waitingForNextTabSync = false;
    358    if (wasWaiting) {
    359      Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED);
    360    }
    361  }
    362 
    363  async onSignedInChange() {
    364    this.logger.debug("onSignedInChange, fxaSignedIn:", this.fxaSignedIn);
    365    // update UI to make the state change
    366    await this.maybeUpdateUI(true);
    367    if (!this.fxaSignedIn) {
    368      // As we just signed out, ensure the waiting flag is reset for next time around
    369      this.abortWaitingForTabs();
    370      return;
    371    }
    372 
    373    // Now we need to figure out if we have recently synced tabs to show
    374    // Or, if we are going to need to trigger a tab sync for them
    375    const recentTabs = await lazy.SyncedTabs.getRecentTabs(50);
    376 
    377    if (!this.fxaSignedIn) {
    378      // We got signed-out in the meantime. We should get an ON_UPDATE which will put us
    379      // back in the right state, so we just do nothing here
    380      return;
    381    }
    382 
    383    // When SyncedTabs has resolved the getRecentTabs promise,
    384    // we also know we can update devices-related internal state
    385    const { deviceStateChanged } = await this.refreshDevices();
    386    if (deviceStateChanged) {
    387      this.logger.debug(
    388        "onSignedInChange, after refreshDevices, calling maybeUpdateUI"
    389      );
    390      // give the UI an opportunity to update as secondaryDeviceConnected or
    391      // mobileDeviceConnected have changed value
    392      await this.maybeUpdateUI(true);
    393    }
    394 
    395    // If we can't get recent tabs, we need to trigger a request for them
    396    const tabSyncNeeded = !recentTabs?.length;
    397    this.logger.debug("onSignedInChange, tabSyncNeeded:", tabSyncNeeded);
    398 
    399    if (tabSyncNeeded) {
    400      this.startWaitingForTabs();
    401      this.logger.debug(
    402        "isPrimaryPasswordLocked:",
    403        this.isPrimaryPasswordLocked
    404      );
    405      this.logger.debug("onSignedInChange, no recentTabs, calling syncTabs");
    406      // If the syncTabs call rejects or resolves false we need to clear the waiting
    407      // flag and update UI
    408      this.syncTabs()
    409        .catch(ex => {
    410          this.logger.debug("onSignedInChange, syncTabs rejected:", ex);
    411          this.stopWaitingForTabs();
    412        })
    413        .then(willSync => {
    414          if (!willSync) {
    415            this.logger.debug("onSignedInChange, no tab sync expected");
    416            this.stopWaitingForTabs();
    417          }
    418        });
    419    }
    420  }
    421 
    422  async startWaitingForNewDeviceTabs() {
    423    // if we're already waiting for tabs, don't reset
    424    if (this._noTabsVisibleFromAddedDeviceTimestamp) {
    425      return;
    426    }
    427 
    428    // take a timestamp whenever the latest device is added and we have 0 tabs to show,
    429    // allowing us to track how long we show an empty list after a new device is added
    430    const hasRecentTabs = (await lazy.SyncedTabs.getRecentTabs(1)).length;
    431    if (this.hasVisibleViews && !hasRecentTabs) {
    432      this._noTabsVisibleFromAddedDeviceTimestamp = Date.now();
    433      this.logger.debug(
    434        "New device added with 0 synced tabs to show, storing timestamp:",
    435        this._noTabsVisibleFromAddedDeviceTimestamp
    436      );
    437    }
    438  }
    439 
    440  async stopWaitingForNewDeviceTabs() {
    441    if (!this._noTabsVisibleFromAddedDeviceTimestamp) {
    442      return;
    443    }
    444    const recentTabs = await lazy.SyncedTabs.getRecentTabs(1);
    445    if (recentTabs.length) {
    446      // We have been waiting for > 0 tabs after a newly-added device, record
    447      // the time elapsed
    448      const elapsed = Date.now() - this._noTabsVisibleFromAddedDeviceTimestamp;
    449      this.logger.debug(
    450        "stopWaitingForTabs, resetting _noTabsVisibleFromAddedDeviceTimestamp and recording telemetry:",
    451        Math.round(elapsed / 1000)
    452      );
    453      this._noTabsVisibleFromAddedDeviceTimestamp = 0;
    454      this._deviceAddedResultsNeverSeen = false;
    455    } else {
    456      // we are still waiting for some tabs to show...
    457      this.logger.debug(
    458        "stopWaitingForTabs: Still no recent tabs, we are still waiting"
    459      );
    460    }
    461  }
    462 
    463  async refreshDevices() {
    464    // If current device not found in recent device list, refresh device list
    465    if (
    466      !lazy.fxAccounts.device.recentDeviceList?.some(
    467        device => device.isCurrentDevice
    468      )
    469    ) {
    470      await lazy.fxAccounts.device.refreshDeviceList({ ignoreCached: true });
    471    }
    472 
    473    // compare new values to the previous values
    474    const mobileDeviceConnected = this.mobileDeviceConnected;
    475    const secondaryDeviceConnected = this.secondaryDeviceConnected;
    476    const oldDevicesCount = this._deviceStateSnapshot?.devicesCount ?? 0;
    477    const devicesCount = lazy.fxAccounts.device?.recentDeviceList?.length ?? 0;
    478 
    479    this.logger.debug(
    480      `refreshDevices, mobileDeviceConnected: ${mobileDeviceConnected}, `,
    481      `secondaryDeviceConnected: ${secondaryDeviceConnected}`
    482    );
    483 
    484    let deviceStateChanged =
    485      this._deviceStateSnapshot.mobileDeviceConnected !=
    486        mobileDeviceConnected ||
    487      this._deviceStateSnapshot.secondaryDeviceConnected !=
    488        secondaryDeviceConnected;
    489    if (
    490      mobileDeviceConnected &&
    491      !this._deviceStateSnapshot.mobileDeviceConnected
    492    ) {
    493      // a mobile device was added, show success if we previously showed the promo
    494      this._shouldShowSuccessConfirmation = this._didShowMobilePromo;
    495    } else if (
    496      !mobileDeviceConnected &&
    497      this._deviceStateSnapshot.mobileDeviceConnected
    498    ) {
    499      // no mobile device connected now, reset
    500      this._shouldShowSuccessConfirmation = false;
    501    }
    502    this._deviceStateSnapshot = {
    503      mobileDeviceConnected,
    504      secondaryDeviceConnected,
    505      devicesCount,
    506    };
    507    if (deviceStateChanged) {
    508      this.logger.debug("refreshDevices: device state did change");
    509      if (!secondaryDeviceConnected) {
    510        this.logger.debug(
    511          "We lost a device, now claim sync hasn't worked before."
    512        );
    513        Services.obs.notifyObservers(null, TOPIC_DEVICESTATE_CHANGED);
    514      }
    515    } else {
    516      this.logger.debug("refreshDevices: no device state change");
    517    }
    518    return {
    519      deviceStateChanged,
    520      deviceAdded: oldDevicesCount < devicesCount,
    521    };
    522  }
    523 
    524  async maybeUpdateUI(forceUpdate = false) {
    525    let nextSetupStateName = this._currentSetupStateName;
    526    let errorState = null;
    527    let stateChanged = false;
    528 
    529    // state transition conditions
    530    for (let state of this.setupState.values()) {
    531      nextSetupStateName = state.name;
    532      if (!state.exitConditions()) {
    533        this.logger.debug(
    534          "maybeUpdateUI, conditions not met to exit state: ",
    535          nextSetupStateName
    536        );
    537        break;
    538      }
    539    }
    540 
    541    let setupState = this.currentSetupState;
    542    const state = this.setupState.get(nextSetupStateName);
    543    const uiStateIndex = state.uiStateIndex;
    544 
    545    if (
    546      uiStateIndex == 0 ||
    547      nextSetupStateName != this._currentSetupStateName
    548    ) {
    549      setupState = state;
    550      this._currentSetupStateName = nextSetupStateName;
    551      stateChanged = true;
    552    }
    553    this.logger.debug(
    554      "maybeUpdateUI, will notify update?:",
    555      stateChanged,
    556      forceUpdate
    557    );
    558    if (stateChanged || forceUpdate) {
    559      if (this.shouldShowMobilePromo) {
    560        this._didShowMobilePromo = true;
    561      }
    562      if (uiStateIndex == 0) {
    563        // Use idleDispatch() to give observers a chance to resolve before
    564        // determining the new state.
    565        errorState = await new Promise(resolve => {
    566          ChromeUtils.idleDispatch(() => {
    567            resolve(lazy.SyncedTabsErrorHandler.getErrorType());
    568          });
    569        });
    570        this.logger.debug("maybeUpdateUI, in error state:", errorState);
    571      }
    572      Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED, errorState);
    573    }
    574    if ("function" == typeof setupState.enter) {
    575      setupState.enter();
    576    }
    577  }
    578 
    579  async openFxASignup(window) {
    580    if (!(await lazy.fxAccounts.constructor.canConnectAccount())) {
    581      return;
    582    }
    583    const url =
    584      await lazy.fxAccounts.constructor.config.promiseConnectAccountURI(
    585        "fx-view"
    586      );
    587    this.didFxaTabOpen = true;
    588    openTabInWindow(window, url, true);
    589  }
    590 
    591  async openFxAPairDevice(window) {
    592    const url = await lazy.fxAccounts.constructor.config.promisePairingURI({
    593      entrypoint: "fx-view",
    594    });
    595    this.didFxaTabOpen = true;
    596    openTabInWindow(window, url, true);
    597  }
    598 
    599  syncOpenTabs() {
    600    // Flip the pref on.
    601    // The observer should trigger re-evaluating state and advance to next step
    602    Services.prefs.setBoolPref(SYNC_TABS_PREF, true);
    603  }
    604 
    605  async syncOnPageReload() {
    606    if (lazy.UIState.isReady() && this.fxaSignedIn) {
    607      this.startWaitingForTabs();
    608      await this.syncTabs(true);
    609    }
    610  }
    611 
    612  tryToClearError() {
    613    if (lazy.UIState.isReady() && this.fxaSignedIn) {
    614      this.startWaitingForTabs();
    615      if (this.isPrimaryPasswordLocked) {
    616        lazy.syncUtils.ensureMPUnlocked();
    617      }
    618      this.logger.debug("tryToClearError: triggering new tab sync");
    619      this.syncTabs();
    620      Services.tm.dispatchToMainThread(() => {});
    621    } else {
    622      this.logger.debug(
    623        `tryToClearError: unable to sync, isReady: ${lazy.UIState.isReady()}, fxaSignedIn: ${
    624          this.fxaSignedIn
    625        }`
    626      );
    627    }
    628  }
    629  // For easy overriding in tests
    630  syncTabs(force = false) {
    631    return lazy.SyncedTabs.syncTabs(force);
    632  }
    633 })();