| // Copyright 2013 The Chromium Authors. All rights reserved. |
| // 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/macros.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_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 (auto* 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 DidFinishNavigation( |
| content::NavigationHandle* navigation_handle) override { |
| // TODO(https://crbug.com/1218946): With MPArch there may be multiple main |
| // frames. This caller was converted automatically to the primary main frame |
| // to preserve its semantics. Follow up to confirm correctness. |
| if (navigation_handle->IsInPrimaryMainFrame() && |
| navigation_handle->HasCommitted()) { |
| monitor_->OnTabNavigationFinished(web_contents()); |
| } |
| } |
| |
| private: |
| 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 (auto* browser : *BrowserList::GetInstance()) |
| OnBrowserRemoved(browser); |
| } |
| |
| void BrowserStatusMonitor::Initialize() { |
| DCHECK(!initialized_); |
| initialized_ = true; |
| |
| // Simulate OnBrowserAdded() for all existing Browsers. |
| for (auto* 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::FindBrowserWithWebContents(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); |
| DCHECK_EQ(num_removed, 1); |
| } |
| #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: |
| case TabStripModelChange::RemoveReason::kCached: |
| #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/1221967): 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::FindBrowserWithWebContents(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::FindBrowserWithWebContents(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 contents does not have a visible navigation 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()) { |
| Browser* browser = chrome::FindBrowserWithWebContents(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/1203992): 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::FindBrowserWithWebContents(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); |
| } |
| } |