chromium/chrome/browser/apps/browser_instance/browser_app_instance_tracker.cc

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

#include "chrome/browser/apps/browser_instance/browser_app_instance_tracker.h"

#include <utility>

#include "base/containers/contains.h"
#include "base/memory/raw_ptr.h"
#include "base/process/process.h"
#include "base/strings/utf_string_conversions.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/apps/browser_instance/browser_app_instance_map.h"
#include "chrome/browser/apps/browser_instance/browser_app_instance_observer.h"
#include "chrome/browser/apps/browser_instance/web_contents_instance_id_utils.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "components/services/app_service/public/cpp/types_util.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/extension.h"
#include "ui/aura/window.h"

#if BUILDFLAG(IS_CHROMEOS_LACROS)
#include "chrome/browser/lacros/lacros_extensions_util.h"
#include "chrome/browser/lacros/profile_util.h"
#include "ui/views/widget/desktop_aura/desktop_window_tree_host_lacros.h"
#else
#include "ui/wm/core/window_util.h"
#endif

namespace apps {

namespace {

#if BUILDFLAG(IS_CHROMEOS_LACROS)
bool HaveSameWindowTreeHostLacros(aura::Window* window1,
                                  aura::Window* window2) {
  if (window1 == nullptr || window2 == nullptr) {
    return false;
  }
  views::DesktopWindowTreeHostPlatform* host1 =
      views::DesktopWindowTreeHostLacros::From(window1->GetHost());
  views::DesktopWindowTreeHostPlatform* host2 =
      views::DesktopWindowTreeHostLacros::From(window2->GetHost());

  if (host1 == nullptr || host2 == nullptr) {
    return false;
  } else {
    // If the host is a window_tree_host for bubble, the associated browser is
    // up in the window_parent() chain.
    while (host1->window_parent()) {
      host1 = host1->window_parent();
    }
    while (host2->window_parent()) {
      host2 = host2->window_parent();
    }
    return host1 == host2;
  }
}
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

Browser* GetBrowserWithTabStripModel(TabStripModel* tab_strip_model) {
  for (Browser* browser : *BrowserList::GetInstance()) {
    if (browser->tab_strip_model() == tab_strip_model) {
      return browser;
    }
  }
  return nullptr;
}

#if BUILDFLAG(IS_CHROMEOS_LACROS)
Browser* GetBrowserWithAuraWindow(aura::Window* aura_window) {
  for (Browser* browser : *BrowserList::GetInstance()) {
    BrowserWindow* window = browser->window();
    if (window && window->GetNativeWindow() == aura_window) {
      return browser;
    }
    if (HaveSameWindowTreeHostLacros(window->GetNativeWindow(), aura_window)) {
      return browser;
    }
  }
  return nullptr;
}
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

aura::Window* AuraWindowForBrowser(Browser* browser) {
  BrowserWindow* window = browser->window();
  DCHECK(window && window->GetNativeWindow());
  aura::Window* aura_window = window->GetNativeWindow();
  DCHECK(aura_window);
  return aura_window;
}

#if BUILDFLAG(IS_CHROMEOS_LACROS)
wm::ActivationClient* ActivationClientForBrowser(Browser* browser) {
  aura::Window* window = AuraWindowForBrowser(browser)->GetRootWindow();
  wm::ActivationClient* client = wm::GetActivationClient(window);
  DCHECK(client);
  return client;
}
#endif  // BUILDFLAG(IS_CHROMEOS_LACROS)

bool IsBrowserActive(Browser* browser) {
  auto* aura_window = AuraWindowForBrowser(browser);
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  auto* activation_client = ActivationClientForBrowser(browser);
  return HaveSameWindowTreeHostLacros(aura_window,
                                      activation_client->GetActiveWindow());
#else
  return wm::IsActiveWindow(aura_window);
#endif
}

bool IsWebContentsActive(Browser* browser, content::WebContents* contents) {
  return browser->tab_strip_model()->GetActiveWebContents() == contents;
}

std::string GetAppIdForTab(content::WebContents* contents, Profile* profile) {
  return GetInstanceAppIdForWebContents(contents).value_or("");
}

std::string GetAppIdForBrowser(Browser* browser) {
  std::string app_id =
      web_app::GetAppIdFromApplicationName(browser->app_name());
  auto* registry = extensions::ExtensionRegistry::Get(browser->profile());
  auto* extension = registry->GetInstalledExtension(app_id);
  // This is a web-app.
  if (!extension) {
    return app_id;
  }

  if (extension->is_hosted_app() || extension->is_legacy_packaged_app()) {
    return app_id;
  }

  return "";
}

std::string GetTitle(content::WebContents* contents) {
  return base::UTF16ToUTF8(contents->GetTitle());
}

std::string GetTitle(Browser* browser) {
  content::WebContents* active_contents =
      browser->tab_strip_model()->GetActiveWebContents();
  return active_contents ? base::UTF16ToUTF8(active_contents->GetTitle()) : "";
}

bool IsExtensionNonAppWindow(Browser* browser) {
  return browser->is_type_app_popup() && GetAppIdForBrowser(browser) == "";
}

bool IsAppWindow(Browser* browser) {
  return (browser->is_type_app() || browser->is_type_app_popup()) &&
         GetAppIdForBrowser(browser) != "";
}

bool IsBrowserWindow(Browser* browser) {
  return browser->is_type_normal() || browser->is_type_popup() ||
         browser->is_type_devtools() || IsExtensionNonAppWindow(browser);
}

}  // namespace

// Helper class to notify BrowserAppInstanceTracker when WebContents navigation
// finishes.
class BrowserAppInstanceTracker::WebContentsObserver
    : public content::WebContentsObserver {
 public:
  explicit WebContentsObserver(content::WebContents* contents,
                               BrowserAppInstanceTracker* owner)
      : content::WebContentsObserver(contents), owner_(owner) {}
  WebContentsObserver(const WebContentsObserver&) = delete;
  WebContentsObserver& operator=(const WebContentsObserver&) = delete;
  ~WebContentsObserver() override = default;

  // content::WebContentsObserver
  void PrimaryPageChanged(content::Page& page) override {
    owner_->OnWebContentsUpdated(web_contents());
  }

  void TitleWasSet(content::NavigationEntry* entry) override {
    if (entry) {
      owner_->OnWebContentsUpdated(web_contents());
    }
  }

 private:
  const raw_ptr<BrowserAppInstanceTracker> owner_;
};

BrowserAppInstanceTracker::BrowserAppInstanceTracker(
    Profile* profile,
    AppRegistryCache& app_registry_cache)
    : profile_(profile), browser_tab_strip_tracker_(this, this) {
  BrowserList::GetInstance()->AddObserver(this);
  browser_tab_strip_tracker_.Init();
  app_registry_cache_observer_.Observe(&app_registry_cache);
}

BrowserAppInstanceTracker::~BrowserAppInstanceTracker() {
  BrowserList::GetInstance()->RemoveObserver(this);
  DCHECK(tracked_browsers_.empty());
  DCHECK(observers_.empty());
}

const BrowserAppInstance* BrowserAppInstanceTracker::GetAppInstance(
    content::WebContents* contents) const {
  // Try get the app tab instance first, if exists.
  const BrowserAppInstance* instance =
      GetInstance(app_tab_instances_, contents);
  if (instance) {
    return instance;
  }
  // Then app window instance, which should be at most one per WebContents,
  // although multiple WebContents can map to a single app window instance, in
  // case of app windows with tab strips.
  Browser* browser = chrome::FindBrowserWithTab(contents);
  if (!browser) {
    return nullptr;
  }
  return GetAppInstance(browser);
}

const BrowserAppInstance* BrowserAppInstanceTracker::GetAppInstance(
    Browser* browser) const {
  return GetInstance(app_window_instances_, browser);
}

const BrowserWindowInstance*
BrowserAppInstanceTracker::GetBrowserWindowInstance(Browser* browser) const {
  return GetInstance(window_instances_, browser);
}

void BrowserAppInstanceTracker::ActivateTabInstance(base::UnguessableToken id) {
  for (const auto& pair : app_tab_instances_) {
    const BrowserAppInstance& instance = *pair.second;
    if (instance.id == id) {
      Browser* browser = chrome::FindBrowserWithTab(pair.first);
      TabStripModel* tab_strip = browser->tab_strip_model();
      int index = tab_strip->GetIndexOfWebContents(pair.first);
      DCHECK_NE(TabStripModel::kNoTab, index);
      tab_strip->ActivateTabAt(index);
      break;
    }
  }
}

void BrowserAppInstanceTracker::StopInstancesOfApp(const std::string& app_id) {
  // Handle app tabs.
  std::vector<content::WebContents*> web_contents_to_close;
  for (const auto& pair : app_tab_instances_) {
    if (pair.second->app_id == app_id) {
      web_contents_to_close.push_back(pair.first);
    }
  }
  for (content::WebContents* web_contents : web_contents_to_close) {
    Browser* browser = chrome::FindBrowserWithTab(web_contents);
    if (!browser) {
      continue;
    }
    int index = browser->tab_strip_model()->GetIndexOfWebContents(web_contents);
    DCHECK(index != TabStripModel::kNoTab);
    browser->tab_strip_model()->CloseWebContentsAt(index,
                                                   TabCloseTypes::CLOSE_NONE);
  }

  // Handle app windows.
  std::vector<Browser*> browsers_to_close;
  for (const auto& pair : app_window_instances_) {
    if (pair.second->app_id == app_id) {
      browsers_to_close.push_back(pair.first);
    }
  }
  for (Browser* browser : browsers_to_close) {
    browser->tab_strip_model()->CloseAllTabs();
  }
}

void BrowserAppInstanceTracker::OnTabStripModelChanged(
    TabStripModel* tab_strip_model,
    const TabStripModelChange& change,
    const TabStripSelectionChange& selection) {
  Browser* browser = GetBrowserWithTabStripModel(tab_strip_model);
  DCHECK(browser);

  switch (change.type()) {
    case TabStripModelChange::kInserted:
      OnTabStripModelChangeInsert(browser, *change.GetInsert(), selection);
      break;
    case TabStripModelChange::kRemoved:
      OnTabStripModelChangeRemove(browser, *change.GetRemove(), selection);
      break;
    case TabStripModelChange::kReplaced:
      OnTabStripModelChangeReplace(browser, *change.GetReplace());
      break;
    case TabStripModelChange::kMoved:
      // Ignored.
      break;
    case TabStripModelChange::kSelectionOnly:
      OnTabStripModelChangeSelection(browser, selection);
      break;
  }
}

bool BrowserAppInstanceTracker::ShouldTrackBrowser(Browser* browser) {
  return profile_->IsSameOrParent(browser->profile());
}

void BrowserAppInstanceTracker::OnBrowserAdded(Browser* browser) {
  DCHECK(!base::Contains(tracked_browsers_, browser));
}

void BrowserAppInstanceTracker::OnBrowserRemoved(Browser* browser) {
  DCHECK(!base::Contains(tracked_browsers_, browser));
}

void BrowserAppInstanceTracker::OnAppUpdate(const AppUpdate& update) {
  if (!apps_util::AppTypeUsesWebContents(update.AppType())) {
    return;
  }
  // Sync app instances for existing tabs.
  // Iterate over the full list of browsers instead of tracked_browsers_ in case
  // tracked_browsers_ is out of date with global state.
  for (Browser* browser : *BrowserList::GetInstance()) {
    if (!IsBrowserTracked(browser)) {
      continue;
    }
    TabStripModel* tab_strip_model = browser->tab_strip_model();
    for (int i = 0; i < tab_strip_model->count(); ++i) {
      content::WebContents* contents = tab_strip_model->GetWebContentsAt(i);
      OnTabUpdated(browser, contents);
    }
  }
}

void BrowserAppInstanceTracker::OnAppRegistryCacheWillBeDestroyed(
    AppRegistryCache* cache) {
  app_registry_cache_observer_.Reset();
}

void BrowserAppInstanceTracker::RemoveBrowserForTesting(Browser* browser) {
  tracked_browsers_.erase(browser);
  app_window_instances_.erase(browser);
  window_instances_.erase(browser);
}

void BrowserAppInstanceTracker::OnTabStripModelChangeInsert(
    Browser* browser,
    const TabStripModelChange::Insert& insert,
    const TabStripSelectionChange& selection) {
  if (selection.old_contents) {
    // A tab got deactivated on insertion.
    OnTabUpdated(browser, selection.old_contents);
  }
  if (insert.contents.size() == 0) {
    return;
  }
  // First tab attached.
  if (browser->tab_strip_model()->count() ==
      static_cast<int>(insert.contents.size())) {
    OnBrowserFirstTabAttached(browser);
  }
  for (const auto& inserted_tab : insert.contents) {
    content::WebContents* contents = inserted_tab.contents;
    bool tab_is_new = !base::Contains(webcontents_to_observer_map_, contents);
#if DCHECK_IS_ON()
    if (tab_is_new) {
      DCHECK(!base::Contains(tabs_in_transit_, contents));
    } else {
      // The tab must be in the set of tabs in transit.
      DCHECK(tabs_in_transit_.erase(contents) == 1);
    }
#endif
    if (tab_is_new) {
      OnTabCreated(browser, contents);
    }
    OnTabAttached(browser, contents);
  }
}

void BrowserAppInstanceTracker::OnTabStripModelChangeRemove(
    Browser* browser,
    const TabStripModelChange::Remove& remove,
    const TabStripSelectionChange& selection) {
  for (const auto& removed_tab : remove.contents) {
    content::WebContents* contents = removed_tab.contents;
    bool tab_will_be_closed = false;
    switch (removed_tab.remove_reason) {
      case TabStripModelChange::RemoveReason::kDeleted:
#if DCHECK_IS_ON()
        DCHECK(!base::Contains(tabs_in_transit_, contents));
#endif
        tab_will_be_closed = true;
        break;
      case TabStripModelChange::RemoveReason::kInsertedIntoOtherTabStrip:
        // The tab will be reinserted immediately into another browser, so
        // this event is ignored.
        if (browser->is_type_devtools()) {
          // TODO(crbug.com/40773744): when a dev tools window is docked, and
          // its WebContents is removed, it will not be reinserted into
          // another tab strip, so it should be treated as closed.
          tab_will_be_closed = true;
        } else {
          // The tab must not be already in the set of tabs in transit.
#if DCHECK_IS_ON()
          DCHECK(tabs_in_transit_.insert(contents).second);
#endif
        }
        break;
    }
    if (tab_will_be_closed) {
      OnTabClosing(browser, contents);
    }
  }
  // Last tab detached.
  if (browser->tab_strip_model()->count() == 0) {
    OnBrowserLastTabDetached(browser);
  }
  if (selection.new_contents) {
    // A tab got activated on removal.
    OnTabUpdated(browser, selection.new_contents);
  }
}

void BrowserAppInstanceTracker::OnTabStripModelChangeReplace(
    Browser* browser,
    const TabStripModelChange::Replace& replace) {
  // Simulate closing the old tab and opening a new tab.
  OnTabClosing(browser, replace.old_contents);
  OnTabCreated(browser, replace.new_contents);
  OnTabAttached(browser, replace.new_contents);
}

void BrowserAppInstanceTracker::OnTabStripModelChangeSelection(
    Browser* browser,
    const TabStripSelectionChange& selection) {
  if (!selection.active_tab_changed()) {
    return;
  }
  if (selection.new_contents) {
    OnTabUpdated(browser, selection.new_contents);
  }
  if (selection.old_contents) {
    OnTabUpdated(browser, selection.old_contents);
  }
}

void BrowserAppInstanceTracker::OnBrowserFirstTabAttached(Browser* browser) {
  tracked_browsers_.insert(browser);
  if (IsBrowserWindow(browser)) {
    CreateBrowserWindowInstance(browser);
  } else if (IsAppWindow(browser)) {
    // All tabs in the app window will map to the same app ID
    std::string app_id = GetAppIdForBrowser(browser);
    CreateAppWindowInstance(std::move(app_id), browser);
  }
}

void BrowserAppInstanceTracker::OnBrowserLastTabDetached(Browser* browser) {
  if (IsBrowserWindow(browser)) {
    RemoveBrowserWindowInstanceIfExists(browser);
  } else if (IsAppWindow(browser)) {
    RemoveAppWindowInstanceIfExists(browser);
  }
  tracked_browsers_.erase(browser);
}

void BrowserAppInstanceTracker::OnTabCreated(Browser* browser,
                                             content::WebContents* contents) {
  webcontents_to_observer_map_[contents] =
      std::make_unique<BrowserAppInstanceTracker::WebContentsObserver>(contents,
                                                                       this);

  if (IsAppWindow(browser)) {
    return;
  }

  std::string app_id = GetAppIdForTab(contents, profile_);
  if (!app_id.empty()) {
    CreateAppTabInstance(std::move(app_id), browser, contents);
  }
}

void BrowserAppInstanceTracker::OnTabAttached(Browser* browser,
                                              content::WebContents* contents) {
  OnTabUpdated(browser, contents);
}

void BrowserAppInstanceTracker::OnTabUpdated(Browser* browser,
                                             content::WebContents* contents) {
  if (IsAppWindow(browser)) {
    BrowserAppInstance* instance = GetInstance(app_window_instances_, browser);
    DCHECK(instance);
    MaybeUpdateAppWindowInstance(*instance, browser);
    return;
  }

  // Handle app tabs.
  std::string new_app_id = GetAppIdForTab(contents, profile_);
  BrowserAppInstance* instance = GetInstance(app_tab_instances_, contents);
  if (instance) {
    if (instance->app_id != new_app_id) {
      // If app ID changed on navigation, remove the old app.
      RemoveAppTabInstanceIfExists(contents);
      // Add the new app instance, if navigated to another app.
      if (!new_app_id.empty()) {
        CreateAppTabInstance(std::move(new_app_id), browser, contents);
      }
    } else {
      // App ID did not change, but other attributes may have.
      MaybeUpdateAppTabInstance(*instance, browser, contents);
    }
  } else if (!new_app_id.empty()) {
    // Tab previously had no app ID, but navigated to a URL that does.
    CreateAppTabInstance(std::move(new_app_id), browser, contents);
  } else {
    // Tab without an app has changed, we don't care about it.
  }
}

void BrowserAppInstanceTracker::OnTabClosing(Browser* browser,
                                             content::WebContents* contents) {
  RemoveAppTabInstanceIfExists(contents);
  DCHECK(base::Contains(webcontents_to_observer_map_, contents));
  webcontents_to_observer_map_.erase(contents);
}

void BrowserAppInstanceTracker::OnWebContentsUpdated(
    content::WebContents* contents) {
  Browser* browser = chrome::FindBrowserWithTab(contents);
  if (browser) {
    OnTabUpdated(browser, contents);
  }
}

void BrowserAppInstanceTracker::CreateAppTabInstance(
    std::string app_id,
    Browser* browser,
    content::WebContents* contents) {
  auto new_instance = std::make_unique<BrowserAppInstance>(
      GenerateId(), BrowserAppInstance::Type::kAppTab, std::move(app_id),
      browser->window()->GetNativeWindow(), GetTitle(contents),
      IsBrowserActive(browser), IsWebContentsActive(browser, contents),
      browser->session_id().id(), browser->create_params().restore_id);
  auto& instance =
      AddInstance(app_tab_instances_, contents, std::move(new_instance));
  for (auto& observer : observers_) {
    observer.OnBrowserAppAdded(instance);
  }
}

void BrowserAppInstanceTracker::MaybeUpdateAppTabInstance(
    BrowserAppInstance& instance,
    Browser* browser,
    content::WebContents* contents) {
  if (instance.MaybeUpdate(
          browser->window()->GetNativeWindow(), GetTitle(contents),
          IsBrowserActive(browser), IsWebContentsActive(browser, contents),
          browser->session_id().id(), browser->create_params().restore_id)) {
    for (auto& observer : observers_) {
      observer.OnBrowserAppUpdated(instance);
    }
  }
}

void BrowserAppInstanceTracker::RemoveAppTabInstanceIfExists(
    content::WebContents* contents) {
  auto instance = PopInstanceIfExists(app_tab_instances_, contents);
  if (instance) {
    for (auto& observer : observers_) {
      observer.OnBrowserAppRemoved(*instance);
    }
  }
}

void BrowserAppInstanceTracker::CreateAppWindowInstance(std::string app_id,
                                                        Browser* browser) {
  auto new_instance = std::make_unique<BrowserAppInstance>(
      GenerateId(), BrowserAppInstance::Type::kAppWindow, std::move(app_id),
      browser->window()->GetNativeWindow(), GetTitle(browser),
      IsBrowserActive(browser),
      /*is_web_contents_active=*/true, browser->session_id().id(),
      browser->create_params().restore_id);
  auto& instance =
      AddInstance(app_window_instances_, browser, std::move(new_instance));
  for (auto& observer : observers_) {
    observer.OnBrowserAppAdded(instance);
  }
}

void BrowserAppInstanceTracker::MaybeUpdateAppWindowInstance(
    BrowserAppInstance& instance,
    Browser* browser) {
  if (instance.MaybeUpdate(browser->window()->GetNativeWindow(),
                           GetTitle(browser), IsBrowserActive(browser),
                           /*is_web_contents_active=*/true,
                           browser->session_id().id(),
                           browser->create_params().restore_id)) {
    for (auto& observer : observers_) {
      observer.OnBrowserAppUpdated(instance);
    }
  }
}

void BrowserAppInstanceTracker::RemoveAppWindowInstanceIfExists(
    Browser* browser) {
  auto instance = PopInstanceIfExists(app_window_instances_, browser);
  if (instance) {
    for (auto& observer : observers_) {
      observer.OnBrowserAppRemoved(*instance);
    }
  }
}

void BrowserAppInstanceTracker::CreateBrowserWindowInstance(Browser* browser) {
  uint64_t lacros_profile_id = 0;
#if BUILDFLAG(IS_CHROMEOS_LACROS)
  lacros_profile_id = HashProfilePathToProfileId(browser->profile()->GetPath());
#endif
  auto& instance = AddInstance(
      window_instances_, browser,
      std::make_unique<BrowserWindowInstance>(
          GenerateId(), browser->window()->GetNativeWindow(),
          browser->session_id().id(), browser->create_params().restore_id,
          browser->profile()->IsIncognitoProfile(), lacros_profile_id,
          IsBrowserActive(browser)));
  for (auto& observer : observers_) {
    observer.OnBrowserWindowAdded(instance);
  }
}

void BrowserAppInstanceTracker::RemoveBrowserWindowInstanceIfExists(
    Browser* browser) {
  auto instance = PopInstanceIfExists(window_instances_, browser);
  if (instance) {
    for (auto& observer : observers_) {
      observer.OnBrowserWindowRemoved(*instance);
    }
  }
}

base::UnguessableToken BrowserAppInstanceTracker::GenerateId() const {
  return base::UnguessableToken::Create();
}

bool BrowserAppInstanceTracker::IsBrowserTracked(Browser* browser) const {
  return base::Contains(tracked_browsers_, browser);
}

#if BUILDFLAG(IS_CHROMEOS_LACROS)
BrowserAppInstanceTrackerLacros::BrowserAppInstanceTrackerLacros(
    Profile* profile,
    AppRegistryCache& app_registry_cache)
    : BrowserAppInstanceTracker(profile, app_registry_cache) {}

BrowserAppInstanceTrackerLacros::~BrowserAppInstanceTrackerLacros() {
  DCHECK_EQ(activation_client_observations_.GetSourcesCount(), 0u);
}

void BrowserAppInstanceTrackerLacros::OnWindowActivated(
    ActivationReason reason,
    aura::Window* gained_active,
    aura::Window* lost_active) {
  if (Browser* browser = GetBrowserWithAuraWindow(lost_active)) {
    OnBrowserWindowUpdated(browser);
  }
  if (Browser* browser = GetBrowserWithAuraWindow(gained_active)) {
    OnBrowserWindowUpdated(browser);
  }
}

void BrowserAppInstanceTrackerLacros::OnBrowserWindowUpdated(Browser* browser) {
  // We only want to send window events for the browsers we track to avoid
  // sending window events before a "browser added" event.
  if (!IsBrowserTracked(browser)) {
    return;
  }
  BrowserWindowInstance* instance = GetInstance(window_instances_, browser);
  if (instance) {
    MaybeUpdateBrowserWindowInstance(*instance, browser);
  }

  TabStripModel* tab_strip_model = browser->tab_strip_model();
  for (int i = 0; i < tab_strip_model->count(); i++) {
    content::WebContents* contents = tab_strip_model->GetWebContentsAt(i);
    OnTabUpdated(browser, contents);
  }
}

void BrowserAppInstanceTrackerLacros::MaybeUpdateBrowserWindowInstance(
    BrowserWindowInstance& instance,
    Browser* browser) {
  if (instance.MaybeUpdate(IsBrowserActive(browser))) {
    for (auto& observer : observers_) {
      observer.OnBrowserWindowUpdated(instance);
    }
  }
}

bool BrowserAppInstanceTrackerLacros::IsActivationClientTracked(
    wm::ActivationClient* client) const {
  // Iterate over the full list of browsers instead of tracked_browsers_ in case
  // tracked_browsers_ is out of date with global state
  // TODO(crbug.com/40782702): This can be changed to iterate tracked_browsers_
  // when confident it doesn't get out of sync.
  for (Browser* browser : *BrowserList::GetInstance()) {
    if (IsBrowserTracked(browser) &&
        ActivationClientForBrowser(browser) == client) {
      return true;
    }
  }
  return false;
}

void BrowserAppInstanceTrackerLacros::OnBrowserFirstTabAttached(
    Browser* browser) {
  // Observe the activation client of the root window of
  // the browser's aura
  // window if this is the first browser matching it (there is no other tracked
  // browser matching it).
  wm::ActivationClient* activation_client = ActivationClientForBrowser(browser);
  if (!IsActivationClientTracked(activation_client)) {
    activation_client_observations_.AddObservation(activation_client);
  }
  BrowserAppInstanceTracker::OnBrowserFirstTabAttached(browser);
}

void BrowserAppInstanceTrackerLacros::OnBrowserLastTabDetached(
    Browser* browser) {
  BrowserAppInstanceTracker::OnBrowserLastTabDetached(browser);

  // Unobserve the activation client of the root window of the browser's aura
  // window if the last browser using it was just removed.
  wm::ActivationClient* activation_client = ActivationClientForBrowser(browser);
  if (!IsActivationClientTracked(activation_client)) {
    activation_client_observations_.RemoveObservation(activation_client);
  }
}
#endif  // #BUILDFLAG(IS_CHROMEOS_LACROS)

}  // namespace apps