chromium/chrome/browser/ui/ash/shelf/browser_status_monitor.cc

// Copyright 2013 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/ui/ash/shelf/browser_status_monitor.h"

#include <memory>

#include "ash/public/cpp/shelf_types.h"
#include "base/containers/contains.h"
#include "base/debug/dump_without_crashing.h"
#include "base/memory/raw_ptr.h"
#include "chrome/browser/ui/ash/multi_user/multi_user_util.h"
#include "chrome/browser/ui/ash/shelf/app_service/app_service_app_window_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller.h"
#include "chrome/browser/ui/ash/shelf/chrome_shelf_controller_util.h"
#include "chrome/browser/ui/ash/shelf/shelf_spinner_controller.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_strip_model.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_utils.h"
#include "chrome/common/chrome_features.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"

namespace {

// Checks if a given browser is running a windowed app. It will return true for
// web apps, hosted apps, and packaged V1 apps.
bool IsAppBrowser(const Browser* browser) {
  return (browser->is_type_app() || browser->is_type_app_popup()) &&
         !web_app::GetAppIdFromApplicationName(browser->app_name()).empty();
}

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

}  // namespace

// This class monitors the WebContent of the all tab and notifies a navigation
// to the BrowserStatusMonitor.
class BrowserStatusMonitor::LocalWebContentsObserver
    : public content::WebContentsObserver {
 public:
  LocalWebContentsObserver(content::WebContents* contents,
                           BrowserStatusMonitor* monitor)
      : content::WebContentsObserver(contents), monitor_(monitor) {}

  LocalWebContentsObserver(const LocalWebContentsObserver&) = delete;
  LocalWebContentsObserver& operator=(const LocalWebContentsObserver&) = delete;

  ~LocalWebContentsObserver() override = default;

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

 private:
  raw_ptr<BrowserStatusMonitor> monitor_;
};

BrowserStatusMonitor::BrowserStatusMonitor(
    ChromeShelfController* shelf_controller)
    : shelf_controller_(shelf_controller),
      browser_tab_strip_tracker_(this, nullptr) {
  DCHECK(shelf_controller_);

  app_service_instance_helper_ =
      shelf_controller->app_service_app_window_controller()
          ->app_service_instance_helper();
  DCHECK(app_service_instance_helper_);
}

BrowserStatusMonitor::~BrowserStatusMonitor() {
  DCHECK(initialized_);

  BrowserList::RemoveObserver(this);

  // Simulate OnBrowserRemoved() for all Browsers.
  for (Browser* browser : *BrowserList::GetInstance()) {
    OnBrowserRemoved(browser);
  }
}

void BrowserStatusMonitor::Initialize() {
  DCHECK(!initialized_);
  initialized_ = true;

  // Simulate OnBrowserAdded() for all existing Browsers.
  for (Browser* browser : *BrowserList::GetInstance()) {
    OnBrowserAdded(browser);
  }

  // BrowserList::AddObserver() comes before BrowserTabStripTracker::Init() to
  // ensure that OnBrowserAdded() is always invoked before
  // OnTabStripModelChanged() is invoked to describe the initial state of the
  // Browser.
  BrowserList::AddObserver(this);
  browser_tab_strip_tracker_.Init();
}

void BrowserStatusMonitor::ActiveUserChanged(const std::string& user_email) {
  if (web_app::IsWebAppsCrosapiEnabled()) {
    return;
  }
  // When the active profile changes, all windowed and tabbed apps owned by the
  // newly selected profile are added to the shelf, and the ones owned by other
  // profiles are removed.
  for (Browser* browser : *BrowserList::GetInstance()) {
    bool owned = multi_user_util::IsProfileFromActiveUser(browser->profile());
    TabStripModel* tab_strip_model = browser->tab_strip_model();

    if (browser->is_type_app() || browser->is_type_app_popup()) {
      // Add windowed apps owned by the current profile, and remove the one
      // owned by other profiles.
      bool app_in_shelf = IsAppBrowserInShelf(browser);
      content::WebContents* active_web_contents =
          tab_strip_model->GetActiveWebContents();

      if (owned && !app_in_shelf) {
        // Adding an app to the shelf consists of two actions: add the browser
        // (shelf item) and add the content (shelf item status).
        AddAppBrowserToShelf(browser);
        if (active_web_contents) {
          shelf_controller_->UpdateAppState(active_web_contents,
                                            false /*remove*/);
        }
      } else if (!owned && app_in_shelf) {
        // Removing an app from the shelf requires to remove the content and
        // the shelf item (reverse order of addition).
        if (active_web_contents) {
          shelf_controller_->UpdateAppState(active_web_contents,
                                            true /*remove*/);
        }
        RemoveAppBrowserFromShelf(browser);
      }

    } else if (browser->is_type_normal()) {
      // Add tabbed apps owned by the current profile, and remove the ones owned
      // by other profiles.
      for (int i = 0; i < tab_strip_model->count(); ++i) {
        shelf_controller_->UpdateAppState(tab_strip_model->GetWebContentsAt(i),
                                          !owned /*remove*/);
      }
    }
  }

  // Update the browser state since some of the additions/removals above might
  // have had an impact on the browser item state.
  UpdateBrowserItemState();
}

