summaryrefslogtreecommitdiffstats
path: root/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs')
-rw-r--r--browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs667
1 files changed, 667 insertions, 0 deletions
diff --git a/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs
new file mode 100644
index 0000000000..3696ae17b1
--- /dev/null
+++ b/browser/components/firefoxview/firefox-view-tabs-setup-manager.sys.mjs
@@ -0,0 +1,667 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This module exports the TabsSetupFlowManager singleton, which manages the state and
+ * diverse inputs which drive the Firefox View synced tabs setup flow
+ */
+
+import { PromiseUtils } from "resource://gre/modules/PromiseUtils.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Log: "resource://gre/modules/Log.sys.mjs",
+ SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs",
+ SyncedTabsErrorHandler:
+ "resource:///modules/firefox-view-synced-tabs-error-handler.sys.mjs",
+ UIState: "resource://services-sync/UIState.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "syncUtils", () => {
+ return ChromeUtils.importESModule("resource://services-sync/util.sys.mjs")
+ .Utils;
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "fxAccounts", () => {
+ return ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ ).getFxAccountsSingleton();
+});
+
+const SYNC_TABS_PREF = "services.sync.engine.tabs";
+const TOPIC_TABS_CHANGED = "services.sync.tabs.changed";
+const MOBILE_PROMO_DISMISSED_PREF =
+ "browser.tabs.firefox-view.mobilePromo.dismissed";
+const LOGGING_PREF = "browser.tabs.firefox-view.logLevel";
+const TOPIC_SETUPSTATE_CHANGED = "firefox-view.setupstate.changed";
+const TOPIC_DEVICESTATE_CHANGED = "firefox-view.devicestate.changed";
+const TOPIC_DEVICELIST_UPDATED = "fxaccounts:devicelist_updated";
+const NETWORK_STATUS_CHANGED = "network:offline-status-changed";
+const SYNC_SERVICE_ERROR = "weave:service:sync:error";
+const FXA_DEVICE_CONNECTED = "fxaccounts:device_connected";
+const FXA_DEVICE_DISCONNECTED = "fxaccounts:device_disconnected";
+const SYNC_SERVICE_FINISHED = "weave:service:sync:finish";
+const PRIMARY_PASSWORD_UNLOCKED = "passwordmgr-crypto-login";
+const TAB_PICKUP_OPEN_STATE_PREF =
+ "browser.tabs.firefox-view.ui-state.tab-pickup.open";
+
+function openTabInWindow(window, url) {
+ const { switchToTabHavingURI } =
+ window.docShell.chromeEventHandler.ownerGlobal;
+ switchToTabHavingURI(url, true, {});
+}
+
+export const TabsSetupFlowManager = new (class {
+ constructor() {
+ this.QueryInterface = ChromeUtils.generateQI(["nsIObserver"]);
+
+ this.setupState = new Map();
+ this.resetInternalState();
+ this._currentSetupStateName = "";
+ this.syncIsConnected = lazy.UIState.get().syncEnabled;
+ this.didFxaTabOpen = false;
+
+ this.registerSetupState({
+ uiStateIndex: 0,
+ name: "error-state",
+ exitConditions: () => {
+ return lazy.SyncedTabsErrorHandler.isSyncReady();
+ },
+ });
+ this.registerSetupState({
+ uiStateIndex: 1,
+ name: "not-signed-in",
+ exitConditions: () => {
+ return this.fxaSignedIn;
+ },
+ });
+ this.registerSetupState({
+ uiStateIndex: 2,
+ name: "connect-secondary-device",
+ exitConditions: () => {
+ return this.secondaryDeviceConnected;
+ },
+ });
+ this.registerSetupState({
+ uiStateIndex: 3,
+ name: "disabled-tab-sync",
+ exitConditions: () => {
+ return this.syncTabsPrefEnabled;
+ },
+ });
+ this.registerSetupState({
+ uiStateIndex: 4,
+ name: "synced-tabs-loaded",
+ exitConditions: () => {
+ // This is the end state
+ return false;
+ },
+ });
+
+ Services.obs.addObserver(this, lazy.UIState.ON_UPDATE);
+ Services.obs.addObserver(this, TOPIC_DEVICELIST_UPDATED);
+ Services.obs.addObserver(this, NETWORK_STATUS_CHANGED);
+ Services.obs.addObserver(this, SYNC_SERVICE_ERROR);
+ Services.obs.addObserver(this, SYNC_SERVICE_FINISHED);
+ Services.obs.addObserver(this, TOPIC_TABS_CHANGED);
+ Services.obs.addObserver(this, PRIMARY_PASSWORD_UNLOCKED);
+ Services.obs.addObserver(this, FXA_DEVICE_CONNECTED);
+ Services.obs.addObserver(this, FXA_DEVICE_DISCONNECTED);
+
+ // this.syncTabsPrefEnabled will track the value of the tabs pref
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "syncTabsPrefEnabled",
+ SYNC_TABS_PREF,
+ false,
+ () => {
+ this.maybeUpdateUI(true);
+ }
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "mobilePromoDismissedPref",
+ MOBILE_PROMO_DISMISSED_PREF,
+ false,
+ () => {
+ this.maybeUpdateUI(true);
+ }
+ );
+
+ this._lastFxASignedIn = this.fxaSignedIn;
+ this.logger.debug(
+ "TabsSetupFlowManager constructor, fxaSignedIn:",
+ this._lastFxASignedIn
+ );
+ this.onSignedInChange();
+ }
+
+ resetInternalState() {
+ // assign initial values for all the managed internal properties
+ delete this._lastFxASignedIn;
+ this._currentSetupStateName = "not-signed-in";
+ this._shouldShowSuccessConfirmation = false;
+ this._didShowMobilePromo = false;
+ this.abortWaitingForTabs();
+
+ Services.obs.notifyObservers(null, TOPIC_DEVICESTATE_CHANGED);
+
+ // keep track of what is connected so we can respond to changes
+ this._deviceStateSnapshot = {
+ mobileDeviceConnected: this.mobileDeviceConnected,
+ secondaryDeviceConnected: this.secondaryDeviceConnected,
+ };
+ // keep track of tab-pickup-container instance visibilities
+ this._viewVisibilityStates = new Map();
+ }
+
+ get isPrimaryPasswordLocked() {
+ return lazy.syncUtils.mpLocked();
+ }
+
+ uninit() {
+ Services.obs.removeObserver(this, lazy.UIState.ON_UPDATE);
+ Services.obs.removeObserver(this, TOPIC_DEVICELIST_UPDATED);
+ Services.obs.removeObserver(this, NETWORK_STATUS_CHANGED);
+ Services.obs.removeObserver(this, SYNC_SERVICE_ERROR);
+ Services.obs.removeObserver(this, SYNC_SERVICE_FINISHED);
+ Services.obs.removeObserver(this, TOPIC_TABS_CHANGED);
+ Services.obs.removeObserver(this, PRIMARY_PASSWORD_UNLOCKED);
+ Services.obs.removeObserver(this, FXA_DEVICE_CONNECTED);
+ Services.obs.removeObserver(this, FXA_DEVICE_DISCONNECTED);
+ }
+ get hasVisibleViews() {
+ return Array.from(this._viewVisibilityStates.values()).reduce(
+ (hasVisible, visibility) => {
+ return hasVisible || visibility == "visible";
+ },
+ false
+ );
+ }
+ get currentSetupState() {
+ return this.setupState.get(this._currentSetupStateName);
+ }
+ get isTabSyncSetupComplete() {
+ return this.currentSetupState.uiStateIndex >= 4;
+ }
+ get uiStateIndex() {
+ return this.currentSetupState.uiStateIndex;
+ }
+ get fxaSignedIn() {
+ let { UIState } = lazy;
+ let syncState = UIState.get();
+ return (
+ UIState.isReady() &&
+ syncState.status === UIState.STATUS_SIGNED_IN &&
+ // syncEnabled just checks the "services.sync.username" pref has a value
+ syncState.syncEnabled
+ );
+ }
+ get secondaryDeviceConnected() {
+ if (!this.fxaSignedIn) {
+ return false;
+ }
+ let recentDevices = lazy.fxAccounts.device?.recentDeviceList?.length;
+ return recentDevices > 1;
+ }
+ get mobileDeviceConnected() {
+ if (!this.fxaSignedIn) {
+ return false;
+ }
+ let mobileClients = lazy.fxAccounts.device.recentDeviceList?.filter(
+ device => device.type == "mobile" || device.type == "tablet"
+ );
+ return mobileClients?.length > 0;
+ }
+ get shouldShowMobilePromo() {
+ return (
+ this.syncIsConnected &&
+ this.fxaSignedIn &&
+ this.currentSetupState.uiStateIndex >= 4 &&
+ !this.mobileDeviceConnected &&
+ !this.mobilePromoDismissedPref
+ );
+ }
+ get shouldShowMobileConnectedSuccess() {
+ return (
+ this.currentSetupState.uiStateIndex >= 3 &&
+ this._shouldShowSuccessConfirmation &&
+ this.mobileDeviceConnected
+ );
+ }
+ get logger() {
+ if (!this._log) {
+ let setupLog = lazy.Log.repository.getLogger("FirefoxView.TabsSetup");
+ setupLog.manageLevelFromPref(LOGGING_PREF);
+ setupLog.addAppender(
+ new lazy.Log.ConsoleAppender(new lazy.Log.BasicFormatter())
+ );
+ this._log = setupLog;
+ }
+ return this._log;
+ }
+
+ registerSetupState(state) {
+ this.setupState.set(state.name, state);
+ }
+
+ async observe(subject, topic, data) {
+ switch (topic) {
+ case lazy.UIState.ON_UPDATE:
+ this.logger.debug("Handling UIState update");
+ this.syncIsConnected = lazy.UIState.get().syncEnabled;
+ if (this._lastFxASignedIn !== this.fxaSignedIn) {
+ this.onSignedInChange();
+ } else {
+ await this.maybeUpdateUI();
+ }
+ this._lastFxASignedIn = this.fxaSignedIn;
+ break;
+ case TOPIC_DEVICELIST_UPDATED:
+ this.logger.debug("Handling observer notification:", topic, data);
+ const { deviceStateChanged, deviceAdded } = await this.refreshDevices();
+ if (deviceStateChanged) {
+ await this.maybeUpdateUI(true);
+ }
+ if (deviceAdded && this.secondaryDeviceConnected) {
+ this.logger.debug("device was added");
+ this._deviceAddedResultsNeverSeen = true;
+ if (this.hasVisibleViews) {
+ this.startWaitingForNewDeviceTabs();
+ }
+ }
+ break;
+ case FXA_DEVICE_CONNECTED:
+ case FXA_DEVICE_DISCONNECTED:
+ await lazy.fxAccounts.device.refreshDeviceList({ ignoreCached: true });
+ await this.maybeUpdateUI(true);
+ break;
+ case SYNC_SERVICE_ERROR:
+ this.logger.debug(`Handling ${SYNC_SERVICE_ERROR}`);
+ if (lazy.UIState.get().status == lazy.UIState.STATUS_SIGNED_IN) {
+ this.abortWaitingForTabs();
+ await this.maybeUpdateUI(true);
+ }
+ break;
+ case NETWORK_STATUS_CHANGED:
+ this.abortWaitingForTabs();
+ await this.maybeUpdateUI(true);
+ break;
+ case SYNC_SERVICE_FINISHED:
+ this.logger.debug(`Handling ${SYNC_SERVICE_FINISHED}`);
+ // We intentionally leave any empty-tabs timestamp
+ // as we may be still waiting for a sync that delivers some tabs
+ this._waitingForNextTabSync = false;
+ await this.maybeUpdateUI(true);
+ break;
+ case TOPIC_TABS_CHANGED:
+ this.stopWaitingForTabs();
+ break;
+ case PRIMARY_PASSWORD_UNLOCKED:
+ this.logger.debug(`Handling ${PRIMARY_PASSWORD_UNLOCKED}`);
+ this.tryToClearError();
+ break;
+ }
+ }
+
+ updateViewVisibility(instanceId, visibility) {
+ const wasVisible = this.hasVisibleViews;
+ this.logger.debug(
+ `updateViewVisibility for instance: ${instanceId}, visibility: ${visibility}`
+ );
+ if (visibility == "unloaded") {
+ this._viewVisibilityStates.delete(instanceId);
+ } else {
+ this._viewVisibilityStates.set(instanceId, visibility);
+ }
+ const isVisible = this.hasVisibleViews;
+ if (isVisible && !wasVisible) {
+ // If we're already timing waiting for tabs from a newly-added device
+ // we might be able to stop
+ if (this._noTabsVisibleFromAddedDeviceTimestamp) {
+ return this.stopWaitingForNewDeviceTabs();
+ }
+ if (this._deviceAddedResultsNeverSeen) {
+ // If this is the first time a view has been visible since a device was added
+ // we may want to start the empty-tabs visible timer
+ return this.startWaitingForNewDeviceTabs();
+ }
+ }
+ if (!isVisible) {
+ this.logger.debug(
+ "Resetting timestamp and tabs pending flags as there are no visible views"
+ );
+ // if there's no view visible, we're not really waiting anymore
+ this.abortWaitingForTabs();
+ }
+ return null;
+ }
+
+ get waitingForTabs() {
+ return (
+ // signed in & at least 1 other device is syncing indicates there's something to wait for
+ this.secondaryDeviceConnected && this._waitingForNextTabSync
+ );
+ }
+
+ abortWaitingForTabs() {
+ this._waitingForNextTabSync = false;
+ // also clear out the device-added / tabs pending flags
+ this._noTabsVisibleFromAddedDeviceTimestamp = 0;
+ this._deviceAddedResultsNeverSeen = false;
+ }
+
+ startWaitingForTabs() {
+ if (!this._waitingForNextTabSync) {
+ this._waitingForNextTabSync = true;
+ Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED);
+ }
+ }
+
+ async stopWaitingForTabs() {
+ const wasWaiting = this.waitingForTabs;
+ if (this.hasVisibleViews && this._deviceAddedResultsNeverSeen) {
+ await this.stopWaitingForNewDeviceTabs();
+ }
+ this._waitingForNextTabSync = false;
+ if (wasWaiting) {
+ Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED);
+ }
+ }
+
+ async onSignedInChange() {
+ this.logger.debug("onSignedInChange, fxaSignedIn:", this.fxaSignedIn);
+ // update UI to make the state change
+ await this.maybeUpdateUI(true);
+ if (!this.fxaSignedIn) {
+ // As we just signed out, ensure the waiting flag is reset for next time around
+ this.abortWaitingForTabs();
+ return;
+ }
+
+ // Set Tab pickup open state pref to true when signing in
+ Services.prefs.setBoolPref(TAB_PICKUP_OPEN_STATE_PREF, true);
+
+ // Now we need to figure out if we have recently synced tabs to show
+ // Or, if we are going to need to trigger a tab sync for them
+ const recentTabs = await lazy.SyncedTabs.getRecentTabs(50);
+
+ if (!this.fxaSignedIn) {
+ // We got signed-out in the meantime. We should get an ON_UPDATE which will put us
+ // back in the right state, so we just do nothing here
+ return;
+ }
+
+ // When SyncedTabs has resolved the getRecentTabs promise,
+ // we also know we can update devices-related internal state
+ const { deviceStateChanged } = await this.refreshDevices();
+ if (deviceStateChanged) {
+ this.logger.debug(
+ "onSignedInChange, after refreshDevices, calling maybeUpdateUI"
+ );
+ // give the UI an opportunity to update as secondaryDeviceConnected or
+ // mobileDeviceConnected have changed value
+ await this.maybeUpdateUI(true);
+ }
+
+ // If we can't get recent tabs, we need to trigger a request for them
+ const tabSyncNeeded = !recentTabs?.length;
+ this.logger.debug("onSignedInChange, tabSyncNeeded:", tabSyncNeeded);
+
+ if (tabSyncNeeded) {
+ this.startWaitingForTabs();
+ this.logger.debug(
+ "isPrimaryPasswordLocked:",
+ this.isPrimaryPasswordLocked
+ );
+ this.logger.debug("onSignedInChange, no recentTabs, calling syncTabs");
+ // If the syncTabs call rejects or resolves false we need to clear the waiting
+ // flag and update UI
+ this.syncTabs()
+ .catch(ex => {
+ this.logger.debug("onSignedInChange, syncTabs rejected:", ex);
+ this.stopWaitingForTabs();
+ })
+ .then(willSync => {
+ if (!willSync) {
+ this.logger.debug("onSignedInChange, no tab sync expected");
+ this.stopWaitingForTabs();
+ }
+ });
+ }
+ }
+
+ async startWaitingForNewDeviceTabs() {
+ // if we're already waiting for tabs, don't reset
+ if (this._noTabsVisibleFromAddedDeviceTimestamp) {
+ return;
+ }
+
+ // take a timestamp whenever the latest device is added and we have 0 tabs to show,
+ // allowing us to track how long we show an empty list after a new device is added
+ const hasRecentTabs = (await lazy.SyncedTabs.getRecentTabs(1)).length;
+ if (this.hasVisibleViews && !hasRecentTabs) {
+ this._noTabsVisibleFromAddedDeviceTimestamp = Date.now();
+ this.logger.debug(
+ "New device added with 0 synced tabs to show, storing timestamp:",
+ this._noTabsVisibleFromAddedDeviceTimestamp
+ );
+ }
+ }
+
+ async stopWaitingForNewDeviceTabs() {
+ if (!this._noTabsVisibleFromAddedDeviceTimestamp) {
+ return;
+ }
+ const recentTabs = await lazy.SyncedTabs.getRecentTabs(1);
+ if (recentTabs.length) {
+ // We have been waiting for > 0 tabs after a newly-added device, record
+ // the time elapsed
+ const elapsed = Date.now() - this._noTabsVisibleFromAddedDeviceTimestamp;
+ this.logger.debug(
+ "stopWaitingForTabs, resetting _noTabsVisibleFromAddedDeviceTimestamp and recording telemetry:",
+ Math.round(elapsed / 1000)
+ );
+ this._noTabsVisibleFromAddedDeviceTimestamp = 0;
+ this._deviceAddedResultsNeverSeen = false;
+ Services.telemetry.recordEvent(
+ "firefoxview",
+ "synced_tabs_empty",
+ "since_device_added",
+ Math.round(elapsed / 1000).toString()
+ );
+ } else {
+ // we are still waiting for some tabs to show...
+ this.logger.debug(
+ "stopWaitingForTabs: Still no recent tabs, we are still waiting"
+ );
+ }
+ }
+
+ async refreshDevices() {
+ // If current device not found in recent device list, refresh device list
+ if (
+ !lazy.fxAccounts.device.recentDeviceList?.some(
+ device => device.isCurrentDevice
+ )
+ ) {
+ await lazy.fxAccounts.device.refreshDeviceList({ ignoreCached: true });
+ }
+
+ // compare new values to the previous values
+ const mobileDeviceConnected = this.mobileDeviceConnected;
+ const secondaryDeviceConnected = this.secondaryDeviceConnected;
+ const oldDevicesCount = this._deviceStateSnapshot?.devicesCount ?? 0;
+ const devicesCount = lazy.fxAccounts.device?.recentDeviceList?.length ?? 0;
+
+ this.logger.debug(
+ `refreshDevices, mobileDeviceConnected: ${mobileDeviceConnected}, `,
+ `secondaryDeviceConnected: ${secondaryDeviceConnected}`
+ );
+
+ let deviceStateChanged =
+ this._deviceStateSnapshot.mobileDeviceConnected !=
+ mobileDeviceConnected ||
+ this._deviceStateSnapshot.secondaryDeviceConnected !=
+ secondaryDeviceConnected;
+ if (
+ mobileDeviceConnected &&
+ !this._deviceStateSnapshot.mobileDeviceConnected
+ ) {
+ // a mobile device was added, show success if we previously showed the promo
+ this._shouldShowSuccessConfirmation = this._didShowMobilePromo;
+ } else if (
+ !mobileDeviceConnected &&
+ this._deviceStateSnapshot.mobileDeviceConnected
+ ) {
+ // no mobile device connected now, reset
+ Services.prefs.clearUserPref(MOBILE_PROMO_DISMISSED_PREF);
+ this._shouldShowSuccessConfirmation = false;
+ }
+ this._deviceStateSnapshot = {
+ mobileDeviceConnected,
+ secondaryDeviceConnected,
+ devicesCount,
+ };
+ if (deviceStateChanged) {
+ this.logger.debug("refreshDevices: device state did change");
+ if (!secondaryDeviceConnected) {
+ this.logger.debug(
+ "We lost a device, now claim sync hasn't worked before."
+ );
+ Services.obs.notifyObservers(null, TOPIC_DEVICESTATE_CHANGED);
+ }
+ } else {
+ this.logger.debug("refreshDevices: no device state change");
+ }
+ return {
+ deviceStateChanged,
+ deviceAdded: oldDevicesCount < devicesCount,
+ };
+ }
+
+ async maybeUpdateUI(forceUpdate = false) {
+ let nextSetupStateName = this._currentSetupStateName;
+ let errorState = null;
+ let stateChanged = false;
+
+ // state transition conditions
+ for (let state of this.setupState.values()) {
+ nextSetupStateName = state.name;
+ if (!state.exitConditions()) {
+ this.logger.debug(
+ "maybeUpdateUI, conditions not met to exit state: ",
+ nextSetupStateName
+ );
+ break;
+ }
+ }
+
+ let setupState = this.currentSetupState;
+ const state = this.setupState.get(nextSetupStateName);
+ const uiStateIndex = state.uiStateIndex;
+
+ if (
+ uiStateIndex == 0 ||
+ nextSetupStateName != this._currentSetupStateName
+ ) {
+ setupState = state;
+ this._currentSetupStateName = nextSetupStateName;
+ stateChanged = true;
+ }
+ this.logger.debug(
+ "maybeUpdateUI, will notify update?:",
+ stateChanged,
+ forceUpdate
+ );
+ if (stateChanged || forceUpdate) {
+ if (this.shouldShowMobilePromo) {
+ this._didShowMobilePromo = true;
+ }
+ if (uiStateIndex == 0) {
+ // Use idleDispatch() to give observers a chance to resolve before
+ // determining the new state.
+ errorState = await PromiseUtils.idleDispatch(() =>
+ lazy.SyncedTabsErrorHandler.getErrorType()
+ );
+ this.logger.debug("maybeUpdateUI, in error state:", errorState);
+ }
+ Services.obs.notifyObservers(null, TOPIC_SETUPSTATE_CHANGED, errorState);
+ }
+ if ("function" == typeof setupState.enter) {
+ setupState.enter();
+ }
+ }
+
+ dismissMobilePromo() {
+ Services.prefs.setBoolPref(MOBILE_PROMO_DISMISSED_PREF, true);
+ }
+
+ dismissMobileConfirmation() {
+ this._shouldShowSuccessConfirmation = false;
+ this._didShowMobilePromo = false;
+ this.maybeUpdateUI(true);
+ }
+
+ async openFxASignup(window) {
+ if (!(await lazy.fxAccounts.constructor.canConnectAccount())) {
+ return;
+ }
+ const url =
+ await lazy.fxAccounts.constructor.config.promiseConnectAccountURI(
+ "fx-view"
+ );
+ this.didFxaTabOpen = true;
+ openTabInWindow(window, url, true);
+ Services.telemetry.recordEvent("firefoxview", "fxa_continue", "sync", null);
+ }
+
+ async openFxAPairDevice(window) {
+ const url = await lazy.fxAccounts.constructor.config.promisePairingURI({
+ entrypoint: "fx-view",
+ });
+ this.didFxaTabOpen = true;
+ openTabInWindow(window, url, true);
+ Services.telemetry.recordEvent("firefoxview", "fxa_mobile", "sync", null, {
+ has_devices: this.secondaryDeviceConnected.toString(),
+ });
+ }
+
+ syncOpenTabs(containerElem) {
+ // Flip the pref on.
+ // The observer should trigger re-evaluating state and advance to next step
+ Services.prefs.setBoolPref(SYNC_TABS_PREF, true);
+ }
+
+ async syncOnPageReload() {
+ if (lazy.UIState.isReady() && this.fxaSignedIn) {
+ this.startWaitingForTabs();
+ await this.syncTabs(true);
+ }
+ }
+
+ tryToClearError() {
+ if (lazy.UIState.isReady() && this.fxaSignedIn) {
+ this.startWaitingForTabs();
+ if (this.isPrimaryPasswordLocked) {
+ lazy.syncUtils.ensureMPUnlocked();
+ }
+ this.logger.debug("tryToClearError: triggering new tab sync");
+ this.syncTabs();
+ Services.tm.dispatchToMainThread(() => {});
+ } else {
+ this.logger.debug(
+ `tryToClearError: unable to sync, isReady: ${lazy.UIState.isReady()}, fxaSignedIn: ${
+ this.fxaSignedIn
+ }`
+ );
+ }
+ }
+ // For easy overriding in tests
+ syncTabs(force = false) {
+ return lazy.SyncedTabs.syncTabs(force);
+ }
+})();