// Copyright 2019 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/app_service/app_service_instance_registry_helper.h"
#include <set>
#include <string>
#include <vector>
#include "base/containers/contains.h"
#include "base/stl_util.h"
#include "base/time/time.h"
#include "chrome/browser/apps/app_service/app_service_proxy.h"
#include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
#include "chrome/browser/lifetime/browser_shutdown.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.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/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_list.h"
#include "chromeos/ui/base/app_types.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/app_constants/constants.h"
#include "components/exo/shell_surface_util.h"
#include "components/services/app_service/public/cpp/app_types.h"
#include "components/services/app_service/public/cpp/instance_update.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/browser/web_contents.h"
#include "ui/wm/core/window_util.h"
#include "ui/wm/public/activation_client.h"
AppServiceInstanceRegistryHelper::AppServiceInstanceRegistryHelper(
AppServiceAppWindowShelfController* controller)
: controller_(controller),
proxy_(apps::AppServiceProxyFactory::GetForProfile(
controller->owner()->profile())),
shelf_controller_helper_(std::make_unique<ShelfControllerHelper>(
controller->owner()->profile())) {
DCHECK(controller_);
}
AppServiceInstanceRegistryHelper::~AppServiceInstanceRegistryHelper() = default;
void AppServiceInstanceRegistryHelper::ActiveUserChanged() {
proxy_ = apps::AppServiceProxyFactory::GetForProfile(
ProfileManager::GetActiveUserProfile());
}
void AppServiceInstanceRegistryHelper::AdditionalUserAddedToSession() {
proxy_ = apps::AppServiceProxyFactory::GetForProfile(
ProfileManager::GetActiveUserProfile());
}
void AppServiceInstanceRegistryHelper::OnActiveTabChanged(
content::WebContents* old_contents,
content::WebContents* new_contents) {
if (old_contents) {
auto* old_window = old_contents->GetNativeView();
// Get the app_id from the existed instance first. If there is no record for
// the window, get the app_id from contents. Because when Chrome app open
// method is changed from windows to tabs, the app_id could be changed based
// on the URL, e.g. google photos, which might cause instance app_id
// inconsistent DCHECK error.
std::string app_id = GetAppId(old_window);
if (app_id.empty())
app_id = shelf_controller_helper_->GetAppID(old_contents);
// If app_id is empty, we should not set it as inactive because this is
// Chrome's tab.
if (!app_id.empty()) {
apps::InstanceState state = GetState(old_window);
// If the app has been inactive, we don't need to update the instance.
if ((state & apps::InstanceState::kActive) !=
apps::InstanceState::kUnknown) {
state = static_cast<apps::InstanceState>(state &
~apps::InstanceState::kActive);
OnInstances(app_id, old_window, std::string(), state);
}
}
}
if (new_contents) {
auto* window = GetWindow(new_contents);
// Get the app_id from the existed instance first. If there is no record for
// the window, get the app_id from contents. Because when Chrome app open
// method is changed from windows to tabs, the app_id could be changed based
// on the URL, e.g. google photos, which might cause instance app_id
// inconsistent DCHECK error.
std::string app_id = GetAppId(window);
if (app_id.empty())
app_id = GetAppId(new_contents);
// When the user drags a tab to a new browser, or to an other browser, the
// top window could be changed, so the relation for the tap window and the
// browser window.
UpdateTabWindow(app_id, window);
// If the app is active, it should be started, running, and visible.
apps::InstanceState state = static_cast<apps::InstanceState>(
apps::InstanceState::kStarted | apps::InstanceState::kRunning |
apps::InstanceState::kActive | apps::InstanceState::kVisible);
OnInstances(app_id, window, std::string(), state);
}
}
void AppServiceInstanceRegistryHelper::OnTabReplaced(
content::WebContents* old_contents,
content::WebContents* new_contents) {
OnTabClosing(old_contents);
OnTabInserted(new_contents);
}
void AppServiceInstanceRegistryHelper::OnTabInserted(
content::WebContents* contents) {
std::string app_id = GetAppId(contents);
auto* window = GetWindow(contents);
// When the user drags a tab to a new browser, or to an other browser, it
// could generate a temp instance for this window with the Chrome application
// app_id. For this case, this temp instance can be deleted, otherwise, DCHECK
// error for inconsistent app_id.
const std::string old_app_id = GetAppId(window);
if (!old_app_id.empty() && app_id != old_app_id) {
RemoveTabWindow(old_app_id, window);
OnInstances(old_app_id, window, std::string(),
apps::InstanceState::kDestroyed);
}
// The tab window could be dragged to a new browser, and the top window could
// be changed, so clear the old top window first, then add the new top window.
UpdateTabWindow(app_id, window);
apps::InstanceState state = static_cast<apps::InstanceState>(
apps::InstanceState::kStarted | apps::InstanceState::kRunning);
OnInstances(app_id, window, std::string(), state);
}
void AppServiceInstanceRegistryHelper::OnTabClosing(
content::WebContents* contents) {
auto* window = GetWindow(contents);
// When the tab is closed, if the window does not exists in the AppService
// InstanceRegistry, we don't need to update the status.
const std::string app_id = GetAppId(window);
if (app_id.empty())
return;
RemoveTabWindow(app_id, window);
OnInstances(app_id, window, std::string(), apps::InstanceState::kDestroyed);
}
void AppServiceInstanceRegistryHelper::OnBrowserRemoved() {
auto instances = GetInstances(app_constants::kChromeAppId);
for (const auto* instance : instances) {
if (!chrome::FindBrowserWithWindow(instance->Window())) {
// The tabs in the browser should be closed, and tab windows have been
// removed from |browser_window_to_tab_windows_|.
DCHECK(
!base::Contains(browser_window_to_tab_windows_, instance->Window()));
// The browser is removed if the window can't be found, so update the
// Chrome window instance as destroyed.
OnInstances(app_constants::kChromeAppId, instance->Window(),
std::string(), apps::InstanceState::kDestroyed);
}
}
}
void AppServiceInstanceRegistryHelper::OnInstances(const std::string& app_id,
aura::Window* window,
const std::string& launch_id,
apps::InstanceState state) {
if (app_id.empty() || !window)
return;
// The window could be teleported from the inactive user's profile to the
// current active user, so search all proxies. If the instance is found from a
// proxy, still save to that proxy, otherwise, save to the current active user
// profile's proxy.
apps::AppServiceProxy* proxy = proxy_;
for (Profile* profile : controller_->GetProfileList()) {
auto* proxy_for_profile =
apps::AppServiceProxyFactory::GetForProfile(profile);
if (proxy_for_profile->InstanceRegistry().Exists(window)) {
proxy = proxy_for_profile;
break;
}
}
apps::InstanceParams params(app_id, window);
params.launch_id = launch_id;
params.state = std::make_pair(state, base::Time::Now());
proxy->InstanceRegistry().CreateOrUpdateInstance(std::move(params));
}
void AppServiceInstanceRegistryHelper::OnSetShelfIDForBrowserWindowContents(
content::WebContents* contents) {
// Do not try to update window status on shutdown, because during the shutdown
// phase, we can't guaranteen the window destroy sequence, and it might cause
// crash.
if (browser_shutdown::HasShutdownStarted())
return;
auto* window = GetWindow(contents);
if (!window || !window->GetToplevelWindow())
return;
// If the app id is changed, call OnTabInserted to remove the old app id in
// AppService InstanceRegistry, and insert the new app id.
std::string app_id = GetAppId(contents);
const std::string old_app_id = GetAppId(window);
if (app_id != old_app_id)
OnTabInserted(contents);
// When system startup, session restore creates windows before
// ChromeShelfController is created, so windows restored can’t get the
// visible and activated status from OnWindowVisibilityChanged and
// OnWindowActivated. Also web apps are ready at the very late phase which
// delays the shelf id setting for windows. So check the top window's visible
// and activated status when we have the shelf id.
window = window->GetToplevelWindow();
const std::string top_app_id = GetAppId(window);
if (!top_app_id.empty()) {
app_id = top_app_id;
} else if (window->GetProperty(chromeos::kAppTypeKey) ==
chromeos::AppType::BROWSER) {
// For a normal browser window, set the app id as the browser app id.
app_id = app_constants::kChromeAppId;
}
OnWindowVisibilityChanged(ash::ShelfID(app_id), window, window->IsVisible());
auto* client = wm::GetActivationClient(window->GetRootWindow());
if (client) {
SetWindowActivated(ash::ShelfID(app_id), window,
/*active*/ window == client->GetActiveWindow());
}
}
void AppServiceInstanceRegistryHelper::OnWindowVisibilityChanged(
const ash::ShelfID& shelf_id,
aura::Window* window,
bool visible) {
if (shelf_id.app_id != app_constants::kChromeAppId) {
// For Web apps opened in an app window, we need to find the top level
// window to compare with the parameter |window|, because we save the tab
// window in AppService InstanceRegistry for Web apps, and we should set the
// state for the tab window to keep one instance for the Web app.
auto instances = GetInstances(shelf_id.app_id);
for (const auto* instance : instances) {
auto tab_it = tab_window_to_browser_window_.find(instance->Window());
if (tab_it == tab_window_to_browser_window_.end() ||
tab_it->second != window) {
continue;
}
// When the user drags a tab to a new browser, or to an other browser, the
// top window could be changed, so update the relation for the tap window
// and the browser window.
UpdateTabWindow(shelf_id.app_id, instance->Window());
apps::InstanceState state =
CalculateVisibilityState(instance->Window(), visible);
OnInstances(shelf_id.app_id, instance->Window(), shelf_id.launch_id,
state);
return;
}
return;
}
OnInstances(app_constants::kChromeAppId, window, std::string(),
CalculateVisibilityState(window, visible));
if (!base::Contains(browser_window_to_tab_windows_, window))
return;
// For Chrome browser app windows, sets the state for each tab window instance
// in this browser.
for (aura::Window* it : browser_window_to_tab_windows_[window]) {
const std::string app_id = GetAppId(it);
if (app_id.empty())
continue;
OnInstances(app_id, it, std::string(),
CalculateVisibilityState(it, visible));
}
}
void AppServiceInstanceRegistryHelper::SetWindowActivated(
const ash::ShelfID& shelf_id,
aura::Window* window,
bool active) {
if (shelf_id.app_id != app_constants::kChromeAppId) {
// For Web apps opened in an app window, we need to find the top level
// window to compare with |window|, because we save the tab
// window in AppService InstanceRegistry for Web apps, and we should set the
// state for the tab window to keep one instance for the Web app.
auto instances = GetInstances(shelf_id.app_id);
for (const auto* instance : instances) {
if (instance->Window()->GetToplevelWindow() != window) {
continue;
}
// When the user drags a tab to a new browser, or to an other browser, the
// top window could be changed, so the relation for the tab window and the
// browser window.
UpdateTabWindow(shelf_id.app_id, instance->Window());
apps::InstanceState state =
CalculateActivatedState(instance->Window(), active);
OnInstances(shelf_id.app_id, instance->Window(), shelf_id.launch_id,
state);
return;
}
return;
}
OnInstances(app_constants::kChromeAppId, window, std::string(),
CalculateActivatedState(window, active));
if (!base::Contains(browser_window_to_tab_windows_, window))
return;
// For the Chrome browser, when the window is activated, the active tab is set
// as started, running, visible and active state.
if (active) {
Browser* browser = chrome::FindBrowserWithWindow(window);
if (!browser)
return;
content::WebContents* contents =
browser->tab_strip_model()->GetActiveWebContents();
if (!contents)
return;
constexpr auto kState = static_cast<apps::InstanceState>(
apps::InstanceState::kStarted | apps::InstanceState::kRunning |
apps::InstanceState::kActive | apps::InstanceState::kVisible);
auto* contents_window = GetWindow(contents);
// Get the app_id from the existed instance first. The app_id for PWAs could
// be changed based on the URL, e.g. google photos, which might cause
// instance app_id inconsistent DCHECK error.
std::string app_id = GetAppId(contents_window);
app_id = app_id.empty() ? GetAppId(contents) : app_id;
// When the user drags a tab to a new browser, or to an other browser, the
// top window could be changed, so the relation for the tap window and the
// browser window.
UpdateTabWindow(app_id, contents_window);
OnInstances(app_id, contents_window, std::string(), kState);
return;
}
// For Chrome browser app windows, sets the state for each tab window instance
// in this browser.
for (aura::Window* it : browser_window_to_tab_windows_[window]) {
const std::string app_id = GetAppId(it);
if (app_id.empty())
continue;
OnInstances(app_id, it, std::string(), CalculateActivatedState(it, active));
}
}
apps::InstanceState AppServiceInstanceRegistryHelper::CalculateVisibilityState(
const aura::Window* window,
bool visible) const {
apps::InstanceState state = GetState(window);
state = static_cast<apps::InstanceState>(
state | apps::InstanceState::kStarted | apps::InstanceState::kRunning);
state = (visible) ? static_cast<apps::InstanceState>(
state | apps::InstanceState::kVisible)
: static_cast<apps::InstanceState>(
state & ~(apps::InstanceState::kVisible));
return state;
}
apps::InstanceState AppServiceInstanceRegistryHelper::CalculateActivatedState(
const aura::Window* window,
bool active) const {
// If the app is active, it should be started, running, and visible.
if (active) {
return static_cast<apps::InstanceState>(
apps::InstanceState::kStarted | apps::InstanceState::kRunning |
apps::InstanceState::kActive | apps::InstanceState::kVisible);
}
apps::InstanceState state = GetState(window);
state = static_cast<apps::InstanceState>(
state | apps::InstanceState::kStarted | apps::InstanceState::kRunning);
state =
static_cast<apps::InstanceState>(state & ~apps::InstanceState::kActive);
return state;
}
bool AppServiceInstanceRegistryHelper::IsOpenedInBrowser(
const std::string& app_id,
aura::Window* window) const {
// Windows created by exo with app/startup ids are not browser windows.
if (exo::GetShellApplicationId(window) || exo::GetShellStartupId(window))
return false;
for (Profile* profile : controller_->GetProfileList()) {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
auto app_type = proxy->AppRegistryCache().GetAppType(app_id);
if (app_type == apps::AppType::kUnknown)
continue;
// Skip extensions because the browser controller is responsible for
// extension windows.
if (app_type == apps::AppType::kExtension)
return true;
if (app_type != apps::AppType::kChromeApp &&
app_type != apps::AppType::kSystemWeb &&
app_type != apps::AppType::kWeb) {
return false;
}
}
// For Extension apps, and Web apps, AppServiceAppWindowShelfController
// should only handle Chrome apps, managed by extensions::AppWindow, which
// should set |browser_context| in AppService InstanceRegistry. So if
// |browser_context| is not null, the app is a Chrome app,
// AppServiceAppWindowShelfController should handle it, otherwise, it is
// opened in a browser, and AppServiceAppWindowShelfController should skip
// them.
//
// The window could be teleported from the inactive user's profile to the
// current active user, so search all proxies.
for (Profile* profile : controller_->GetProfileList()) {
content::BrowserContext* browser_context = nullptr;
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
bool found = false;
proxy->InstanceRegistry().ForInstancesWithWindow(
window, [&browser_context, &found](const apps::InstanceUpdate& update) {
browser_context = update.BrowserContext();
found = true;
});
if (found) {
return !browser_context;
}
}
return true;
}
std::string AppServiceInstanceRegistryHelper::GetAppId(
const aura::Window* window) const {
for (Profile* profile : controller_->GetProfileList()) {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
std::string app_id = proxy->InstanceRegistry().GetShelfId(window).app_id;
if (!app_id.empty())
return app_id;
}
return std::string();
}
std::string AppServiceInstanceRegistryHelper::GetAppId(
content::WebContents* contents) const {
std::string app_id = shelf_controller_helper_->GetAppID(contents);
if (!app_id.empty())
return app_id;
return app_constants::kChromeAppId;
}
aura::Window* AppServiceInstanceRegistryHelper::GetWindow(
content::WebContents* contents) {
std::string app_id = shelf_controller_helper_->GetAppID(contents);
aura::Window* window = contents->GetNativeView();
// If |app_id| is empty, it is a browser tab. Returns the toplevel window in
// this case.
if (app_id.empty())
window = window->GetToplevelWindow();
return window;
}
std::set<const apps::Instance*> AppServiceInstanceRegistryHelper::GetInstances(
const std::string& app_id) {
std::set<const apps::Instance*> instances;
for (Profile* profile : controller_->GetProfileList()) {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
instances = base::STLSetUnion<std::set<const apps::Instance*>>(
instances, proxy->InstanceRegistry().GetInstances(app_id));
}
return instances;
}
apps::InstanceState AppServiceInstanceRegistryHelper::GetState(
const aura::Window* window) const {
for (Profile* profile : controller_->GetProfileList()) {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
auto state = proxy->InstanceRegistry().GetState(window);
if (state != apps::InstanceState::kUnknown)
return state;
}
return apps::InstanceState::kUnknown;
}
void AppServiceInstanceRegistryHelper::AddTabWindow(const std::string& app_id,
aura::Window* window) {
if (app_id == app_constants::kChromeAppId)
return;
aura::Window* top_level_window = window->GetToplevelWindow();
browser_window_to_tab_windows_[top_level_window].insert(window);
tab_window_to_browser_window_[window] = top_level_window;
}
void AppServiceInstanceRegistryHelper::RemoveTabWindow(
const std::string& app_id,
aura::Window* window) {
if (app_id == app_constants::kChromeAppId)
return;
auto it = tab_window_to_browser_window_.find(window);
if (it == tab_window_to_browser_window_.end())
return;
aura::Window* top_level_window = it->second;
auto browser_it = browser_window_to_tab_windows_.find(top_level_window);
DCHECK(browser_it != browser_window_to_tab_windows_.end());
browser_it->second.erase(window);
if (browser_it->second.empty())
browser_window_to_tab_windows_.erase(browser_it);
tab_window_to_browser_window_.erase(it);
}
void AppServiceInstanceRegistryHelper::UpdateTabWindow(
const std::string& app_id,
aura::Window* window) {
RemoveTabWindow(app_id, window);
AddTabWindow(app_id, window);
}