void BrowserStatusMonitor::UpdateAppItemState(content::WebContents* contents,
                                              bool remove) {
  DCHECK(!web_app::IsWebAppsCrosapiEnabled());
  DCHECK(contents);
  DCHECK(initialized_);
  // It is possible to come here from Browser::SwapTabContent where the contents
  // cannot be associated with a browser. A removal however should be properly
  // processed.
  Browser* browser = chrome::FindBrowserWithTab(contents);
  if (remove || (browser && multi_user_util::IsProfileFromActiveUser(
                                browser->profile()))) {
    shelf_controller_->UpdateAppState(contents, remove);
  }
}

void BrowserStatusMonitor::UpdateBrowserItemState() {
  DCHECK(!web_app::IsWebAppsCrosapiEnabled());
  DCHECK(initialized_);
  shelf_controller_->UpdateBrowserItemState();
}

void BrowserStatusMonitor::OnBrowserAdded(Browser* browser) {
  DCHECK(initialized_);
#if DCHECK_IS_ON()
  auto insert_result = known_browsers_.insert(browser);
  DCHECK(insert_result.second);
#endif

  if (!web_app::IsWebAppsCrosapiEnabled()) {
    if (IsAppBrowser(browser) &&
        multi_user_util::IsProfileFromActiveUser(browser->profile())) {
      AddAppBrowserToShelf(browser);
    }
  }
}

void BrowserStatusMonitor::OnBrowserRemoved(Browser* browser) {
  DCHECK(initialized_);
#if DCHECK_IS_ON()
  size_t num_removed = known_browsers_.erase(browser);
  DCHECK_EQ(num_removed, 1U);
#endif

  if (!web_app::IsWebAppsCrosapiEnabled()) {
    if (IsAppBrowser(browser) &&
        multi_user_util::IsProfileFromActiveUser(browser->profile())) {
      RemoveAppBrowserFromShelf(browser);
    }

    UpdateBrowserItemState();
  }
  if (app_service_instance_helper_)
    app_service_instance_helper_->OnBrowserRemoved();
}

void BrowserStatusMonitor::OnTabStripModelChanged(
    TabStripModel* tab_strip_model,
    const TabStripModelChange& change,
    const TabStripSelectionChange& selection) {
  // OnBrowserAdded() must be invoked before OnTabStripModelChanged(). See
  // comment in constructor.
  Browser* browser = GetBrowserWithTabStripModel(tab_strip_model);
#if DCHECK_IS_ON()
  DCHECK(base::Contains(known_browsers_, browser));
#endif

  if (change.type() == TabStripModelChange::kInserted) {
    for (const auto& contents : change.GetInsert()->contents) {
      if (base::Contains(webcontents_to_observer_map_, contents.contents)) {
#if DCHECK_IS_ON()
        {
          // The tab must be in the set of tabs in transit.
          size_t num_removed = tabs_in_transit_.erase(contents.contents.get());
          DCHECK_EQ(num_removed, 1u);
        }
#endif
        OnTabMoved(tab_strip_model, contents.contents);
      } else {
#if DCHECK_IS_ON()
        DCHECK(!base::Contains(tabs_in_transit_, contents.contents));
#endif
        OnTabInserted(tab_strip_model, contents.contents);
      }
    }
    if (!web_app::IsWebAppsCrosapiEnabled()) {
      UpdateBrowserItemState();
    }
  } else if (change.type() == TabStripModelChange::kRemoved) {
    auto* remove = change.GetRemove();
    for (const auto& contents : remove->contents) {
      switch (contents.remove_reason) {
        case TabStripModelChange::RemoveReason::kDeleted:
#if DCHECK_IS_ON()
          DCHECK(!base::Contains(tabs_in_transit_, contents.contents));
#endif
          OnTabClosing(contents.contents);
          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.
            OnTabClosing(contents.contents);
          } else {
#if DCHECK_IS_ON()
            // The tab must not be already in the set of tabs in transit.
            DCHECK(tabs_in_transit_.insert(contents.contents).second);
#endif
          }
          break;
      }
    }
  } else if (change.type() == TabStripModelChange::kReplaced) {
    auto* replace = change.GetReplace();
    OnTabReplaced(tab_strip_model, replace->old_contents,
                  replace->new_contents);
  }

  if (tab_strip_model->empty())
    return;

  if (selection.active_tab_changed())
    OnActiveTabChanged(selection.old_contents, selection.new_contents);
}

