blob: 88619a0f9a805506bec3f30588d11780c1006aca [file] [log] [blame]
// Copyright (c) 2021 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/media/webrtc/media_stream_focus_delegate.h"
#include "build/build_config.h"
#if BUILDFLAG(IS_ANDROID)
#error "Unsupported on Android."
#endif // BUILDFLAG(IS_ANDROID)
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "chrome/browser/bad_message.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/desktop_capture.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "modules/desktop_capture/desktop_capture_options.h"
#include "third_party/webrtc/modules/desktop_capture/desktop_capturer.h"
namespace {
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class ConditionalFocusDecision {
kExplicitFocusCapturedSurface = 0,
kExplicitNoFocusChange = 1,
kMicrotaskClosedWindow = 2,
kBrowserSideTimerClosedWindow = 3,
kMaxValue = kBrowserSideTimerClosedWindow
};
using Decision = ConditionalFocusDecision;
// Readability-enhancing aliases.
constexpr bad_message::BadMessageReason MSFD_MULTIPLE_EXPLICIT_CALLS_TO_FOCUS =
bad_message::BadMessageReason::MSFD_MULTIPLE_EXPLICIT_CALLS_TO_FOCUS;
constexpr bad_message::BadMessageReason
MSFD_MULTIPLE_CLOSURES_OF_FOCUSABILITY_WINDOW = bad_message::
BadMessageReason::MSFD_MULTIPLE_CLOSURES_OF_FOCUSABILITY_WINDOW;
} // namespace
MediaStreamFocusDelegate::MediaStreamFocusDelegate(
content::WebContents* web_contents)
: capture_start_time_(base::TimeTicks::Now()) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!web_contents) {
return;
}
Browser* const browser = chrome::FindBrowserWithWebContents(web_contents);
if (!browser) {
return;
}
TabStripModel* const tab_strip_model = browser->tab_strip_model();
if (!tab_strip_model) {
return;
}
tab_strip_model->AddObserver(this);
capturing_web_contents_ = web_contents->GetWeakPtr();
}
MediaStreamFocusDelegate::~MediaStreamFocusDelegate() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
}
void MediaStreamFocusDelegate::SetFocus(const content::DesktopMediaID& media_id,
bool focus,
bool is_from_microtask,
bool is_from_timer) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!capturing_web_contents_) {
return;
}
if (!UpdateUMA(focus, is_from_microtask, is_from_timer)) {
return; // Render process killed off - |capturing_web_contents_| invalid.
}
if (!focus_window_of_opportunity_open_) {
return; // Too late.
}
focus_window_of_opportunity_open_ = false;
if (!focus) {
return; // Window of opportunity to change focus now closed - we're done.
}
if (!IsWidgetFocused()) {
// Capturing window not focused - likely the user has alt-tabbed away.
return;
}
if (media_id.type == content::DesktopMediaID::TYPE_WEB_CONTENTS) {
FocusTab(media_id);
} else if (media_id.type == content::DesktopMediaID::TYPE_WINDOW) {
FocusWindow(media_id);
}
}
void MediaStreamFocusDelegate::OnTabStripModelChanged(
TabStripModel* tab_strip_model,
const TabStripModelChange& change,
const TabStripSelectionChange& selection) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
focus_window_of_opportunity_open_ = false;
}
bool MediaStreamFocusDelegate::IsWidgetFocused() const {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(capturing_web_contents_); // Tested by caller.
content::RenderWidgetHostView* const rwhv =
capturing_web_contents_->GetRenderWidgetHostView();
if (!rwhv) {
return false;
}
return rwhv->HasFocus();
}
void MediaStreamFocusDelegate::FocusTab(
const content::DesktopMediaID& media_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
content::RenderFrameHost* const rfh = content::RenderFrameHost::FromID(
media_id.web_contents_id.render_process_id,
media_id.web_contents_id.main_render_frame_id);
if (!rfh) {
return;
}
content::WebContents* const web_contents =
content::WebContents::FromRenderFrameHost(rfh);
if (!web_contents) {
return;
}
content::WebContentsDelegate* const delegate = web_contents->GetDelegate();
if (!delegate) {
return;
}
delegate->ActivateContents(web_contents);
Browser* const browser = chrome::FindBrowserWithWebContents(web_contents);
if (browser && browser->window()) {
browser->window()->Activate();
}
}
void MediaStreamFocusDelegate::FocusWindow(
const content::DesktopMediaID& media_id) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
std::unique_ptr<webrtc::DesktopCapturer> window_capturer =
webrtc::DesktopCapturer::CreateWindowCapturer(
content::desktop_capture::CreateDesktopCaptureOptions());
if (window_capturer && window_capturer->SelectSource(media_id.id)) {
window_capturer->FocusOnSelectedSource();
}
}
bool MediaStreamFocusDelegate::UpdateUMA(bool focus,
bool is_from_microtask,
bool is_from_timer) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(capturing_web_contents_); // Tested by caller.
DCHECK(!is_from_microtask || !is_from_timer); // Can't be both.
const bool explicit_decision = (!is_from_microtask && !is_from_timer);
// Invocations from the microtask/timer focus the captured display surface.
DCHECK(explicit_decision || focus);
// The shape and result of the API invocation is only recorded once,
// on the first invocation that has an effect.
if (focus_window_of_opportunity_open_) {
base::UmaHistogramEnumeration(
"Media.ConditionalFocus.Decision",
is_from_microtask
? Decision::kMicrotaskClosedWindow
: is_from_timer ? Decision::kBrowserSideTimerClosedWindow
: focus ? Decision::kExplicitFocusCapturedSurface
: Decision::kExplicitNoFocusChange);
}
const base::TimeDelta delay = base::TimeTicks::Now() - capture_start_time_;
if (explicit_decision) {
if (explicit_decision_) {
return BadMessage(MSFD_MULTIPLE_EXPLICIT_CALLS_TO_FOCUS);
}
explicit_decision_ = true;
if (!microtask_fired_ && !timer_expired_) { // Timely API invocation.
// Record the delay of this on-time explicit API invocation.
// Note that 1s corresponds to the value GetConditionalFocusWindow()
// returns by default.
UMA_HISTOGRAM_CUSTOM_TIMES("Media.ConditionalFocus.ExplicitOnTimeCall",
delay, base::Milliseconds(1), base::Seconds(1),
100);
} else if (timer_expired_) { // Late (compared to browser-side timer).
// Record the delay of this late explicit API invocation.
// Note that 1s corresponds to the value GetConditionalFocusWindow()
// returns by default.
UMA_HISTOGRAM_CUSTOM_TIMES("Media.ConditionalFocus.ExplicitLateCall",
delay, base::Seconds(1), base::Seconds(5),
100);
} else { // microtask_fired_
// The case of |microtask_fired_| is not currently measured.
// It's an error on the Web-application's part and addressable by the app.
}
}
if (is_from_microtask) {
if (microtask_fired_) {
return BadMessage(MSFD_MULTIPLE_CLOSURES_OF_FOCUSABILITY_WINDOW);
}
UMA_HISTOGRAM_CUSTOM_TIMES("Media.ConditionalFocus.MicrotaskDelay", delay,
base::Milliseconds(1), base::Seconds(5), 100);
microtask_fired_ = true;
}
if (is_from_timer) {
DCHECK(!timer_expired_); // The timer can only expire once.
timer_expired_ = true;
}
return true;
}
bool MediaStreamFocusDelegate::BadMessage(
bad_message::BadMessageReason reason) {
content::RenderFrameHost* const rfh = capturing_web_contents_->GetMainFrame();
if (rfh) {
bad_message::ReceivedBadMessage(rfh->GetProcess(), reason);
}
return false;
}