chromium/ios/chrome/browser/shared/model/browser_state/incognito_session_tracker.mm

// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/shared/model/browser_state/incognito_session_tracker.h"

#import "base/ranges/algorithm.h"
#import "base/scoped_multi_source_observation.h"
#import "base/scoped_observation.h"
#import "ios/chrome/browser/shared/model/browser/browser.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_factory.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_observer.h"
#import "ios/chrome/browser/shared/model/profile/profile_manager_ios.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer.h"

// Observer used to track the state of an individual ChromeBrowserState
// and inform the IncognitoSessionTracker when the state of the incognito
// session for that ChromeBrowserState changes.
class IncognitoSessionTracker::Observer final : public BrowserListObserver,
                                                public WebStateListObserver {
 public:
  // Callback invoked when the presence of off-the-record tabs has changed.
  using Callback = base::RepeatingCallback<void(bool)>;

  Observer(BrowserList* list, Callback callback);
  ~Observer() final;

  // Returns whether any of the BrowserList's Browser has incognito tabs open.
  bool has_incognito_tabs() const { return has_incognito_tabs_; }

  // BrowserListObserver:
  void OnBrowserAdded(const BrowserList* list, Browser* browser) final;
  void OnBrowserRemoved(const BrowserList* list, Browser* browser) final;
  void OnBrowserListShutdown(BrowserList* list) final;

  // WebStateListObserver:
  void WebStateListDidChange(WebStateList* web_state_list,
                             const WebStateListChange& change,
                             const WebStateListStatus& status) final;
  void BatchOperationEnded(WebStateList* web_state_list) final;

 private:
  // Invoked when a potentially significant change is detected in any of
  // the observed WebStateList.
  void OnWebStateListChanged();

  // Closure invoked when the presence of off-the-record tabs has changed.
  Callback callback_;

  // Manages the observation of the BrowserList.
  base::ScopedObservation<BrowserList, BrowserListObserver>
      browser_list_observation_{this};

  // Manages the observation of all off-the-record WebStateLists.
  base::ScopedMultiSourceObservation<WebStateList, WebStateListObserver>
      web_state_list_observations_{this};

  // Whether any of the WebStateList has an off-the-record tab open.
  bool has_incognito_tabs_ = false;
};

IncognitoSessionTracker::Observer::Observer(BrowserList* browser_list,
                                            Callback callback)
    : callback_(std::move(callback)) {
  DCHECK(!callback_.is_null());
  browser_list_observation_.Observe(browser_list);

  // Observe all pre-existing off-the-record Browsers.
  const auto kIncognitoBrowserType = BrowserList::BrowserType::kIncognito;
  for (Browser* browser : browser_list->BrowsersOfType(kIncognitoBrowserType)) {
    web_state_list_observations_.AddObservation(browser->GetWebStateList());
  }

  // Check whether any of the Browsers has any open off-the-record tabs.
  OnWebStateListChanged();
}

IncognitoSessionTracker::Observer::~Observer() = default;

void IncognitoSessionTracker::Observer::OnBrowserAdded(
    const BrowserList* browser_list,
    Browser* browser) {
  // Ignore non-incognito Browsers.
  if (browser->type() != Browser::Type::kIncognito) {
    return;
  }

  WebStateList* const web_state_list = browser->GetWebStateList();
  web_state_list_observations_.AddObservation(web_state_list);

  // If the WebStateList was not empty, then it may be necessary to
  // notify the callback.
  if (!web_state_list->empty()) {
    OnWebStateListChanged();
  }
}

void IncognitoSessionTracker::Observer::OnBrowserRemoved(
    const BrowserList* browser_list,
    Browser* browser) {
  // Ignore non-incognito Browsers.
  if (browser->type() != Browser::Type::kIncognito) {
    return;
  }

  WebStateList* const web_state_list = browser->GetWebStateList();
  web_state_list_observations_.RemoveObservation(web_state_list);

  // If the WebStateList was not empty, then it may be necessary to
  // notify the callback.
  if (!web_state_list->empty()) {
    OnWebStateListChanged();
  }
}