void BrowserStatusMonitor::AddAppBrowserToShelf(Browser* browser) {
  DCHECK(!web_app::IsWebAppsCrosapiEnabled());
  DCHECK(IsAppBrowser(browser));
  DCHECK(initialized_);

  std::string app_id =
      web_app::GetAppIdFromApplicationName(browser->app_name());
  DCHECK(!app_id.empty());
  if (!IsAppBrowserInShelfWithAppId(app_id)) {
    if (auto* chrome_controller = ChromeShelfController::instance()) {
      chrome_controller->GetShelfSpinnerController()->CloseSpinner(app_id);
    }
    shelf_controller_->SetAppStatus(app_id, ash::STATUS_RUNNING);
  }
  browser_to_app_id_map_[browser] = app_id;
}

void BrowserStatusMonitor::RemoveAppBrowserFromShelf(Browser* browser) {
  DCHECK(!web_app::IsWebAppsCrosapiEnabled());
  DCHECK(IsAppBrowser(browser));
  DCHECK(initialized_);

  auto iter = browser_to_app_id_map_.find(browser);
  if (iter != browser_to_app_id_map_.end()) {
    std::string app_id = iter->second;
    browser_to_app_id_map_.erase(iter);
    if (!IsAppBrowserInShelfWithAppId(app_id))
      shelf_controller_->SetAppStatus(app_id, ash::STATUS_CLOSED);
  }
}

bool BrowserStatusMonitor::IsAppBrowserInShelf(Browser* browser) {
  DCHECK(!web_app::IsWebAppsCrosapiEnabled());
  return browser_to_app_id_map_.find(browser) != browser_to_app_id_map_.end();
}

bool BrowserStatusMonitor::IsAppBrowserInShelfWithAppId(
    const std::string& app_id) {
  DCHECK(!web_app::IsWebAppsCrosapiEnabled());
  for (const auto& iter : browser_to_app_id_map_) {
    if (iter.second == app_id)
      return true;
  }
  return false;
}

void BrowserStatusMonitor::OnActiveTabChanged(
    content::WebContents* old_contents,
    content::WebContents* new_contents) {
  if (!web_app::IsWebAppsCrosapiEnabled()) {
    Browser* browser = nullptr;
    // Use |new_contents|. |old_contents| could be nullptr.
    DCHECK(new_contents);
    browser = chrome::FindBrowserWithTab(new_contents);

    // Update immediately on a tab change.
    if (old_contents &&
        (TabStripModel::kNoTab !=
         browser->tab_strip_model()->GetIndexOfWebContents(old_contents))) {
      UpdateAppItemState(old_contents, false /*remove*/);
    }

    if (new_contents) {
      UpdateAppItemState(new_contents, false /*remove*/);
      UpdateBrowserItemState();
      SetShelfIDForBrowserWindowContents(browser, new_contents);
    }
  }

  if (app_service_instance_helper_) {
    app_service_instance_helper_->OnActiveTabChanged(old_contents,
                                                     new_contents);
  }
}

