blob: 23a31812e82eb2c813a8e8857939366f74c2114e [file] [log] [blame]
// Copyright 2022 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/apps/app_service/metrics/website_metrics.h"
#include "base/containers/contains.h"
#include "base/json/values_util.h"
#include "chrome/browser/apps/app_service/web_contents_app_id_utils.h"
#include "chrome/browser/history/history_service_factory.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/web_applications/web_app_helpers.h"
#include "components/history/core/browser/history_types.h"
#include "third_party/blink/public/common/manifest/manifest_util.h"
#include "third_party/blink/public/common/permissions_policy/permissions_policy.h"
#include "third_party/blink/public/mojom/installation/installation.mojom.h"
#include "third_party/blink/public/mojom/manifest/manifest.mojom.h"
#include "ui/aura/window.h"
#include "ui/wm/core/window_util.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();
}
aura::Window* GetWindowWithBrowser(Browser* browser) {
if (!browser) {
return nullptr;
}
BrowserWindow* browser_window = browser->window();
// In some test cases, browser window might be skipped.
return browser_window ? browser_window->GetNativeWindow() : nullptr;
}
aura::Window* GetWindowWithTabStripModel(TabStripModel* tab_strip_model) {
for (auto* browser : *BrowserList::GetInstance()) {
if (browser->tab_strip_model() == tab_strip_model) {
return GetWindowWithBrowser(browser);
}
}
return nullptr;
}
wm::ActivationClient* GetActivationClientWithTabStripModel(
TabStripModel* tab_strip_model) {
auto* window = GetWindowWithTabStripModel(tab_strip_model);
if (!window) {
return nullptr;
}
auto* root_window = window->GetRootWindow();
DCHECK(root_window);
return wm::GetActivationClient(root_window);
}
} // namespace
namespace apps {
constexpr char kWebsiteUsageTime[] = "app_platform_metrics.website_usage_time";
constexpr char kRunningTimeKey[] = "time";
constexpr char kUrlContentKey[] = "url_content";
constexpr char kPromotableKey[] = "promotable";
WebsiteMetrics::ActiveTabWebContentsObserver::ActiveTabWebContentsObserver(
content::WebContents* contents,
WebsiteMetrics* owner)
: content::WebContentsObserver(contents), owner_(owner) {}
WebsiteMetrics::ActiveTabWebContentsObserver::~ActiveTabWebContentsObserver() =
default;
void WebsiteMetrics::ActiveTabWebContentsObserver::PrimaryPageChanged(
content::Page& page) {
owner_->OnWebContentsUpdated(web_contents());
if (app_banner_manager_observer_.IsObserving()) {
return;
}
auto* app_banner_manager =
webapps::AppBannerManager::FromWebContents(web_contents());
// In some test cases, AppBannerManager might be null.
if (app_banner_manager) {
app_banner_manager_observer_.Observe(app_banner_manager);
}
}
void WebsiteMetrics::ActiveTabWebContentsObserver::WebContentsDestroyed() {
app_banner_manager_observer_.Reset();
}
void WebsiteMetrics::ActiveTabWebContentsObserver::
OnInstallableWebAppStatusUpdated() {
owner_->OnInstallableWebAppStatusUpdated(web_contents());
}
base::Value WebsiteMetrics::UrlInfo::ConvertToValue() const {
base::Value usage_time_dict(base::Value::Type::DICTIONARY);
usage_time_dict.SetPath(kRunningTimeKey,
base::TimeDeltaToValue(running_time));
usage_time_dict.SetIntKey(kUrlContentKey, static_cast<int>(url_content));
usage_time_dict.SetBoolKey(kPromotableKey, promotable);
return usage_time_dict;
}
WebsiteMetrics::WebsiteMetrics(Profile* profile)
: profile_(profile), browser_tab_strip_tracker_(this, nullptr) {
BrowserList::GetInstance()->AddObserver(this);
browser_tab_strip_tracker_.Init();
history::HistoryService* history_service =
HistoryServiceFactory::GetForProfileWithoutCreating(profile);
if (history_service) {
history_observation_.Observe(history_service);
}
}
WebsiteMetrics::~WebsiteMetrics() {
BrowserList::RemoveObserver(this);
}
void WebsiteMetrics::OnBrowserAdded(Browser* browser) {
if (IsAppBrowser(browser)) {
return;
}
auto* window = GetWindowWithBrowser(browser);
if (window) {
window_to_web_contents_[window] = nullptr;
}
}
void WebsiteMetrics::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
DCHECK(tab_strip_model);
auto* window = GetWindowWithTabStripModel(tab_strip_model);
if (!window || !base::Contains(window_to_web_contents_, window)) {
// Skip the app browser window.
return;
}
switch (change.type()) {
case TabStripModelChange::kInserted:
OnTabStripModelChangeInsert(tab_strip_model, *change.GetInsert(),
selection);
break;
case TabStripModelChange::kRemoved:
OnTabStripModelChangeRemove(window, tab_strip_model, *change.GetRemove(),
selection);
break;
case TabStripModelChange::kReplaced:
OnTabStripModelChangeReplace(*change.GetReplace());
break;
case TabStripModelChange::kMoved:
case TabStripModelChange::kSelectionOnly:
break;
}
if (selection.active_tab_changed()) {
OnActiveTabChanged(window, selection.old_contents, selection.new_contents);
}
}
void WebsiteMetrics::OnWindowActivated(ActivationReason reason,
aura::Window* gained_active,
aura::Window* lost_active) {
SetWindowInActivated(lost_active);
SetWindowActivated(gained_active);
}
void WebsiteMetrics::OnURLsDeleted(history::HistoryService* history_service,
const history::DeletionInfo& deletion_info) {
// To simplify the implementation, remove all recorded urls no matter whatever
// `deletion_info`.
webcontents_to_ukm_key_.clear();
url_infos_.clear();
DictionaryPrefUpdate usage_time_update(profile_->GetPrefs(),
kWebsiteUsageTime);
auto& dict = usage_time_update->GetDict();
dict.clear();
}
void WebsiteMetrics::HistoryServiceBeingDeleted(
history::HistoryService* history_service) {
DCHECK(history_observation_.IsObservingSource(history_service));
history_observation_.Reset();
}
void WebsiteMetrics::OnFiveMinutes() {
SaveUsageTime();
}
void WebsiteMetrics::OnTwoHours() {
// TODO(crbug.com/1334173): Records the usage time UKM, and reset the local
// variables after recording the UKM.
std::map<GURL, UrlInfo> url_infos;
for (const auto& it : webcontents_to_ukm_key_) {
if (!base::Contains(url_infos, it.second)) {
url_infos[it.second] = std::move(url_infos_[it.second]);
}
}
url_infos.swap(url_infos_);
}
void WebsiteMetrics::OnTabStripModelChangeInsert(
TabStripModel* tab_strip_model,
const TabStripModelChange::Insert& insert,
const TabStripSelectionChange& selection) {
if (insert.contents.size() == 0) {
return;
}
// First tab attached.
if (tab_strip_model->count() == static_cast<int>(insert.contents.size())) {
// 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).
auto* activation_client =
GetActivationClientWithTabStripModel(tab_strip_model);
if (!activation_client_observations_.IsObservingSource(activation_client)) {
activation_client_observations_.AddObservation(activation_client);
}
}
}
void WebsiteMetrics::OnTabStripModelChangeRemove(
aura::Window* window,
TabStripModel* tab_strip_model,
const TabStripModelChange::Remove& remove,
const TabStripSelectionChange& selection) {
for (const auto& removed_tab : remove.contents) {
OnTabClosed(removed_tab.contents);
}
// Last tab detached.
if (tab_strip_model->count() == 0) {
// Unobserve the activation client of the root window of the browser's aura
// window if the last browser using it was just removed.
auto* activation_client =
GetActivationClientWithTabStripModel(tab_strip_model);
if (activation_client_observations_.IsObservingSource(activation_client)) {
activation_client_observations_.RemoveObservation(activation_client);
}
// The browser window will be closed, so remove the window and the web
// contents.
auto it = window_to_web_contents_.find(window);
if (it != window_to_web_contents_.end()) {
OnTabClosed(it->second);
window_to_web_contents_.erase(it);
}
}
}
void WebsiteMetrics::OnTabStripModelChangeReplace(
const TabStripModelChange::Replace& replace) {
OnTabClosed(replace.old_contents);
}
void WebsiteMetrics::OnActiveTabChanged(aura::Window* window,
content::WebContents* old_contents,
content::WebContents* new_contents) {
if (old_contents) {
SetTabInActivated(old_contents);
// Clear `old_contents` from `window_to_web_contents_`.
auto it = window_to_web_contents_.find(window);
if (it != window_to_web_contents_.end())
it->second = nullptr;
}
if (new_contents) {
SetTabActivated(new_contents);
window_to_web_contents_[window] = new_contents;
if (!base::Contains(webcontents_to_observer_map_, new_contents)) {
webcontents_to_observer_map_[new_contents] =
std::make_unique<WebsiteMetrics::ActiveTabWebContentsObserver>(
new_contents, this);
}
}
}
void WebsiteMetrics::OnTabClosed(content::WebContents* web_contents) {
SetTabInActivated(web_contents);
webcontents_to_ukm_key_.erase(web_contents);
webcontents_to_observer_map_.erase(web_contents);
}
void WebsiteMetrics::OnWebContentsUpdated(content::WebContents* web_contents) {
// If there is an app for the url, we don't need to record the url, because
// the app metrics can record the usage time metrics.
if (GetInstanceAppIdForWebContents(web_contents).has_value()) {
webcontents_to_ukm_key_.erase(web_contents);
return;
}
auto* window =
GetWindowWithBrowser(chrome::FindBrowserWithWebContents(web_contents));
if (!window) {
return;
}
// When the primary page of `web_contents` is changed, call SetTabInActivated
// to calculate the usage time for the previous ukm key url.
SetTabInActivated(web_contents);
// When the primary page of `web_contents` is changed called by
// contents::WebContentsObserver::PrimaryPageChanged(), set the visible url as
// default value for the ukm key url.
webcontents_to_ukm_key_[web_contents] = web_contents->GetVisibleURL();
AddUrlInfo(web_contents->GetVisibleURL(), base::TimeTicks::Now(),
UrlContent::kFullUrl, wm::IsActiveWindow(window),
/*promotable=*/false);
}
void WebsiteMetrics::OnInstallableWebAppStatusUpdated(
content::WebContents* web_contents) {
auto it = webcontents_to_ukm_key_.find(web_contents);
if (it == webcontents_to_ukm_key_.end()) {
// If the `web_contents` has been removed or replaced, we don't need to set
// the url.
return;
}
// WebContents in app windows are filtered out in OnBrowserAdded. Installed
// web apps opened in tabs are filtered out too. So every WebContents here
// must be a website not installed. Check the manifest to get the scope or the
// start url if there is a manifest.
auto* app_banner_manager =
webapps::AppBannerManager::FromWebContents(web_contents);
DCHECK(app_banner_manager);
if (blink::IsEmptyManifest(app_banner_manager->manifest())) {
return;
}
auto* window =
GetWindowWithBrowser(chrome::FindBrowserWithWebContents(web_contents));
if (!window) {
return;
}
DCHECK(!app_banner_manager->manifest().scope.is_empty());
UpdateUrlInfo(it->second, app_banner_manager->manifest().scope,
UrlContent::kScope, wm::IsActiveWindow(window),
/*promotable=*/true);
it->second = app_banner_manager->manifest().scope;
}
void WebsiteMetrics::AddUrlInfo(const GURL& url,
const base::TimeTicks& start_time,
UrlContent url_content,
bool is_activated,
bool promotable) {
auto& url_info = url_infos_[url];
url_info.start_time = start_time;
url_info.url_content = url_content;
url_info.is_activated = is_activated;
url_info.promotable = promotable;
}
void WebsiteMetrics::UpdateUrlInfo(const GURL& old_url,
const GURL& new_url,
UrlContent url_content,
bool is_activated,
bool promotable) {
base::TimeTicks start_time = base::TimeTicks::Now();
auto it = url_infos_.find(old_url);
if (it != url_infos_.end()) {
start_time = it->second.start_time;
url_infos_.erase(old_url);
}
AddUrlInfo(new_url, start_time, url_content, is_activated, promotable);
}
void WebsiteMetrics::SetWindowActivated(aura::Window* window) {
auto it = window_to_web_contents_.find(window);
if (it != window_to_web_contents_.end()) {
SetTabActivated(it->second);
}
}
void WebsiteMetrics::SetWindowInActivated(aura::Window* window) {
auto it = window_to_web_contents_.find(window);
if (it != window_to_web_contents_.end()) {
SetTabInActivated(it->second);
}
}
void WebsiteMetrics::SetTabActivated(content::WebContents* web_contents) {
auto web_contents_it = webcontents_to_ukm_key_.find(web_contents);
if (web_contents_it == webcontents_to_ukm_key_.end()) {
return;
}
auto url_it = url_infos_.find(web_contents_it->second);
if (url_it == url_infos_.end()) {
return;
}
url_it->second.start_time = base::TimeTicks::Now();
url_it->second.is_activated = true;
}
void WebsiteMetrics::SetTabInActivated(content::WebContents* web_contents) {
auto web_contents_it = webcontents_to_ukm_key_.find(web_contents);
if (web_contents_it == webcontents_to_ukm_key_.end()) {
return;
}
// Check whether `web_contents` is activated. If yes, calculate the running
// time based on the start time set when `web_contents` is activated.
auto it = url_infos_.find(web_contents_it->second);
if (it == url_infos_.end() || !it->second.is_activated) {
return;
}
DCHECK_GE(base::TimeTicks::Now(), it->second.start_time);
it->second.running_time += base::TimeTicks::Now() - it->second.start_time;
it->second.is_activated = false;
}
void WebsiteMetrics::SaveUsageTime() {
DictionaryPrefUpdate usage_time_update(profile_->GetPrefs(),
kWebsiteUsageTime);
auto& dict = usage_time_update->GetDict();
dict.clear();
for (auto it : url_infos_) {
if (it.second.is_activated) {
it.second.running_time += base::TimeTicks::Now() - it.second.start_time;
it.second.start_time = base::TimeTicks::Now();
}
if (!it.second.running_time.is_zero()) {
dict.Set(it.first.spec(), it.second.ConvertToValue());
}
}
}
} // namespace apps