void IncognitoSessionTracker::Observer::WebStateListDidChange(
    WebStateList* web_state_list,
    const WebStateListChange& change,
    const WebStateListStatus& status) {
  // Ignore changes during batch operations.
  if (web_state_list->IsBatchInProgress()) {
    return;
  }

  switch (change.type()) {
    // None of those events can change the number of off-the-record tabs,
    // ignore them.
    case WebStateListChange::Type::kStatusOnly:
    case WebStateListChange::Type::kMove:
    case WebStateListChange::Type::kReplace:
    case WebStateListChange::Type::kGroupCreate:
    case WebStateListChange::Type::kGroupVisualDataUpdate:
    case WebStateListChange::Type::kGroupMove:
    case WebStateListChange::Type::kGroupDelete:
      return;

    // Those events either increment or decrement the number of open
    // off-the-record tabs, so update the state.
    case WebStateListChange::Type::kDetach:
    case WebStateListChange::Type::kInsert:
      OnWebStateListChanged();
      return;
  }
}

void IncognitoSessionTracker::Observer::OnWebStateListChanged() {
  const bool has_incognito_tabs = base::ranges::any_of(
      web_state_list_observations_.sources(),
      [](WebStateList* web_state_list) { return !web_state_list->empty(); });

  if (has_incognito_tabs_ != has_incognito_tabs) {
    has_incognito_tabs_ = has_incognito_tabs;
    callback_.Run(has_incognito_tabs_);
  }
}

void IncognitoSessionTracker::Observer::BatchOperationEnded(
    WebStateList* web_state_list) {
  // Anything can change during a batch operation. Update the state.
  OnWebStateListChanged();
}

void IncognitoSessionTracker::Observer::OnBrowserListShutdown(
    BrowserList* browser_list) {
  browser_list_observation_.Reset();
}

IncognitoSessionTracker::IncognitoSessionTracker(ProfileManagerIOS* manager) {
  // ProfileManagerIOS invoke OnProfileLoaded(...) for all Profiles already
  // loaded, so there is no need to manually iterate over them.
  scoped_manager_observation_.Observe(manager);
}

IncognitoSessionTracker::~IncognitoSessionTracker() = default;

bool IncognitoSessionTracker::HasIncognitoSessionTabs() const {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return has_incognito_session_tabs_;
}

base::CallbackListSubscription IncognitoSessionTracker::RegisterCallback(
    SessionStateChangedCallback callback) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  return callbacks_.Add(std::move(callback));
}

void IncognitoSessionTracker::OnProfileManagerDestroyed(
    ProfileManagerIOS* manager) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  scoped_manager_observation_.Reset();
}

void IncognitoSessionTracker::OnProfileCreated(ProfileManagerIOS* manager,
                                               ChromeBrowserState* profile) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // The Profile is still not fully loaded, so the KeyedService cannot be
  // accessed (and it may be destroyed before the load complete). Wait until the
  // end of the initialisation before tracking its session.
}

void IncognitoSessionTracker::OnProfileLoaded(ProfileManagerIOS* manager,
                                              ChromeBrowserState* profile) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  // The Profile is fully loaded, we can access its BrowserList and register an
  // Observer. The use of `base::Unretained(this)` is safe as the
  // `IncognitoSessionTracker` owns the `Observer` and the closure cannot
  // outlive `this`.
  auto [_, inserted] = observers_.insert(std::make_pair(
      profile, std::make_unique<Observer>(
                   BrowserListFactory::GetForBrowserState(profile),
                   base::BindRepeating(
                       &IncognitoSessionTracker::OnIncognitoSessionStateChanged,
                       base::Unretained(this)))));

  DCHECK(inserted);
}

void IncognitoSessionTracker::OnIncognitoSessionStateChanged(
    bool has_incognito_tabs) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  const bool has_incognito_session_tabs =
      has_incognito_tabs ||
      base::ranges::any_of(
          observers_, &Observer::has_incognito_tabs,
          [](auto& pair) -> const Observer& { return *pair.second; });

  if (has_incognito_session_tabs_ != has_incognito_session_tabs) {
    has_incognito_session_tabs_ = has_incognito_session_tabs;
    callbacks_.Notify(has_incognito_session_tabs_);
  }
}