void BrowserStatusMonitor::OnTabReplaced(TabStripModel* tab_strip_model,
                                         content::WebContents* old_contents,
                                         content::WebContents* new_contents) {
  if (!web_app::IsWebAppsCrosapiEnabled()) {
    DCHECK(old_contents && new_contents);
    Browser* browser = chrome::FindBrowserWithTab(new_contents);

    UpdateAppItemState(old_contents, true /*remove*/);
    RemoveWebContentsObserver(old_contents);

    UpdateAppItemState(new_contents, false /*remove*/);
    UpdateBrowserItemState();

    if (browser && IsAppBrowserInShelf(browser) &&
        multi_user_util::IsProfileFromActiveUser(browser->profile())) {
      shelf_controller_->SetAppStatus(
          web_app::GetAppIdFromApplicationName(browser->app_name()),
          ash::STATUS_RUNNING);
    }

    if (tab_strip_model->GetActiveWebContents() == new_contents)
      SetShelfIDForBrowserWindowContents(browser, new_contents);

    AddWebContentsObserver(new_contents);
  }

  if (app_service_instance_helper_)
    app_service_instance_helper_->OnTabReplaced(old_contents, new_contents);
}

void BrowserStatusMonitor::OnTabInserted(TabStripModel* tab_strip_model,
                                         content::WebContents* contents) {
  if (!web_app::IsWebAppsCrosapiEnabled()) {
    UpdateAppItemState(contents, false /*remove*/);
    // If the visible navigation entry is the initial entry, wait until a
    // navigation status changes before setting the browser window Shelf ID
    // (done by the web contents observer added by AddWebContentsObserver()).
    if (tab_strip_model->GetActiveWebContents() == contents &&
        !contents->GetController().GetVisibleEntry()->IsInitialEntry()) {
      Browser* browser = chrome::FindBrowserWithTab(contents);
      SetShelfIDForBrowserWindowContents(browser, contents);
    }

    AddWebContentsObserver(contents);
  }
  if (app_service_instance_helper_)
    app_service_instance_helper_->OnTabInserted(contents);
}

void BrowserStatusMonitor::OnTabClosing(content::WebContents* contents) {
  if (!web_app::IsWebAppsCrosapiEnabled()) {
    UpdateAppItemState(contents, true /*remove*/);
    RemoveWebContentsObserver(contents);
  }
  if (app_service_instance_helper_)
    app_service_instance_helper_->OnTabClosing(contents);
}

void BrowserStatusMonitor::OnTabMoved(TabStripModel* tab_strip_model,
                                      content::WebContents* contents) {
  // TODO(crbug.com/40763808): split this into inserted and moved cases.
  OnTabInserted(tab_strip_model, contents);
}

void BrowserStatusMonitor::OnTabNavigationFinished(
    content::WebContents* contents) {
  if (web_app::IsWebAppsCrosapiEnabled()) {
    return;
  }
  UpdateAppItemState(contents, false /*remove*/);
  UpdateBrowserItemState();

  // Navigating may change the ShelfID associated with the WebContents.
  Browser* browser = chrome::FindBrowserWithTab(contents);
  if (browser &&
      browser->tab_strip_model()->GetActiveWebContents() == contents) {
    SetShelfIDForBrowserWindowContents(browser, contents);
  }
}

void BrowserStatusMonitor::AddWebContentsObserver(
    content::WebContents* contents) {
  if (webcontents_to_observer_map_.find(contents) ==
      webcontents_to_observer_map_.end()) {
    webcontents_to_observer_map_[contents] =
        std::make_unique<LocalWebContentsObserver>(contents, this);
  }
}

void BrowserStatusMonitor::RemoveWebContentsObserver(
    content::WebContents* contents) {
  DCHECK(webcontents_to_observer_map_.find(contents) !=
         webcontents_to_observer_map_.end());
  webcontents_to_observer_map_.erase(contents);
}

void BrowserStatusMonitor::SetShelfIDForBrowserWindowContents(
    Browser* browser,
    content::WebContents* web_contents) {
  if (!web_app::IsWebAppsCrosapiEnabled()) {
    shelf_controller_->SetShelfIDForBrowserWindowContents(browser,
                                                          web_contents);
  }

  if (app_service_instance_helper_) {
    app_service_instance_helper_->OnSetShelfIDForBrowserWindowContents(
        web_contents);
  }
}