blob: d81e0ee3466aa3ea84ae0e5b8e08fffbfcf546f1 [file] [log] [blame]
// Copyright 2025 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/glic/fre/glic_fre_controller.h"
#include <string>
#include <utility>
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_forward.h"
#include "base/memory/scoped_refptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/timer/elapsed_timer.h"
#include "base/version_info/channel.h"
#include "chrome/browser/background/glic/glic_launcher_configuration.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/glic/fre/fre_util.h"
#include "chrome/browser/glic/fre/glic_fre_dialog_view.h"
#include "chrome/browser/glic/glic_pref_names.h"
#include "chrome/browser/glic/glic_profile_manager.h"
#include "chrome/browser/glic/host/auth_controller.h"
#include "chrome/browser/glic/host/glic.mojom.h"
#include "chrome/browser/glic/public/glic_enabling.h"
#include "chrome/browser/glic/public/glic_keyed_service.h"
#include "chrome/browser/glic/public/glic_keyed_service_factory.h"
#include "chrome/browser/predictors/loading_predictor.h"
#include "chrome/browser/predictors/loading_predictor_factory.h"
#include "chrome/browser/shell_integration.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_finder.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/tabs/public/tab_dialog_manager.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/common/channel_info.h"
#include "chrome/common/chrome_features.h"
#include "components/google/core/common/google_util.h"
#include "components/prefs/pref_service.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/web_contents.h"
namespace glic {
GlicFreController::GlicFreController(Profile* profile,
signin::IdentityManager* identity_manager)
: profile_(profile),
auth_controller_(profile, identity_manager, /*use_for_fre=*/true) {}
GlicFreController::~GlicFreController() = default;
void GlicFreController::WebUiStateChanged(mojom::FreWebUiState new_state) {
if (webui_state_ == new_state) {
return;
}
if (new_state == mojom::FreWebUiState::kReady) {
base::RecordAction(base::UserMetricsAction("Glic.Fre.LoadSuccess"));
interaction_timer_.emplace();
}
// UI State has changed
webui_state_ = new_state;
webui_state_callback_list_.Notify(webui_state_);
// It is possible for the FRE to open directly in an error state. In this
// case, we should not record the FRE load time metric if the content is
// loaded at a later point.
if (new_state == mojom::FreWebUiState::kError ||
new_state == mojom::FreWebUiState::kOffline) {
presentation_timer_.reset();
}
RecordMetricsIfDialogIsShowingAndReady();
}
base::CallbackListSubscription GlicFreController::AddWebUiStateChangedCallback(
WebUiStateChangedCallback callback) {
return webui_state_callback_list_.Add(std::move(callback));
}
void GlicFreController::Shutdown() {
DismissFre(webui_state_);
}
bool GlicFreController::ShouldShowFreDialog() {
// If the given profile has not previously completed the FRE and is eligible,
// then it should be shown.
return GlicEnabling::IsEnabledForProfile(profile_) &&
!GlicEnabling::HasConsentedForProfile(profile_);
}
bool GlicFreController::CanShowFreDialog(Browser* browser) {
// The FRE can only be shown given a valid browser. If there is no browser,
// then an OS-level entrypoint is being used, which should not be possible
// before the FRE has been accepted.
if (!browser) {
return false;
}
// If there is a browser, the FRE can only be shown if no other modal is
// currently being shown on the same tab.
tabs::TabInterface* tab = browser->GetActiveTabInterface();
return tab && tab->CanShowModalUI();
}
void GlicFreController::OpenFreDialogInNewTab(BrowserWindowInterface* bwi,
mojom::InvocationSource source) {
Browser* browser = bwi->GetBrowserForMigrationOnly();
if (!ShouldShowFreDialog()) {
return;
}
chrome::AddAndReturnTabAt(browser, GURL(), /*index=*/-1, /*foreground=*/true);
if (CanShowFreDialog(browser)) {
ShowFreDialog(browser, source);
}
}
void GlicFreController::ShowFreDialog(Browser* browser,
mojom::InvocationSource source) {
CHECK(CanShowFreDialog(browser));
presentation_timer_.emplace();
open_timer_.emplace();
profile_->GetPrefs()->SetInteger(
prefs::kGlicCompletedFre,
static_cast<int>(prefs::FreStatus::kIncomplete));
if (auth_controller_.CheckAuthBeforeShowSync(
base::BindOnce(&GlicFreController::OpenFreDialogInNewTab,
GetWeakPtr(), browser, source))) {
ShowFreDialogAfterAuthCheck(browser->AsWeakPtr(), source);
} else {
// Sign-in required and handled by AuthController. In this case, do not
// record the FRE load time metric.
base::RecordAction(
base::UserMetricsAction("Glic.Fre.CheckAuthBeforeShowSync"));
presentation_timer_.reset();
}
}
void GlicFreController::ShowFreDialogAfterAuthCheck(
base::WeakPtr<Browser> browser,
mojom::InvocationSource source) {
// Abort if the browser was closed, to avoid crashing. Note, the user
// shouldn't have much chance to close the browser between ShowFreDialog() and
// ShowFreDialogAfterAuthCheck().
if (!browser) {
return;
}
// Close any existing FRE dialog before showing.
if (IsShowingDialog()) {
DismissFre(webui_state_);
}
source_browser_ = browser.get();
base::ElapsedTimer widget_creation_timer;
CreateView();
tab_showing_modal_ = browser->GetActiveTabInterface();
// Note that this call to `CreateShowDialogAndBlockTabInteraction` is
// necessarily preceded by a call to `CanShowModalUI`. See
// `GlicFreController::CanShowFreDialog`.
// TODO(crbug.com/393400004): This returned widget should be configured to
// use a synchronous close.
fre_widget_ = tab_showing_modal_->GetTabFeatures()
->tab_dialog_manager()
->CreateTabScopedDialog(fre_view_.release());
auto params = std::make_unique<tabs::TabDialogManager::Params>();
// Don't close the dialog on navigations, as the FRE is a web-based dialog
// and can have its own internal navigations.
params->close_on_navigate = false;
// Don't close the dialog on tab detach, as we have custom logic to handle
// this in OnTabShowingModalWillDetach() which includes metrics.
params->close_on_detach = false;
tab_showing_modal_->GetTabFeatures()->tab_dialog_manager()->ShowDialog(
fre_widget_.get(), std::move(params));
GetWebContents()->Focus();
will_detach_subscription_ = tab_showing_modal_->RegisterWillDetach(
base::BindRepeating(&GlicFreController::OnTabShowingModalWillDetach,
base::Unretained(this)));
fre_widget_->MakeCloseSynchronous(base::BindOnce(
&GlicFreController::CloseWithReason, base::Unretained(this)));
base::RecordAction(base::UserMetricsAction("Glic.Fre.Shown"));
base::UmaHistogramEnumeration("Glic.FRE.InvocationSource", source);
base::UmaHistogramMediumTimes("Glic.Fre.WidgetCreationTime",
widget_creation_timer.Elapsed());
webui_framework_load_timer_.emplace();
auth_controller_.OnGlicWindowOpened();
// Recording the load latency time when FRE contents were preloaded.
RecordMetricsIfDialogIsShowingAndReady();
}
void GlicFreController::DismissFreIfOpenOnActiveTab(Browser* browser) {
if (!browser) {
return;
}
tabs::TabInterface* tab = browser->GetActiveTabInterface();
// If the FRE is being shown on the current tab, close it.
if (fre_widget_ && tab_showing_modal_ == tab) {
base::RecordAction(base::UserMetricsAction("Glic.Fre.CloseWithToggle"));
DismissFre(webui_state_);
}
}
void GlicFreController::AcceptFre() {
accepted_ = true;
if (open_timer_) {
base::UmaHistogramMediumTimes("Glic.Fre.TotalTime.Accepted",
open_timer_->Elapsed());
open_timer_.reset();
}
if (interaction_timer_) {
base::UmaHistogramTimes("Glic.Fre.InteractionTime.Accepted",
interaction_timer_->Elapsed());
interaction_timer_.reset();
}
base::RecordAction(base::UserMetricsAction("Glic.Fre.Accept"));
// Update FRE related preferences.
profile_->GetPrefs()->SetInteger(
prefs::kGlicCompletedFre, static_cast<int>(prefs::FreStatus::kCompleted));
// Enable the launcher if it is still disabled by default and the browser
// is default or is on the stable channel.
bool is_enabled_default = false;
const bool is_launcher_enabled =
GlicLauncherConfiguration::IsEnabled(&is_enabled_default);
if (is_enabled_default && !is_launcher_enabled) {
base::MakeRefCounted<shell_integration::DefaultBrowserWorker>()
->StartCheckIsDefault(
base::BindOnce(&GlicFreController::OnCheckIsDefaultBrowserFinished,
chrome::GetChannel()));
}
// Dismiss the FRE window and then show the Glic panel, but store source
// browser before it is cleared.
Browser* source_browser = source_browser_;
DismissFre(webui_state_);
// Show a glic window attached to the invocation source browser.
if (source_browser) {
GlicKeyedServiceFactory::GetGlicKeyedService(profile_)->ToggleUI(
source_browser, /*prevent_close=*/true, mojom::InvocationSource::kFre);
}
}
void GlicFreController::RejectFre() {
base::RecordAction(base::UserMetricsAction("Glic.Fre.NoThanks"));
if (open_timer_) {
base::UmaHistogramMediumTimes("Glic.Fre.TotalTime.NoThanks",
open_timer_->Elapsed());
open_timer_.reset();
}
if (interaction_timer_) {
base::UmaHistogramTimes("Glic.Fre.InteractionTime.NoThanks",
interaction_timer_->Elapsed());
interaction_timer_.reset();
}
DismissFre(webui_state_);
}
void GlicFreController::CloseWithReason(views::Widget::ClosedReason reason) {
base::UmaHistogramEnumeration("Glic.Fre.WidgetClosedReason", reason);
switch (reason) {
case views::Widget::ClosedReason::kAcceptButtonClicked:
case views::Widget::ClosedReason::kCancelButtonClicked:
case views::Widget::ClosedReason::kEscKeyPressed:
case views::Widget::ClosedReason::kUnspecified:
break;
case views::Widget::ClosedReason::kLostFocus:
base::RecordAction(
base::UserMetricsAction("Glic.Fre.CloseByClickOutside"));
break;
case views::Widget::ClosedReason::kCloseButtonClicked:
base::RecordAction(base::UserMetricsAction("Glic.Fre.CloseWithX"));
break;
}
DismissFre(webui_state_);
}
void GlicFreController::DismissFre(mojom::FreWebUiState panel) {
if (open_timer_ && !accepted_) {
base::UmaHistogramMediumTimes("Glic.Fre.TotalTime.Dismissed",
open_timer_->Elapsed());
open_timer_.reset();
}
if (interaction_timer_ && !accepted_) {
base::UmaHistogramTimes("Glic.Fre.InteractionTime.Dismissed",
interaction_timer_->Elapsed());
interaction_timer_.reset();
}
if (IsShowingDialog()) {
switch (panel) {
case mojom::FreWebUiState::kError:
base::RecordAction(
base::UserMetricsAction("Glic.Fre.ErrorPanelClosed"));
break;
case mojom::FreWebUiState::kDisabledByAdmin:
base::RecordAction(
base::UserMetricsAction("Glic.Fre.DisabledByAdminPanelClosed"));
break;
case mojom::FreWebUiState::kOffline:
base::RecordAction(
base::UserMetricsAction("Glic.Fre.OfflinePanelClosed"));
break;
case mojom::FreWebUiState::kBeginLoading:
case mojom::FreWebUiState::kShowLoading:
case mojom::FreWebUiState::kHoldLoading:
case mojom::FreWebUiState::kFinishLoading:
base::RecordAction(
base::UserMetricsAction("Glic.Fre.LoadingPanelClosed"));
break;
case mojom::FreWebUiState::kReady:
base::RecordAction(
base::UserMetricsAction("Glic.Fre.ReadyPanelClosed"));
break;
case mojom::FreWebUiState::kUninitialized:
base::RecordAction(
base::UserMetricsAction("Glic.Fre.UninitializedPanelClosed"));
break;
}
}
web_contents_ = nullptr;
source_browser_ = nullptr;
if (fre_view_ || fre_widget_) {
auto* service = GlicKeyedServiceFactory::GetGlicKeyedService(profile_);
glic::GlicProfileManager* glic_profile_manager =
glic::GlicProfileManager::GetInstance();
if (glic_profile_manager) {
glic_profile_manager->OnUnloadingClientForService(service);
}
}
if (fre_widget_) {
base::UmaHistogramEnumeration("Glic.FreModalWebUiState.FinishState2",
webui_state_);
fre_widget_.reset();
tab_showing_modal_ = nullptr;
will_detach_subscription_ = {};
presentation_timer_.reset();
webui_framework_load_timer_.reset();
web_client_load_timer_.reset();
}
fre_view_.reset();
}
void GlicFreController::PrepareForClient(
base::OnceCallback<void(bool)> callback) {
auth_controller_.CheckAuthBeforeLoad(
base::BindOnce([](mojom::PrepareForClientResult result) {
switch (result) {
case mojom::PrepareForClientResult::kErrorResyncingCookies:
base::UmaHistogramEnumeration(
"Glic.FreErrorStateReason",
FreErrorStateReason::kErrorResyncingCookies);
break;
case mojom::PrepareForClientResult::kRequiresSignIn:
base::UmaHistogramEnumeration("Glic.FreErrorStateReason",
FreErrorStateReason::kSignInRequired);
break;
case mojom::PrepareForClientResult::kSuccess:
break;
}
return result == mojom::PrepareForClientResult::kSuccess;
}).Then(std::move(callback)));
}
void GlicFreController::ExceededTimeoutError() {
base::UmaHistogramEnumeration("Glic.FreErrorStateReason",
FreErrorStateReason::kTimeoutExceeded);
}
void GlicFreController::OnLinkClicked(const GURL& url) {
if (url.DomainIs("support.google.com")) {
if (url.path().find("13594961") != std::string::npos) {
base::RecordAction(
base::UserMetricsAction("Glic.Fre.PrivacyNoticeLinkOpened"));
} else {
base::RecordAction(
base::UserMetricsAction("Glic.Fre.HelpCenterLinkOpened"));
}
return;
}
if (url.DomainIs("policies.google.com")) {
base::RecordAction(base::UserMetricsAction("Glic.Fre.PolicyLinkOpened"));
return;
}
if (url.DomainIs("myactivity.google.com")) {
base::RecordAction(
base::UserMetricsAction("Glic.Fre.MyActivityLinkOpened"));
return;
}
}
void GlicFreController::TryPreload() {
// Callers should not attempt to preload if the widget is showing.
CHECK(!fre_widget_);
if (fre_view_ || auth_controller_.RequiresSignIn()) {
return;
}
CreateView();
}
bool GlicFreController::IsWarmed() const {
return !!fre_view_;
}
content::WebContents* GlicFreController::GetWebContents() {
return web_contents_;
}
namespace {
// TODO(jbroman): This should be updated with more specifics once more
// information about Glic is available, with updated strings and policy details.
constexpr net::NetworkTrafficAnnotationTag kGlicFrePreconnectTrafficAnnotation =
net::DefineNetworkTrafficAnnotation("glic_fre_preconnect",
R"(
semantics {
sender: "Glic FRE Preconnect"
description:
"This request is issued when the Glic first-run experience is "
"predicted to be issued soon, to establish a connection to the "
"server."
trigger:
"Hovering or focusing the Glic button."
data:
"Minimal data is exchanged, though this may share network state "
"with credentialed requests."
destination: GOOGLE_OWNED_SERVICE
internal {
contacts {
owners: "//chrome/browser/glic/OWNERS"
}
}
user_data {
type: NONE
}
last_reviewed: "2025-02-26"
}
policy {
cookies_allowed: YES
cookies_store: "user"
setting:
"There are a number of ways to prevent this request:"
"A) Disable predictive operations under Settings > Performance "
" > Preload pages for faster browsing and searching,"
"B) Disable Glic altogether"
chrome_policy {
URLBlocklist {
URLBlocklist: { entries: '*' }
}
}
chrome_policy {
URLAllowlist {
URLAllowlist { }
}
}
}
comments:
"This feature can be safely disabled, but enabling it may result in "
"faster load of the Glic first-run experience. Using either "
"URLBlocklist or URLAllowlist policies (or a combination of both) "
"limits the scope of these requests."
)");
BASE_FEATURE(kGlicFrePreconnect, base::FEATURE_ENABLED_BY_DEFAULT);
BASE_FEATURE_PARAM(bool,
kGlicFrePreconnectToSubresourceDomains,
&kGlicFrePreconnect,
"GlicFrePreconnectToSubresourceDomains",
true);
} // namespace
void GlicFreController::MaybePreconnect() {
if (!ShouldShowFreDialog() ||
!base::FeatureList::IsEnabled(kGlicFrePreconnect)) {
return;
}
GURL fre_url = glic::GetFreURL(profile_);
// We'll need this to be in the "same-site" socket pool for the FRE's site,
// since that's the one that will be used for a real page load.
net::NetworkAnonymizationKey anonymization_key =
net::NetworkAnonymizationKey::CreateSameSite(net::SchemefulSite(fre_url));
predictors::LoadingPredictor* loading_predictor =
predictors::LoadingPredictorFactory::GetForProfile(profile_);
content::StoragePartitionConfig storage_partition_config =
GetFreStoragePartitionConfig(profile_);
loading_predictor->PreconnectURLIfAllowed(
glic::GetFreURL(profile_), /*allow_credentials=*/true, anonymization_key,
kGlicFrePreconnectTrafficAnnotation, &storage_partition_config);
if (kGlicFrePreconnectToSubresourceDomains.Get() &&
google_util::IsGoogleDomainUrl(fre_url, google_util::ALLOW_SUBDOMAIN,
google_util::ALLOW_NON_STANDARD_PORTS)) {
loading_predictor->PreconnectURLIfAllowed(
GURL("https://www.gstatic.com/"), /*allow_credentials=*/true,
anonymization_key, kGlicFrePreconnectTrafficAnnotation,
&storage_partition_config);
}
}
// static
void GlicFreController::OnCheckIsDefaultBrowserFinished(
version_info::Channel channel,
shell_integration::DefaultWebClientState state) {
// Don't do anything because a different channel is the default browser
if (state ==
shell_integration::DefaultWebClientState::OTHER_MODE_IS_DEFAULT) {
return;
}
// Enables the launcher if the current browser is the default or
// is on the stable channel.
if (g_browser_process &&
(state == shell_integration::DefaultWebClientState::IS_DEFAULT ||
channel == version_info::Channel::STABLE)) {
g_browser_process->local_state()->SetBoolean(prefs::kGlicLauncherEnabled,
true);
}
}
void GlicFreController::OnTabShowingModalWillDetach(
tabs::TabInterface* tab,
tabs::TabInterface::DetachReason reason) {
switch (reason) {
case tabs::TabInterface::DetachReason::kDelete:
base::RecordAction(
base::UserMetricsAction("Glic.Fre.CloseByClosingHostTab"));
break;
case tabs::TabInterface::DetachReason::kInsertIntoOtherWindow:
base::RecordAction(
base::UserMetricsAction("Glic.Fre.CloseByMovingHostTab"));
break;
}
CloseWithReason(views::Widget::ClosedReason::kUnspecified);
}
void GlicFreController::CreateView() {
if (fre_view_) {
return;
}
fre_view_ = std::make_unique<GlicFreDialogView>(profile_, this);
web_contents_ = fre_view_->web_contents();
web_contents_->Resize(gfx::Rect(GetFreInitialSize()));
auto* service = GlicKeyedServiceFactory::GetGlicKeyedService(profile_);
GlicProfileManager::GetInstance()->OnLoadingClientForService(service);
}
void GlicFreController::RecordMetricsIfDialogIsShowingAndReady() {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
if (!!fre_widget_ && webui_state_ == mojom::FreWebUiState::kReady &&
presentation_timer_ && web_client_load_timer_) {
base::UmaHistogramMediumTimes("Glic.Fre.WebClientLoadTime",
web_client_load_timer_->Elapsed());
base::UmaHistogramMediumTimes("Glic.FrePresentationTime",
presentation_timer_->Elapsed());
presentation_timer_.reset();
web_client_load_timer_.reset();
}
}
void GlicFreController::LogWebUiLoadComplete() {
if (webui_framework_load_timer_) {
base::UmaHistogramMediumTimes("Glic.Fre.WebUiFrameworkLoadTime",
webui_framework_load_timer_->Elapsed());
webui_framework_load_timer_.reset();
// The FRE webclient begins loading as soon as the web ui is loaded
// successfully.
web_client_load_timer_.emplace();
}
}
bool GlicFreController::IsShowingDialog() const {
if (is_showing_dialog_for_testing_.has_value()) {
return is_showing_dialog_for_testing_.value();
}
return !!fre_widget_;
}
bool GlicFreController::IsShowingDialogAndStateInitialized() const {
return !!fre_widget_ &&
(webui_state_ != mojom::FreWebUiState::kUninitialized);
}
gfx::Size GlicFreController::GetFreInitialSize() {
return gfx::Size(features::kGlicFreInitialWidth.Get(),
features::kGlicFreInitialHeight.Get());
}
void GlicFreController::UpdateFreWidgetSize(const gfx::Size& new_size) {
if (!fre_widget_) {
return;
}
fre_widget_->SetSize(new_size);
}
} // namespace glic