blob: 17ea6f61792ca471d622172dff53bebb92f24407 [file] [log] [blame]
// Copyright 2021 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/metrics/usage_scenario/tab_usage_scenario_tracker.h"
#include "base/containers/contains.h"
#include "chrome/browser/metrics/usage_scenario/usage_scenario_data_store.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/visibility.h"
#include "content/public/browser/web_contents.h"
#include "extensions/browser/script_injection_tracker.h"
#include "extensions/common/extension_id.h"
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "ui/display/screen.h"
#include "url/origin.h"
namespace metrics {
namespace {
std::pair<ukm::SourceId, url::Origin> GetNavigationInfoForContents(
content::WebContents* contents) {
auto* main_frame = contents->GetPrimaryMainFrame();
if (!main_frame || main_frame->GetLastCommittedURL().is_empty())
return std::make_pair(ukm::kInvalidSourceId, url::Origin());
return std::make_pair(main_frame->GetPageUkmSourceId(),
main_frame->GetLastCommittedOrigin());
}
extensions::ExtensionIdSet GetExtensionsThatRanContentScriptsInWebContents(
content::WebContents* contents) {
content::RenderFrameHost* main_frame = contents->GetPrimaryMainFrame();
if (!main_frame) {
// WebContents is being destroyed.
return {};
}
// Find the complete set of processes in the WebContents' frame tree first so
// that each is only handled once.
std::set<content::RenderProcessHost*> processes;
processes.insert(main_frame->GetProcess());
main_frame->ForEachRenderFrameHost(
[&processes](content::RenderFrameHost* frame) {
processes.insert(frame->GetProcess());
});
// Find the set of extensions that ran scripts in these processes.
//
// The result may include extra extensions for the following reasons:
//
// * ScriptInjectionTracker returns all extensions that have injected scripts
// into a process. Multiple pages on the same domain might share a process
// so we can't be sure which pages the extension affected.
// * It also returns extensions that have EVER injected scripts into the
// process, even if the extension is no longer active (eg. after navigating
// to a new page or removing the extension).
// * There are renderer-side permission checks that might cause a page to
// refuse to inject a script, which ScriptInjectionTracker isn't aware of so
// assumes the script was injected.
//
// In the first two points, the extension COULD still be affecting pages
// loaded in the process (due to bugs or malicious code, or just because
// changes the extension made to the page content had lingering effects).
//
// It may also miss extensions for the following reasons:
//
// * ScriptInjectionTracker only includes extensions that inject Javascript.
// It doesn't track injected CSS scripts, declarative web requests, or
// anything else an extension might do that could affect the page content.
// * The set of processes for the page includes only those that hosted frames,
// not workers.
extensions::ExtensionIdSet extensions;
for (const auto* process : processes) {
extensions.merge(extensions::ScriptInjectionTracker::
GetExtensionsThatRanContentScriptsInProcess(*process));
}
return extensions;
}
} // namespace
TabUsageScenarioTracker::TabUsageScenarioTracker(
UsageScenarioDataStoreImpl* usage_scenario_data_store)
: usage_scenario_data_store_(usage_scenario_data_store) {
// TODO(crbug.com/40158987): Owners of this class have to set the initial
// state. Constructing the object like this starts off the state as empty. If
// tabs/windows already exist when this object is created they need to be
// added using the normal functions after creation.
}
TabUsageScenarioTracker::~TabUsageScenarioTracker() {
// Make sure that this doesn't get destroyed after destroying the global
// screen instance.
DCHECK(display::Screen::GetScreen());
}
void TabUsageScenarioTracker::OnTabAdded(content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
usage_scenario_data_store_->OnTabAdded();
// Tab is added already visible. It will not get a separate visibility update
// so we handle the visibility here.
if (web_contents->GetVisibility() == content::Visibility::VISIBLE) {
DCHECK(!base::Contains(visible_tabs_, web_contents));
usage_scenario_data_store_->OnWindowVisible();
InsertContentsInMapOfVisibleTabs(web_contents);
}
}
void TabUsageScenarioTracker::OnTabRemoved(content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnWebContentsRemoved(web_contents);
usage_scenario_data_store_->OnTabClosed();
}
void TabUsageScenarioTracker::OnTabReplaced(
content::WebContents* old_contents,
content::WebContents* new_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnWebContentsRemoved(old_contents);
DCHECK(!base::Contains(visible_tabs_, old_contents));
DCHECK(!base::Contains(contents_playing_video_, old_contents));
DCHECK(!base::Contains(contents_playing_video_fullscreen_, old_contents));
// Start tracking |new_contents| if needed.
if (new_contents->GetVisibility() == content::Visibility::VISIBLE)
OnTabVisibilityChanged(new_contents);
if (new_contents->IsCurrentlyAudible())
usage_scenario_data_store_->OnAudioStarts();
}
void TabUsageScenarioTracker::OnTabVisibilityChanged(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto iter = visible_tabs_.find(web_contents);
const bool was_visible = iter != visible_tabs_.end();
const bool is_visible =
web_contents->GetVisibility() == content::Visibility::VISIBLE;
// The first content::Visibility::VISIBLE notification is always sent, even
// if the tab starts in the visible state.
if (!was_visible && is_visible) {
usage_scenario_data_store_->OnWindowVisible();
// If this tab is playing video then record that it became visible.
if (base::Contains(contents_playing_video_, web_contents)) {
usage_scenario_data_store_->OnVideoStartsInVisibleTab();
}
InsertContentsInMapOfVisibleTabs(web_contents);
} else if (was_visible && !is_visible) {
// The tab was previously visible and it's now hidden or occluded.
OnTabBecameHidden(&iter);
}
}
void TabUsageScenarioTracker::OnTabDiscarded(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Record that the ukm::SourceID associated with this tab isn't visible
// anymore, if necessary.
auto iter = visible_tabs_.find(web_contents);
CHECK_EQ(iter != visible_tabs_.end(),
web_contents->GetVisibility() == content::Visibility::VISIBLE);
if (iter != visible_tabs_.end() &&
iter->second.first != ukm::kInvalidSourceId) {
usage_scenario_data_store_->OnUkmSourceBecameHidden(iter->second.first,
iter->second.second);
// Discard may destroy the associated WebContents or replace the
// WebContent's primary document with an empty docoument. For the latter
// case the source id must be invalidated as the UKM source should no longer
// be considered valid.
iter->second.first = ukm::kInvalidSourceId;
}
}
void TabUsageScenarioTracker::OnTabInteraction(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
usage_scenario_data_store_->OnUserInteraction();
}
void TabUsageScenarioTracker::OnTabIsAudibleChanged(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (web_contents->IsCurrentlyAudible()) {
usage_scenario_data_store_->OnAudioStarts();
} else {
usage_scenario_data_store_->OnAudioStops();
}
}
void TabUsageScenarioTracker::OnMediaEffectivelyFullscreenChanged(
content::WebContents* web_contents,
bool is_fullscreen) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!last_num_displays_.has_value()) {
last_num_displays_ = GetNumDisplays();
}
// Use `last_num_displays_` instead of `GetNumDisplays()` below, to let
// `OnNumDisplaysChanged()` handle changes in the number of displays.
if (is_fullscreen) {
auto [it, inserted] =
contents_playing_video_fullscreen_.insert(web_contents);
if (inserted && contents_playing_video_fullscreen_.size() == 1U &&
last_num_displays_.value() == 1) {
usage_scenario_data_store_->OnFullScreenVideoStartsOnSingleMonitor();
}
} else {
auto num_removed = contents_playing_video_fullscreen_.erase(web_contents);
if (num_removed == 1U && contents_playing_video_fullscreen_.empty() &&
last_num_displays_.value() == 1) {
usage_scenario_data_store_->OnFullScreenVideoEndsOnSingleMonitor();
}
}
}
void TabUsageScenarioTracker::OnMediaDestroyed(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Return early if the tab is being destroyed. It will be removed from
// `contents_playing_video_fullscreen_` by
// TabUsageScenarioTracker::OnWebContentsRemoved().
//
// This is an unfortunate workaround for a crash (crbug.com/1393544) that
// occurs when a Browser that still contains WebContents is destroyed,
// resulting in:
// 1. The Browser destroys its `exclusive_access_manager_`.
// 2. The Browser destroys its WebContents.
// 3. The WebContents destroys its frames.
// 4. The frame deletion calls TabUsageScenarioTracker::OnMediaDestroyed
// (this method).
// 5. This method calls HasActiveEffectivelyFullscreenVideo(), which ends
// up calling Browser::IsFullscreenForTabOrPending().
// 6. Browser::IsFullscreenForTabOrPending() accesses a deleted
// `exclusive_access_manager_`.
// According to a DCHECK in ~Browser
// (https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/browser.cc;l=575;drc=60e7a86ffafb2aafa40cb00214d6b813b41c6f75),
// a Browser's WebContents should be deleted before the Browser itself is
// deleted, but it looks like it's not always the case.
if (web_contents->IsBeingDestroyed())
return;
// Destroying a media may cause the WebContents to no longer have a fullscreen
// media.
OnMediaEffectivelyFullscreenChanged(
web_contents, web_contents->HasActiveEffectivelyFullscreenVideo());
}
void TabUsageScenarioTracker::OnPrimaryMainFrameNavigationCommitted(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
usage_scenario_data_store_->OnTopLevelNavigation();
if (web_contents->GetVisibility() == content::Visibility::VISIBLE) {
auto iter = visible_tabs_.find(web_contents);
CHECK(iter != visible_tabs_.end());
// If there's already an entry with a valid SourceID for this in
// |visible_tabs_| then it means that there's been a main frame navigation
// for a visible tab. Records that the SourceID previously associated with
// this tab isn't visible anymore.
if (iter->second.first != ukm::kInvalidSourceId) {
usage_scenario_data_store_->OnUkmSourceBecameHidden(iter->second.first,
iter->second.second);
}
iter->second = GetNavigationInfoForContents(web_contents);
if (iter->second.first != ukm::kInvalidSourceId) {
usage_scenario_data_store_->OnUkmSourceBecameVisible(
iter->second.first, iter->second.second,
GetExtensionsThatRanContentScriptsInWebContents(web_contents));
}
}
}
void TabUsageScenarioTracker::OnVideoStartedPlaying(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!base::Contains(contents_playing_video_, web_contents));
contents_playing_video_.insert(web_contents);
if (base::Contains(visible_tabs_, web_contents))
usage_scenario_data_store_->OnVideoStartsInVisibleTab();
}
void TabUsageScenarioTracker::OnVideoStoppedPlaying(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(base::Contains(contents_playing_video_, web_contents));
contents_playing_video_.erase(web_contents);
if (base::Contains(visible_tabs_, web_contents))
usage_scenario_data_store_->OnVideoStopsInVisibleTab();
}
void TabUsageScenarioTracker::OnDisplayAdded(const display::Display&) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnNumDisplaysChanged();
}
void TabUsageScenarioTracker::OnDisplaysRemoved(const display::Displays&) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
OnNumDisplaysChanged();
}
int TabUsageScenarioTracker::GetNumDisplays() {
auto* screen = display::Screen::GetScreen();
DCHECK(screen);
return screen->GetNumDisplays();
}
void TabUsageScenarioTracker::OnTabBecameHidden(
VisibleTabsMap::iterator* visible_tab_iter) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// If this tab is playing video then record that it became non visible.
content::WebContents* const web_contents = (*visible_tab_iter)->first;
if (base::Contains(contents_playing_video_, web_contents))
usage_scenario_data_store_->OnVideoStopsInVisibleTab();
// Record that the ukm::SourceID associated with this tab isn't visible
// anymore if necessary.
if ((*visible_tab_iter)->second.first != ukm::kInvalidSourceId) {
usage_scenario_data_store_->OnUkmSourceBecameHidden(
(*visible_tab_iter)->second.first, (*visible_tab_iter)->second.second);
}
visible_tabs_.erase(*visible_tab_iter);
usage_scenario_data_store_->OnWindowHidden();
}
void TabUsageScenarioTracker::OnWebContentsRemoved(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto iter = visible_tabs_.find(web_contents);
DCHECK_EQ(iter != visible_tabs_.end(),
web_contents->GetVisibility() == content::Visibility::VISIBLE);
// If |web_contents| is tracked in the list of visible WebContents then a
// synthetic visibility change event should be emitted.
if (iter != visible_tabs_.end())
OnTabBecameHidden(&iter);
// Remove |web_contents| from the list of contents playing video. If
// necessary, the data store was already informed that a video stopped playing
// in a visible tab in the OnTabBecameHidden() call above.
contents_playing_video_.erase(web_contents);
{
// Remove |web_contents| from the list of contents will fullscreen media and
// if necessary, inform the data store that there is no more fullscreen
// video playing on a single monitor.
size_t num_removed = contents_playing_video_fullscreen_.erase(web_contents);
if (num_removed == 1 && contents_playing_video_fullscreen_.empty() &&
last_num_displays_.has_value() && last_num_displays_.value() == 1) {
usage_scenario_data_store_->OnFullScreenVideoEndsOnSingleMonitor();
}
}
// If necessary, inform the data store that audio stopped.
if (web_contents->IsCurrentlyAudible())
usage_scenario_data_store_->OnAudioStops();
}
void TabUsageScenarioTracker::InsertContentsInMapOfVisibleTabs(
content::WebContents* web_contents) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
DCHECK(!base::Contains(visible_tabs_, web_contents));
auto iter = visible_tabs_.emplace(web_contents,
GetNavigationInfoForContents(web_contents));
if (iter.first->second.first != ukm::kInvalidSourceId) {
usage_scenario_data_store_->OnUkmSourceBecameVisible(
iter.first->second.first, iter.first->second.second,
GetExtensionsThatRanContentScriptsInWebContents(web_contents));
}
}
void TabUsageScenarioTracker::OnNumDisplaysChanged() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
// Multiple displays can be added or removed before OnDisplayAdded and
// OnDidRemoveDisplays are dispatched. It is therefore incorrect to assume
// that the number of displays has increased / decreased compared to
// `last_num_displays_` following a call to OnDisplayAdded/ OnDisplaysRemoved.
const int num_displays = GetNumDisplays();
if (!contents_playing_video_fullscreen_.empty()) {
// `last_num_displays_` is set when `contents_playing_video_fullscreen_`
// becomes non-empty.
CHECK(last_num_displays_.has_value());
if (num_displays == 1 && last_num_displays_ != 1) {
usage_scenario_data_store_->OnFullScreenVideoStartsOnSingleMonitor();
} else if (num_displays != 1 && last_num_displays_ == 1) {
usage_scenario_data_store_->OnFullScreenVideoEndsOnSingleMonitor();
}
}
last_num_displays_ = num_displays;
}
} // namespace metrics