s a
        // fragment navigation.
        this.#trace(
          lazy.truncate`Location=fragmentNavigated: ${location.spec}`,
          context.id
        );

        lazy.notifyFragmentNavigated(payload);
        return;
      }

      this.#trace(
        lazy.truncate`Location=sameDocumentChanged: ${location.spec}`,
        context.id
      );

      lazy.notifySameDocumentChanged(payload);
    }
  };

  #onStateChange = (progress, request, stateFlags, status) => {
    const context = progress.browsingContext;
    const targetURI = this.#getTargetURI(request);

    const isBindingAborted = status == Cr.NS_BINDING_ABORTED;
    const isStart = !!(stateFlags & Ci.nsIWebProgressListener.STATE_START);
    const isStop = !!(stateFlags & Ci.nsIWebProgressListener.STATE_STOP);

    if (lazy.Log.isTraceLevelOrMore) {
      const isNetwork = !!(
        stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
      );
      this.#trace(
        `Loading state: flags: ${stateFlags}, status: ${status}, ` +
          ` isStart: ${isStart}, isStop: ${isStop}, isNetwork: ${isNetwork},` +
          ` isBindingAborted: ${isBindingAborted},` +
          lazy.truncate` targetURI: ${targetURI?.spec}`,
        context.id
      );
    }

    const url = targetURI?.spec;

    const isUncommittedInitialDocument =
      lazy.isUncommittedInitialDocument(context);
    if (isUncommittedInitialDocument && url === "about:blank") {
      this.#trace("Skip initial navigation to about:blank", context.id);
      return;
    }

    try {
      if (isStart) {
        lazy.notifyNavigationStarted({
          contextDetails: { context },
          url,
        });

        return;
      }

      if (isStop && !isBindingAborted) {
        const errorName = ChromeUtils.getXPCOMErrorName(status);
        if (this.#isContentBlocked(errorName)) {
          lazy.notifyNavigationFailed({
            contextDetails: { context },
            errorName,
            status,
            url,
          });
        } else {
          lazy.notifyNavigationStopped({
            contextDetails: { context },
            status,
            url,
          });
        }
      }
    } catch (e) {
      if (e.name === "InvalidStateError") {
        // We'll arrive here if we no longer have our manager, so we can
        // just swallow this error.
        return;
      }
      throw e;
    }
  };

  startListening() {
    if (this.#listening) {
      return;
    }

    this.#contextListener.startListening();
    // Start listening for navigation on all existing contexts.
    this.#getAllBrowsingContexts().forEach(browsingContext =>
      this.#startWatchingBrowsingContextNavigation(browsingContext)
    );

    this.#listening = true;
  }

  stopListening() {
    if (!this.#listening) {
      return;
    }

    this.#contextListener.stopListening();
    for (const webProgress of this.#monitoredWebProgress.keys()) {
      try {
        webProgress.removeProgressListener(this.#listener);
      } catch (e) {
        this.#trace(`Failed to remove the progress listener`);
      }
    }
    this.#monitoredWebProgress = new Map();

    this.#listening = false;
  }

  #getAllBrowsingContexts() {
    return lazy.TabManager.getBrowsers().flatMap(browser =>
      browser.browsingContext.getAllBrowsingContextsInSubtree()
    );
  }

  #getTargetURI(request) {
    try {
      return request.QueryInterface(Ci.nsIChannel).originalURI;
    } catch (e) {}

    return null;
  }

  #isContentBlocked(blockedReason) {
    return [
      // If content is blocked with e.g. CSP meta tag.
      "NS_ERROR_CONTENT_BLOCKED",
      // If a resource load was blocked because of the CSP header.
      "NS_ERROR_CSP_FRAME_ANCESTOR_VIOLATION",
      // If a resource load was blocked because of the Cross-Origin-Embedder-Policy header.
      "NS_ERROR_DOM_COEP_FAILED",
      // If a resource load was blocked because of the X-Frame-Options header.
      "NS_ERROR_XFO_VIOLATION",
    ].includes(blockedReason);
  }

  #onContextAttached = async (eventName, data) => {
    const { browsingContext } = data;

    if (browsingContext.isContent) {
      this.#startWatchingBrowsingContextNavigation(browsingContext);
    }
  };

  #onContextDiscarded = async (eventName, data = {}) => {
    const { browsingContext } = data;

    if (browsingContext.isContent) {
      this.#stopWatchingBrowsingContextNavigation(browsingContext);
    }
  };

  #startWatchingBrowsingContextNavigation(browsingContext) {
    if (browsingContext.parent) {
      // Frame contexts will be monitored through the webprogress listener of
      // the top window.
      return;
    }

    this.#trace(
      `Start watching updates for browsing context`,
      browsingContext.id
    );

    const webProgress = browsingContext.webProgress;
    if (!webProgress) {
      this.#trace(
        `No web progress attached to this browsing context, bailing out`,
        browsingContext.id
      );
      return;
    }

    if (!this.#monitoredWebProgress.has(webProgress)) {
      this.#trace(
        `The web progress was not monitored yet, adding a progress listener`,
        browsingContext.id
      );
      this.#monitoredWebProgress.set(webProgress, new Set());
      webProgress.addProgressListener(
        this.#listener,
        Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
          Ci.nsIWebProgress.NOTIFY_LOCATION
      );
    }

    this.#monitoredWebProgress.get(webProgress).add(browsingContext);
  }

  #stopWatchingBrowsingContextNavigation(browsingContext) {
    if (browsingContext.parent) {
      // Frame contexts will be monitored through the webprogress listener of
      // the top window.
      return;
    }

    this.#trace(
      `Stop watching updates for browsing context`,
      browsingContext.id
    );

    const webProgress = browsingContext.webProgress;
    if (!webProgress) {
      this.#trace(
        `No web progress attached to this browsing context, bailing out`,
        browsingContext.id
      );
      return;
    }

    const contexts = this.#monitoredWebProgress.get(webProgress);
    if (!contexts) {
      this.#trace(
        `No browsing context tracked for the web progress, bailing out`,
        browsingContext.id
      );
      return;
    }

    contexts.delete(browsingContext);
    if (!contexts.size) {
      this.#trace(
        `All browsing contexts for this web progress deleted, removing the progress listener`,
        browsingContext.id
      );
      try {
        webProgress.removeProgressListener(this.#listener);
      } catch (e) {
        this.#trace(
          `Failed to remove the progress listener`,
          browsingContext.id
        );
      }

      this.#trace(
        `Removing the web progress from monitored web progress`,
        browsingContext.id
      );
      this.#monitoredWebProgress.delete(webProgress);
    }
  }

  #trace(message, contextId = null) {
    if (contextId !== null) {
      lazy.logger.trace(
        `${this.constructor.name} ${message} [context=${contextId}]`
      );
    } else {
      lazy.logger.trace(`${this.constructor.name} ${message}`);
    }
  }
}
PK