blob: ee9ae8939d382bfce5ebfd89a11831ae6d9c19a2 [file] [log] [blame]
// Copyright 2024 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/glic_keyed_service.h"
#include <memory>
#include "base/check.h"
#include "base/command_line.h"
#include "base/containers/flat_set.h"
#include "base/feature_list.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/rand_util.h"
#include "base/task/sequenced_task_runner.h"
#include "base/time/time.h"
#include "base/types/expected.h"
#include "chrome/browser/actor/actor_keyed_service.h"
#include "chrome/browser/actor/browser_action_util.h"
#include "chrome/browser/actor/tools/tool_request.h"
#include "chrome/browser/actor/ui/actor_ui_state_manager_interface.h"
#include "chrome/browser/contextual_cueing/contextual_cueing_service.h"
#include "chrome/browser/contextual_cueing/contextual_cueing_service_factory.h"
#include "chrome/browser/glic/fre/glic_fre_controller.h"
#include "chrome/browser/glic/glic_enabling.h"
#include "chrome/browser/glic/glic_enums.h"
#include "chrome/browser/glic/glic_keyed_service_factory.h"
#include "chrome/browser/glic/glic_metrics.h"
#include "chrome/browser/glic/glic_occlusion_notifier.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/context/glic_screenshot_capturer.h"
#include "chrome/browser/glic/host/context/glic_sharing_manager_impl.h"
#include "chrome/browser/glic/host/context/glic_tab_data.h"
#include "chrome/browser/glic/host/glic.mojom.h"
#include "chrome/browser/glic/host/glic_actor_controller.h"
#include "chrome/browser/glic/host/host.h"
#include "chrome/browser/glic/host/webui_contents_container.h"
#include "chrome/browser/glic/widget/glic_widget.h"
#include "chrome/browser/glic/widget/glic_window_controller_impl.h"
#include "chrome/browser/global_features.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/browser_list.h"
#include "chrome/browser/ui/browser_navigator.h"
#include "chrome/browser/ui/browser_navigator_params.h"
#include "chrome/common/actor/action_result.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/chrome_switches.h"
#include "components/guest_view/browser/guest_view_base.h"
#include "components/optimization_guide/proto/features/actions_data.pb.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/common/url_constants.h"
#include "extensions/browser/guest_view/web_view/web_view_guest.h"
#include "mojo/public/cpp/base/proto_wrapper.h"
#include "mojo/public/cpp/bindings/callback_helpers.h"
#include "net/log/net_log_with_source.h"
#include "third_party/abseil-cpp/absl/strings/str_format.h"
#include "third_party/blink/public/common/web_preferences/web_preferences.h"
#include "ui/base/page_transition_types.h"
#include "ui/views/widget/widget.h"
#include "url/gurl.h"
namespace glic {
namespace {
base::TimeDelta GetWarmingDelay() {
base::TimeDelta delay_start =
base::Milliseconds(features::kGlicWarmingDelayMs.Get());
base::TimeDelta delay_limit =
delay_start + base::Milliseconds(features::kGlicWarmingJitterMs.Get());
if (delay_limit > delay_start) {
return RandTimeDelta(delay_start, delay_limit);
}
return delay_start;
}
// TODO(b/421426722): Use net::DefineNetworkTrafficAnnotationTag() to define the
// annotation, and remove this file from tools/traffic_annotation/safe_list.txt.
const net::NetworkTrafficAnnotationTag kGlicWebUITrafficAnnotation =
MISSING_TRAFFIC_ANNOTATION;
} // namespace
GlicKeyedService::GlicKeyedService(
Profile* profile,
signin::IdentityManager* identity_manager,
ProfileManager* profile_manager,
GlicProfileManager* glic_profile_manager,
contextual_cueing::ContextualCueingService* contextual_cueing_service)
: profile_(profile),
enabling_(std::make_unique<GlicEnabling>(
profile,
&profile_manager->GetProfileAttributesStorage())),
metrics_(std::make_unique<GlicMetrics>(profile, enabling_.get())),
host_(std::make_unique<Host>(profile)),
window_controller_(
std::make_unique<GlicWindowControllerImpl>(profile,
identity_manager,
this,
enabling_.get())),
sharing_manager_(
std::make_unique<GlicSharingManagerImpl>(profile,
window_controller_.get(),
host_.get(),
metrics_.get())),
screenshot_capturer_(std::make_unique<GlicScreenshotCapturer>()),
auth_controller_(std::make_unique<AuthController>(profile,
identity_manager,
/*use_for_fre=*/false)),
occlusion_notifier_(
std::make_unique<GlicOcclusionNotifier>(*window_controller_)),
zero_state_suggestions_manager_(
std::make_unique<GlicZeroStateSuggestionsManager>(
sharing_manager_.get(),
contextual_cueing_service,
host_.get())),
contextual_cueing_service_(contextual_cueing_service) {
CHECK(GlicEnabling::IsProfileEligible(Profile::FromBrowserContext(profile)));
host_->Initialize(window_controller_.get());
metrics_->SetControllers(window_controller_.get(), sharing_manager_.get());
memory_pressure_listener_ = std::make_unique<base::MemoryPressureListener>(
FROM_HERE, base::BindRepeating(&GlicKeyedService::OnMemoryPressure,
weak_ptr_factory_.GetWeakPtr()));
// If `--glic-always-open-fre` is present, unset this pref to ensure the FRE
// is shown for testing convenience.
auto* command_line = base::CommandLine::ForCurrentProcess();
if (command_line->HasSwitch(::switches::kGlicAlwaysOpenFre)) {
profile_->GetPrefs()->SetInteger(
prefs::kGlicCompletedFre,
static_cast<int>(prefs::FreStatus::kNotStarted));
// or if automation is enabled, skip FRE
} else if (command_line->HasSwitch(::switches::kGlicAutomation)) {
profile_->GetPrefs()->SetInteger(
prefs::kGlicCompletedFre,
static_cast<int>(prefs::FreStatus::kCompleted));
}
if (base::FeatureList::IsEnabled(features::kGlicActor)) {
actor_controller_ = std::make_unique<GlicActorController>(profile_);
}
// This is only used by automation for tests.
glic_profile_manager->MaybeAutoOpenGlicPanel();
}
GlicKeyedService::~GlicKeyedService() {
host().Destroy();
metrics_->SetControllers(nullptr, nullptr);
}
// static
GlicKeyedService* GlicKeyedService::Get(content::BrowserContext* context) {
return GlicKeyedServiceFactory::GetGlicKeyedService(context);
}
void GlicKeyedService::Shutdown() {
CloseUI();
GlicProfileManager* glic_profile_manager = GlicProfileManager::GetInstance();
if (glic_profile_manager) {
glic_profile_manager->OnServiceShutdown(this);
}
}
void GlicKeyedService::ToggleUI(BrowserWindowInterface* bwi,
bool prevent_close,
mojom::InvocationSource source) {
// Glic may be disabled for certain user profiles (the user is browsing in
// incognito or guest mode, policy, etc). In those cases, the entry points to
// this method should already have been removed.
CHECK(GlicEnabling::IsEnabledForProfile(profile_));
GlicProfileManager* glic_profile_manager = GlicProfileManager::GetInstance();
if (glic_profile_manager) {
glic_profile_manager->SetActiveGlic(this);
}
window_controller_->Toggle(bwi, prevent_close, source);
}
void GlicKeyedService::OpenFreDialogInNewTab(BrowserWindowInterface* bwi,
mojom::InvocationSource source) {
// Glic may be disabled for certain user profiles (the user is browsing in
// incognito or guest mode, policy, etc). In those cases, the entry points to
// this method should already have been removed.
CHECK(GlicEnabling::IsEnabledForProfile(profile_));
GlicProfileManager* glic_profile_manager = GlicProfileManager::GetInstance();
if (glic_profile_manager) {
glic_profile_manager->SetActiveGlic(this);
}
window_controller_->fre_controller()->OpenFreDialogInNewTab(bwi, source);
}
void GlicKeyedService::CloseUI() {
window_controller_->Shutdown();
host().Shutdown();
SetContextAccessIndicator(false);
}
void GlicKeyedService::PrepareForOpen() {
window_controller_->fre_controller()->MaybePreconnect();
auto* active_web_contents =
sharing_manager_->GetFocusedTabData().focus()
? sharing_manager_->GetFocusedTabData().focus()->GetContents()
: nullptr;
if (contextual_cueing_service_ && active_web_contents) {
contextual_cueing_service_
->PrepareToFetchContextualGlicZeroStateSuggestions(active_web_contents);
}
}
void GlicKeyedService::OnZeroStateSuggestionsFetched(
mojom::ZeroStateSuggestionsPtr suggestions,
mojom::WebClientHandler::GetZeroStateSuggestionsForFocusedTabCallback
callback,
std::optional<std::vector<std::string>> returned_suggestions) {
std::vector<mojom::SuggestionContentPtr> output_suggestions;
if (returned_suggestions) {
for (const std::string& suggestion_string : returned_suggestions.value()) {
output_suggestions.push_back(
mojom::SuggestionContent::New(suggestion_string));
}
suggestions->suggestions = std::move(output_suggestions);
}
std::move(callback).Run(std::move(suggestions));
}
void GlicKeyedService::FetchZeroStateSuggestions(
bool is_first_run,
std::optional<std::vector<std::string>> supported_tools,
mojom::WebClientHandler::GetZeroStateSuggestionsForFocusedTabCallback
callback) {
auto* active_web_contents =
sharing_manager_->GetFocusedTabData().focus()
? sharing_manager_->GetFocusedTabData().focus()->GetContents()
: nullptr;
if (contextual_cueing_service_ && active_web_contents && IsWindowShowing()) {
auto suggestions = mojom::ZeroStateSuggestions::New();
suggestions->tab_id = GetTabId(active_web_contents);
suggestions->tab_url = active_web_contents->GetLastCommittedURL();
contextual_cueing_service_
->GetContextualGlicZeroStateSuggestionsForFocusedTab(
active_web_contents, is_first_run, supported_tools,
mojo::WrapCallbackWithDefaultInvokeIfNotRun(
base::BindOnce(&GlicKeyedService::OnZeroStateSuggestionsFetched,
GetWeakPtr(), std::move(suggestions),
std::move(callback)),
std::nullopt));
} else {
std::move(callback).Run(nullptr);
}
}
GlicWindowController& GlicKeyedService::window_controller() {
CHECK(window_controller_);
return *window_controller_.get();
}
GlicSharingManager& GlicKeyedService::sharing_manager() {
return *sharing_manager_.get();
}
void GlicKeyedService::GuestAdded(content::WebContents* guest_contents) {
host().GuestAdded(guest_contents);
}
bool GlicKeyedService::IsWindowShowing() const {
return window_controller_->IsShowing();
}
bool GlicKeyedService::IsWindowDetached() const {
return window_controller_->IsDetached();
}
bool GlicKeyedService::IsWindowOrFreShowing() const {
return window_controller_->IsShowing() ||
window_controller_->fre_controller()->IsShowingDialog();
}
base::CallbackListSubscription
GlicKeyedService::AddContextAccessIndicatorStatusChangedCallback(
ContextAccessIndicatorChangedCallback callback) {
return context_access_indicator_callback_list_.Add(std::move(callback));
}
void GlicKeyedService::CreateTab(
const ::GURL& url,
bool open_in_background,
const std::optional<int32_t>& window_id,
glic::mojom::WebClientHandler::CreateTabCallback callback) {
// If we need to open other URL types, it should be done in a more specific
// function.
if (!url.SchemeIsHTTPOrHTTPS()) {
std::move(callback).Run(nullptr);
return;
}
NavigateParams params(profile_, url, ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
params.disposition = open_in_background
? WindowOpenDisposition::NEW_BACKGROUND_TAB
: WindowOpenDisposition::NEW_FOREGROUND_TAB;
base::WeakPtr<content::NavigationHandle> navigation_handle =
Navigate(&params);
if (!navigation_handle.get()) {
std::move(callback).Run(nullptr);
return;
}
// Right after requesting the navigation, the WebContents will have almost no
// information to populate TabData, hence the overriding of the URL. Should we
// ever want to send more data back to the web client, we should wait until
// the navigation commits.
mojom::TabDataPtr tab_data =
CreateTabData(navigation_handle.get()->GetWebContents());
if (tab_data) {
tab_data->url = url;
}
std::move(callback).Run(std::move(tab_data));
}
void GlicKeyedService::ClosePanel() {
window_controller_->Close();
SetContextAccessIndicator(false);
screenshot_capturer_->CloseScreenPicker();
}
void GlicKeyedService::AttachPanel() {
window_controller_->Attach();
}
void GlicKeyedService::DetachPanel() {
window_controller_->Detach();
}
void GlicKeyedService::ResizePanel(const gfx::Size& size,
base::TimeDelta duration,
base::OnceClosure callback) {
window_controller_->Resize(size, duration, std::move(callback));
}
void GlicKeyedService::SetPanelDraggableAreas(
const std::vector<gfx::Rect>& draggable_areas) {
window_controller_->SetDraggableAreas(draggable_areas);
}
void GlicKeyedService::SetContextAccessIndicator(bool show) {
if (is_context_access_indicator_enabled_ == show) {
return;
}
is_context_access_indicator_enabled_ = show;
context_access_indicator_callback_list_.Notify(show);
}
void GlicKeyedService::CreateTask(
mojom::WebClientHandler::CreateTaskCallback callback) {
if (!base::FeatureList::IsEnabled(features::kGlicActor)) {
std::move(callback).Run(
base::unexpected(mojom::CreateTaskErrorReason::kTaskSystemUnavailable));
return;
}
actor::TaskId task_id = actor::ActorKeyedService::Get(profile_)->CreateTask();
std::move(callback).Run(task_id.value());
}
void PerformActionsFinished(
mojom::WebClientHandler::PerformActionsCallback callback,
actor::mojom::ActionResultCode result_code,
std::optional<size_t> index_of_failed_action) {
optimization_guide::proto::ActionsResult response =
actor::BuildActionsResult(result_code, index_of_failed_action);
std::move(callback).Run(mojo_base::ProtoWrapper(response));
}
void GlicKeyedService::PerformActions(
const std::vector<uint8_t>& actions_proto,
mojom::WebClientHandler::PerformActionsCallback callback) {
optimization_guide::proto::Actions actions;
if (!actions.ParseFromArray(actions_proto.data(), actions_proto.size())) {
std::move(callback).Run(
base::unexpected(mojom::PerformActionsErrorReason::kInvalidProto));
return;
}
if (!actions.has_task_id()) {
std::move(callback).Run(
base::unexpected(mojom::PerformActionsErrorReason::kMissingTaskId));
return;
}
actor::TaskId task_id(actions.task_id());
auto* actor_service = actor::ActorKeyedService::Get(profile_);
actor::BuildToolRequestResult requests = actor::BuildToolRequest(actions);
if (!requests.has_value()) {
actor_service->GetJournal().Log(
GURL::EmptyGURL(), task_id, "Act Failed",
absl::StrFormat("Failed to convert proto::Actions[%d] to ToolRequest",
requests.error()));
optimization_guide::proto::ActionsResult response =
actor::BuildActionsResult(
actor::mojom::ActionResultCode::kArgumentsInvalid,
requests.error());
std::move(callback).Run(mojo_base::ProtoWrapper(response));
return;
}
actor_service->PerformActions(
task_id, std::move(requests.value()),
base::BindOnce(PerformActionsFinished, std::move(callback)));
}
void GlicKeyedService::ActInFocusedTab(
const std::vector<uint8_t>& action_proto,
const mojom::GetTabContextOptions& options,
mojom::WebClientHandler::ActInFocusedTabCallback callback) {
CHECK(base::FeatureList::IsEnabled(features::kGlicActor));
optimization_guide::proto::BrowserAction action;
if (!action.ParseFromArray(action_proto.data(), action_proto.size())) {
mojom::ActInFocusedTabResultPtr result =
mojom::ActInFocusedTabResult::NewErrorReason(
mojom::ActInFocusedTabErrorReason::kInvalidActionProto);
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(callback), std::move(result)));
return;
}
CHECK(actor_controller_);
auto* actor_service = actor::ActorKeyedService::Get(profile_);
CHECK(actor_service);
actor::ActorTask* task = actor_service->GetMostRecentTask();
if (task && (!action.has_task_id() || action.task_id() == 0)) {
action.set_task_id(task->id().value());
}
actor_controller_->Act(action, options, std::move(callback));
}
// TODO(crbug.com/411462297): Stop/Pause/Resume task need to be routed to go
// through the ActorKeyedService, rather than the deprecated ActorController
// which ignores the task_id.
void GlicKeyedService::StopActorTask(actor::TaskId task_id) {
CHECK(base::FeatureList::IsEnabled(features::kGlicActor));
CHECK(actor_controller_);
actor_controller_->StopTask(task_id);
}
void GlicKeyedService::PauseActorTask(actor::TaskId task_id) {
CHECK(base::FeatureList::IsEnabled(features::kGlicActor));
CHECK(actor_controller_);
actor_controller_->PauseTask(task_id);
}
void GlicKeyedService::ResumeActorTask(
actor::TaskId task_id,
const mojom::GetTabContextOptions& context_options,
glic::mojom::WebClientHandler::ResumeActorTaskCallback callback) {
CHECK(base::FeatureList::IsEnabled(features::kGlicActor));
CHECK(actor_controller_);
actor_controller_->ResumeTask(task_id, context_options, std::move(callback));
}
void GlicKeyedService::OnUserInputSubmitted(glic::mojom::WebClientMode mode) {
metrics_->OnUserInputSubmitted(mode);
if (actor_controller_) {
actor_controller_->OnUserInputSubmitted();
}
}
void GlicKeyedService::OnRequestStarted() {
if (actor_controller_) {
actor_controller_->OnRequestStarted();
}
}
void GlicKeyedService::OnResponseStarted() {
metrics_->OnResponseStarted();
if (actor_controller_) {
actor_controller_->OnResponseStarted();
}
}
void GlicKeyedService::OnResponseStopped() {
metrics_->OnResponseStopped();
if (actor_controller_) {
actor_controller_->OnResponseStopped();
}
}
void GlicKeyedService::CaptureScreenshot(
mojom::WebClientHandler::CaptureScreenshotCallback callback) {
screenshot_capturer_->CaptureScreenshot(
window_controller_->GetGlicWidget()->GetNativeWindow(),
std::move(callback));
}
bool GlicKeyedService::IsContextAccessIndicatorShown(
const content::WebContents* contents) {
return is_context_access_indicator_enabled_ &&
sharing_manager_->GetFocusedTabData().focus() &&
sharing_manager_->GetFocusedTabData().focus()->GetContents() ==
contents;
}
void GlicKeyedService::AddPreloadCallback(base::OnceCallback<void()> callback) {
preload_callback_ = std::move(callback);
}
void GlicKeyedService::TryPreload() {
if (base::FeatureList::IsEnabled(features::kGlicDisableWarming) &&
!base::FeatureList::IsEnabled(features::kGlicWarming)) {
// This is to ensure the preload process completes and preload_callback_ is
// called.
FinishPreload(false);
return;
}
GlicProfileManager* glic_profile_manager = GlicProfileManager::GetInstance();
CHECK(glic_profile_manager);
base::TimeDelta delay = GetWarmingDelay();
// TODO(b/411100559): Ideally we'd use post delayed task in all cases,
// but this requires a refactor of tests that are currently brittle. For now,
// just synchronously call ShouldPreloadForProfile if there is no delay.
if (delay.is_zero()) {
glic_profile_manager->ShouldPreloadForProfile(
profile_,
base::BindOnce(&GlicKeyedService::FinishPreload, GetWeakPtr()));
} else {
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE,
base::BindOnce(&GlicKeyedService::TryPreloadAfterDelay, GetWeakPtr()),
delay);
}
}
void GlicKeyedService::TryPreloadAfterDelay() {
GlicProfileManager* glic_profile_manager = GlicProfileManager::GetInstance();
if (glic_profile_manager) {
glic_profile_manager->ShouldPreloadForProfile(
profile_,
base::BindOnce(&GlicKeyedService::FinishPreload, GetWeakPtr()));
}
}
void GlicKeyedService::TryPreloadFre() {
if (base::FeatureList::IsEnabled(features::kGlicDisableWarming) &&
!base::FeatureList::IsEnabled(features::kGlicFreWarming)) {
return;
}
GlicProfileManager* glic_profile_manager = GlicProfileManager::GetInstance();
CHECK(glic_profile_manager);
glic_profile_manager->ShouldPreloadFreForProfile(
profile_,
base::BindOnce(&GlicKeyedService::FinishPreloadFre, GetWeakPtr()));
}
void GlicKeyedService::Reload() {
window_controller().Reload();
}
void GlicKeyedService::OnMemoryPressure(
base::MemoryPressureListener::MemoryPressureLevel level) {
if (level == base::MemoryPressureListener::MemoryPressureLevel::
MEMORY_PRESSURE_LEVEL_NONE ||
(this == GlicProfileManager::GetInstance()->GetLastActiveGlic())) {
return;
}
CloseUI();
}
base::WeakPtr<GlicKeyedService> GlicKeyedService::GetWeakPtr() {
return weak_ptr_factory_.GetWeakPtr();
}
bool GlicKeyedService::IsActiveWebContents(content::WebContents* contents) {
if (!contents) {
return false;
}
return contents == host().webui_contents() ||
contents == window_controller().GetFreWebContents();
}
void GlicKeyedService::FinishPreload(bool should_preload) {
if (preload_callback_) {
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(std::move(preload_callback_)));
}
if (base::FeatureList::IsEnabled(features::kGlicWarming) && profile_ &&
GlicEnabling::IsEnabledAndConsentForProfile(profile_)) {
base::UmaHistogramBoolean("Glic.ShouldPreload", should_preload);
}
if (!should_preload) {
return;
}
window_controller_->Preload();
}
void GlicKeyedService::FinishPreloadFre(bool should_preload) {
if (!should_preload) {
return;
}
window_controller_->PreloadFre();
}
bool GlicKeyedService::IsProcessHostForGlic(
content::RenderProcessHost* process_host) {
auto* fre_contents = window_controller_->GetFreWebContents();
if (fre_contents) {
if (fre_contents->GetPrimaryMainFrame()->GetProcess() == process_host) {
return true;
}
}
return host().IsGlicWebUiHost(process_host);
}
bool GlicKeyedService::IsGlicWebUi(content::WebContents* web_contents) {
return host().IsGlicWebUi(web_contents);
}
void GlicKeyedService::LogDummyNetworkRequestForTrafficAnnotation(
const GURL& url) {
net::NetLogWithSource net_log =
net::NetLogWithSource::Make(net::NetLogSourceType::URL_REQUEST);
net_log.AddEvent(net::NetLogEventType::REQUEST_ALIVE, [&]() {
base::Value::Dict dict;
dict.Set("priority", "IDLE");
dict.Set("url", url.spec());
dict.Set("traffic_annotation",
kGlicWebUITrafficAnnotation.unique_id_hash_code);
dict.Set("dummy_request", true);
return dict;
});
}
} // namespace glic