blob: 2e75d20aafbcceae613dd4dbd7f7dffd972e9f70 [file] [log] [blame]
// Copyright 2019 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/launcher/app_service/app_service_instance_registry_helper.h"
#include <set>
#include <string>
#include <vector>
#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/chromeos/crostini/crostini_shelf_utils.h"
#include "chrome/browser/chromeos/crostini/crostini_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/ash/launcher/app_service/app_service_app_window_launcher_controller.h"
#include "chrome/browser/ui/ash/launcher/chrome_launcher_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/web_applications/components/web_app_id.h"
#include "chrome/common/chrome_features.h"
#include "components/services/app_service/public/cpp/instance_update.h"
#include "components/services/app_service/public/mojom/types.mojom.h"
#include "content/public/browser/web_contents.h"
#include "extensions/common/constants.h"
#include "ui/wm/core/window_util.h"
#include "ui/wm/public/activation_client.h"
AppServiceInstanceRegistryHelper::AppServiceInstanceRegistryHelper(
AppServiceAppWindowLauncherController* controller)
: controller_(controller),
proxy_(apps::AppServiceProxyFactory::GetForProfile(
controller->owner()->profile())),
launcher_controller_helper_(std::make_unique<LauncherControllerHelper>(
controller->owner()->profile())) {
DCHECK(controller_);
DCHECK(proxy_);
}
AppServiceInstanceRegistryHelper::~AppServiceInstanceRegistryHelper() = default;
void AppServiceInstanceRegistryHelper::ActiveUserChanged() {
if (!base::FeatureList::IsEnabled(features::kAppServiceInstanceRegistry))
return;
proxy_ = apps::AppServiceProxyFactory::GetForProfile(
ProfileManager::GetActiveUserProfile());
}
void AppServiceInstanceRegistryHelper::AdditionalUserAddedToSession(
Profile* profile) {
proxy_ = apps::AppServiceProxyFactory::GetForProfile(profile);
}
void AppServiceInstanceRegistryHelper::OnActiveTabChanged(
content::WebContents* old_contents,
content::WebContents* new_contents) {
if (!base::FeatureList::IsEnabled(features::kAppServiceInstanceRegistry))
return;
if (old_contents) {
auto* 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(window);
if (app_id.empty())
app_id = launcher_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(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, GetWindow(old_contents), 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) {
if (!base::FeatureList::IsEnabled(features::kAppServiceInstanceRegistry))
return;
OnTabClosing(old_contents);
OnTabInserted(new_contents);
}
void AppServiceInstanceRegistryHelper::OnTabInserted(
content::WebContents* contents) {
if (!base::FeatureList::IsEnabled(features::kAppServiceInstanceRegistry))
return;
std::string app_id = GetAppId(contents);
aura::Window* 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) {
if (!base::FeatureList::IsEnabled(features::kAppServiceInstanceRegistry))
return;
aura::Window* 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 windows = GetWindows(extension_misc::kChromeAppId);
for (auto* window : windows) {
if (!chrome::FindBrowserWithWindow(window)) {
// Remove windows from |browser_window_to_tab_window_| and
// |tab_window_to_browser_window_|, because OnTabClosing could be not
// called for tabs in the browser, when the browser is removed.
if (base::Contains(browser_window_to_tab_window_, window)) {
for (auto* w : browser_window_to_tab_window_[window]) {
tab_window_to_browser_window_.erase(w);
OnInstances(GetAppId(w), w, std::string(),
apps::InstanceState::kDestroyed);
}
browser_window_to_tab_window_.erase(window);
}
// The browser is removed if the window can't be found, so update the
// Chrome window instance as destroyed.
OnInstances(extension_misc::kChromeAppId, 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())
return;
std::unique_ptr<apps::Instance> instance =
std::make_unique<apps::Instance>(app_id, window);
instance->SetLaunchId(launch_id);
instance->UpdateState(state, base::Time::Now());
std::vector<std::unique_ptr<apps::Instance>> deltas;
deltas.push_back(std::move(instance));
// 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 (auto* profile : controller_->GetProfileList()) {
auto* proxy_for_profile =
apps::AppServiceProxyFactory::GetForProfile(profile);
if (proxy_for_profile->InstanceRegistry().Exists(window)) {
proxy = proxy_for_profile;
break;
}
}
proxy->InstanceRegistry().OnInstances(std::move(deltas));
}
void AppServiceInstanceRegistryHelper::OnSetShelfIDForBrowserWindowContents(
content::WebContents* contents) {
if (!base::FeatureList::IsEnabled(features::kAppServiceInstanceRegistry))
return;
aura::Window* 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
// ChromeLauncherController 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;
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 != extension_misc::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 windows = GetWindows(shelf_id.app_id);
for (auto* it : windows) {
if (it->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 tap window and the
// browser window.
UpdateTabWindow(shelf_id.app_id, it);
apps::InstanceState state = CalculateVisibilityState(it, visible);
OnInstances(shelf_id.app_id, it, shelf_id.launch_id, state);
return;
}
return;
}
apps::InstanceState state = CalculateVisibilityState(window, visible);
OnInstances(extension_misc::kChromeAppId, window, std::string(), state);
if (!base::Contains(browser_window_to_tab_window_, window))
return;
// For Chrome browser app windows, sets the state for each tab window instance
// in this browser.
for (auto* it : browser_window_to_tab_window_[window]) {
const std::string app_id = GetAppId(it);
if (app_id.empty())
continue;
apps::InstanceState state = CalculateVisibilityState(it, visible);
OnInstances(app_id, it, std::string(), state);
}
}
void AppServiceInstanceRegistryHelper::SetWindowActivated(
const ash::ShelfID& shelf_id,
aura::Window* window,
bool active) {
if (shelf_id.app_id != extension_misc::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 windows = GetWindows(shelf_id.app_id);
for (auto* it : windows) {
if (it->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 tap window and the
// browser window.
UpdateTabWindow(shelf_id.app_id, it);
apps::InstanceState state = CalculateActivatedState(it, active);
OnInstances(shelf_id.app_id, it, shelf_id.launch_id, state);
return;
}
return;
}
apps::InstanceState state = CalculateActivatedState(window, active);
OnInstances(extension_misc::kChromeAppId, window, std::string(), state);
if (!base::Contains(browser_window_to_tab_window_, 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();
apps::InstanceState state = 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(), state);
return;
}
// For Chrome browser app windows, sets the state for each tab window instance
// in this browser.
for (auto* it : browser_window_to_tab_window_[window]) {
const std::string app_id = GetAppId(it);
if (app_id.empty())
continue;
apps::InstanceState state = CalculateActivatedState(it, active);
OnInstances(app_id, it, std::string(), state);
}
}
apps::InstanceState AppServiceInstanceRegistryHelper::CalculateVisibilityState(
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(
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 {
// Crostini Terminal App with the app_id kCrostiniTerminalSystemAppId is a
// System Web App.
if (app_id == crostini::kCrostiniTerminalSystemAppId)
return true;
if (crostini::IsUnmatchedCrostiniShelfAppId(app_id))
return false;
for (auto* profile : controller_->GetProfileList()) {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
apps::mojom::AppType app_type =
proxy->AppRegistryCache().GetAppType(app_id);
if (app_type == apps::mojom::AppType::kUnknown)
continue;
if (app_type != apps::mojom::AppType::kExtension &&
app_type != apps::mojom::AppType::kWeb) {
return false;
}
}
// For Extension apps, and Web apps, AppServiceAppWindowLauncherController
// 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,
// AppServiceAppWindowLauncherController should handle it, otherwise, it is
// opened in a browser, and AppServiceAppWindowLauncherController should skip
// them.
//
// The window could be teleported from the inactive user's profile to the
// current active user, so search all proxies.
for (auto* profile : controller_->GetProfileList()) {
content::BrowserContext* browser_context = nullptr;
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
bool found = false;
proxy->InstanceRegistry().ForOneInstance(
window, [&browser_context, &found](const apps::InstanceUpdate& update) {
browser_context = update.BrowserContext();
found = true;
});
if (!found)
continue;
return (browser_context) ? false : true;
}
return true;
}
std::string AppServiceInstanceRegistryHelper::GetAppId(
aura::Window* window) const {
for (auto* 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 = launcher_controller_helper_->GetAppID(contents);
if (!app_id.empty())
return app_id;
return extension_misc::kChromeAppId;
}
aura::Window* AppServiceInstanceRegistryHelper::GetWindow(
content::WebContents* contents) {
std::string app_id = launcher_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<aura::Window*> AppServiceInstanceRegistryHelper::GetWindows(
const std::string& app_id) {
std::set<aura::Window*> windows;
for (auto* profile : controller_->GetProfileList()) {
auto* proxy = apps::AppServiceProxyFactory::GetForProfile(profile);
auto w = proxy->InstanceRegistry().GetWindows(app_id);
windows = base::STLSetUnion<std::set<aura::Window*>>(windows, w);
}
return windows;
}
apps::InstanceState AppServiceInstanceRegistryHelper::GetState(
aura::Window* window) const {
for (auto* 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 == extension_misc::kChromeAppId)
return;
aura::Window* top_level_window = window->GetToplevelWindow();
browser_window_to_tab_window_[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 == extension_misc::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_window_.find(top_level_window);
DCHECK(browser_it != browser_window_to_tab_window_.end());
browser_it->second.erase(window);
if (browser_it->second.empty())
browser_window_to_tab_window_.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);
}