blob: 06ac5cc5c98ea591529f13534f710159caf6860a [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/ui/lens/lens_overlay_entry_point_controller.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/system/sys_info.h"
#include "chrome/browser/autocomplete/aim_eligibility_service_factory.h"
#include "chrome/browser/command_updater.h"
#include "chrome/browser/lens/region_search/lens_region_search_controller.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service.h"
#include "chrome/browser/optimization_guide/optimization_guide_keyed_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/search/search.h"
#include "chrome/browser/search_engines/template_url_service_factory.h"
#include "chrome/browser/ui/actions/chrome_action_id.h"
#include "chrome/browser/ui/browser_actions.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/lens/lens_overlay_controller.h"
#include "chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.h"
#include "chrome/browser/ui/lens/lens_search_controller.h"
#include "chrome/browser/ui/lens/lens_url_matcher.h"
#include "chrome/browser/ui/page_action/page_action_icon_type.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/user_education/browser_user_education_interface.h"
#include "chrome/browser/ui/views/page_action/page_action_controller.h"
#include "chrome/browser/ui/views/page_action/page_action_triggers.h"
#include "chrome/browser/ui/views/toolbar/pinned_toolbar_actions_container.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/browser/ui/webui/new_tab_page/new_tab_page_ui.h"
#include "chrome/browser/ui/webui/new_tab_page_third_party/new_tab_page_third_party_ui.h"
#include "chrome/browser/ui/webui/ntp/new_tab_ui.h"
#include "chrome/grit/branded_strings.h"
#include "components/lens/lens_features.h"
#include "components/lens/lens_overlay_permission_utils.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/omnibox_prefs.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/navigation_entry.h"
namespace {
// TODO(crbug.com/382494946): Similar bespoke checks are used throughout the
// codebase. This should be factored out as a common util and other callsites
// converted to use this.
bool IsNewTabPage(content::WebContents* const web_contents) {
// Use the committed entry (or the visible entry, if the committed entry is
// the initial NavigationEntry).
CHECK(web_contents);
content::NavigationEntry* entry =
web_contents->GetController().GetLastCommittedEntry();
if (entry->IsInitialEntry()) {
entry = web_contents->GetController().GetVisibleEntry();
}
const GURL& url = entry->GetURL();
return NewTabUI::IsNewTab(url) || NewTabPageUI::IsNewTabPageOrigin(url) ||
NewTabPageThirdPartyUI::IsNewTabPageOrigin(url) ||
search::NavEntryIsInstantNTP(web_contents, entry) ||
search::IsSplitViewNewTabPage(url);
}
} // namespace
namespace lens {
LensOverlayEntryPointController::LensOverlayEntryPointController() = default;
void LensOverlayEntryPointController::Initialize(
BrowserWindowInterface* browser_window_interface,
CommandUpdater* command_updater,
views::View* location_bar) {
browser_window_interface_ = browser_window_interface;
location_bar_ = location_bar;
if (location_bar_) {
focus_manager_observation_.Observe(location_bar_->GetFocusManager());
location_bar_->AddObserver(this);
}
pref_change_registrar_.Init(
browser_window_interface_->GetProfile()->GetPrefs());
pref_change_registrar_.Add(
omnibox::kShowGoogleLensShortcut,
base::BindRepeating(
&LensOverlayEntryPointController::UpdatePageActionState,
base::Unretained(this)));
command_updater_ = command_updater;
// Observe changes to fullscreen state.
fullscreen_observation_.Observe(
browser_window_interface_->GetExclusiveAccessManager()
->fullscreen_controller());
// Observe changes to user's DSE.
if (auto* const template_url_service =
TemplateURLServiceFactory::GetForProfile(
browser_window_interface_->GetProfile())) {
template_url_service_observation_.Observe(template_url_service);
}
// Update all entry points.
UpdateEntryPointsState(/*hide_if_needed=*/true);
edu_url_matcher_ = std::make_unique<lens::LensUrlMatcher>(
lens::features::GetLensOverlayEduUrlAllowFilters(),
lens::features::GetLensOverlayEduUrlBlockFilters(),
lens::features::GetLensOverlayEduUrlPathMatchAllowFilters(),
lens::features::GetLensOverlayEduUrlPathMatchBlockFilters(),
lens::features::GetLensOverlayEduUrlForceAllowedMatchPatterns(),
lens::features::GetLensOverlayEduHashedDomainBlockFilters());
if (lens::features::IsLensOverlayOptimizationFilterEnabled()) {
optimization_guide_decider_ =
OptimizationGuideKeyedServiceFactory::GetForProfile(
browser_window_interface_->GetProfile());
if (optimization_guide_decider_) {
optimization_guide_decider_->RegisterOptimizationTypes(
{optimization_guide::proto::OptimizationType::
LENS_OVERLAY_EDU_ACTION_CHIP_BLOCKLIST,
optimization_guide::proto::OptimizationType::
LENS_OVERLAY_EDU_ACTION_CHIP_ALLOWLIST});
}
}
}
LensOverlayEntryPointController::~LensOverlayEntryPointController() {
// Initialize may not have been called (e.g. for non-normal browser windows).
if (location_bar_) {
location_bar_->RemoveObserver(this);
}
}
bool LensOverlayEntryPointController::IsEnabled() const {
// This class is initialized if and only if it is observing.
if (!fullscreen_observation_.IsObserving()) {
return false;
}
// Feature is disabled via finch.
if (!lens::features::IsLensOverlayEnabled()) {
return false;
}
// Disable in fullscreen without top-chrome.
if (!lens::features::GetLensOverlayEnableInFullscreen() &&
browser_window_interface_->GetExclusiveAccessManager()
->context()
->IsFullscreen() &&
!browser_window_interface_->IsTabStripVisible()) {
return false;
}
const PrefService* pref_service =
browser_window_interface_->GetProfile()->GetPrefs();
// Lens Overlay is disabled via the enterprise policy.
lens::prefs::LensOverlaySettingsPolicyValue policy_value =
static_cast<lens::prefs::LensOverlaySettingsPolicyValue>(
pref_service->GetInteger(lens::prefs::kLensOverlaySettings));
if (policy_value ==
lens::prefs::LensOverlaySettingsPolicyValue::kDisabled) {
return false;
}
// Lens Overlay is only enabled if the user's default search engine is Google.
if (lens::features::IsLensOverlayGoogleDseRequired() &&
!search::DefaultSearchProviderIsGoogle(
browser_window_interface_->GetProfile())) {
return false;
}
// Finally, only enable the overlay if user meets our minimum RAM requirement.
static int phys_mem_mb = base::SysInfo::AmountOfPhysicalMemory().InMiB();
return phys_mem_mb > lens::features::GetLensOverlayMinRamMb();
}
bool LensOverlayEntryPointController::AreVisible() const {
return IsEnabled() && !IsOverlayActive();
}
void LensOverlayEntryPointController::UpdateEntryPointsState(
bool hide_toolbar_entrypoint) {
const bool enabled = IsEnabled();
const bool visible = AreVisible();
// Update the 3 dot menu entry point.
command_updater_->UpdateCommandEnabled(IDC_CONTENT_CONTEXT_LENS_OVERLAY,
visible);
// Update the pinnable toolbar entry point. Toolbar entry point is always
// present, therefore, ignore the visibility check.
if (auto* const toolbar_entry_point = GetToolbarEntrypoint()) {
toolbar_entry_point->SetEnabled(enabled);
if (hide_toolbar_entrypoint) {
toolbar_entry_point->SetVisible(enabled);
}
}
UpdatePageActionState();
// Update the homework action chip.
// TODO(crbug.com/433813408): Remove GetBrowserForMigrationOnly after Page
// Actions migration.
CHECK(browser_window_interface_);
browser_window_interface_->GetBrowserForMigrationOnly()
->window()
->UpdatePageActionIcon(PageActionIconType::kLensOverlayHomework);
}
bool LensOverlayEntryPointController::IsUrlEduEligible(const GURL& url) const {
if (!IsEnabled()) {
return false;
}
if (optimization_guide_decider_) {
bool allowed_by_allowlist =
optimization_guide_decider_->CanApplyOptimization(
url,
optimization_guide::proto::LENS_OVERLAY_EDU_ACTION_CHIP_BLOCKLIST,
/*optimization_metadata=*/nullptr) ==
optimization_guide::OptimizationGuideDecision::kTrue;
bool allowed_by_blocklist =
optimization_guide_decider_->CanApplyOptimization(
url,
optimization_guide::proto::LENS_OVERLAY_EDU_ACTION_CHIP_ALLOWLIST,
/*optimization_metadata=*/nullptr) ==
optimization_guide::OptimizationGuideDecision::kTrue;
return allowed_by_allowlist && allowed_by_blocklist;
}
return edu_url_matcher_->IsMatch(url);
}
// static
void LensOverlayEntryPointController::InvokeAction(
tabs::TabInterface* active_tab,
const actions::ActionInvocationContext& context) {
LensSearchController* search_controller =
LensSearchController::From(active_tab);
std::underlying_type_t<page_actions::PageActionTrigger> page_action_trigger =
context.GetProperty(page_actions::kPageActionTriggerKey);
// The Lens Overlay action item has different entry points that may trigger it
// (e.g., toolbar, page action, etc.). Triggers from a page action will have a
// valid PageActionTrigger property set.
if (page_action_trigger != page_actions::kInvalidPageActionTrigger) {
if (static_cast<page_actions::PageActionTrigger>(page_action_trigger) ==
page_actions::PageActionTrigger::kKeyboard &&
!lens::features::IsLensOverlayKeyboardSelectionEnabled()) {
active_tab->GetBrowserWindowInterface()
->GetFeatures()
.lens_region_search_controller()
->Start(active_tab->GetContents(), /*use_fullscreen_capture=*/true,
/*is_google_default_search_provider=*/true,
lens::AmbientSearchEntryPoint::
LENS_OVERLAY_LOCATION_BAR_ACCESSIBILITY_FALLBACK);
} else {
lens::RecordAmbientSearchQuery(
lens::AmbientSearchEntryPoint::LENS_OVERLAY_LOCATION_BAR);
search_controller->OpenLensOverlay(
lens::LensOverlayInvocationSource::kOmnibox);
BrowserUserEducationInterface::From(
active_tab->GetBrowserWindowInterface())
->NotifyNewBadgeFeatureUsed(lens::features::kLensOverlay);
}
return;
}
// Toggle the Lens overlay. There's no need to show or hide the side
// panel as the overlay controller will handle that.
const auto* entry_point_controller =
active_tab->GetBrowserWindowInterface()
->GetFeatures()
.lens_overlay_entry_point_controller();
if (entry_point_controller->IsOverlayActive()) {
search_controller->CloseLensAsync(
lens::LensOverlayDismissalSource::kToolbar);
} else {
search_controller->OpenLensOverlay(
lens::LensOverlayInvocationSource::kToolbar);
}
}
void LensOverlayEntryPointController::OnViewAddedToWidget(views::View* view) {
CHECK(location_bar_);
focus_manager_observation_.Observe(location_bar_->GetFocusManager());
}
void LensOverlayEntryPointController::OnViewRemovedFromWidget(
views::View* view) {
CHECK(location_bar_);
CHECK(location_bar_->GetFocusManager());
focus_manager_observation_.Reset();
}
void LensOverlayEntryPointController::OnDidChangeFocus(views::View* before,
views::View* now) {
UpdatePageActionState();
}
void LensOverlayEntryPointController::OnFullscreenStateChanged() {
// Disable the Lens entry points in the top chrome if there is no top bar in
// Chrome. On Mac and ChromeOS, it is possible to hover over the top of the
// screen to get the top bar back, but since does top bar does not stay
// open, we need to disable those entry points.
UpdateEntryPointsState(/*hide_toolbar_entrypoint=*/false);
}
void LensOverlayEntryPointController::OnTemplateURLServiceChanged() {
// Possibly add/remove the entrypoints based on the new users default search
// engine.
UpdateEntryPointsState(/*hide_toolbar_entrypoint=*/true);
}
void LensOverlayEntryPointController::OnTemplateURLServiceShuttingDown() {
template_url_service_observation_.Reset();
}
actions::ActionItem* LensOverlayEntryPointController::GetToolbarEntrypoint() {
return actions::ActionManager::Get().FindAction(
kActionSidePanelShowLensOverlayResults,
/*scope=*/browser_window_interface_->GetActions()->root_action_item());
}
void LensOverlayEntryPointController::UpdatePageActionState() {
if (!IsPageActionMigrated(PageActionIconType::kLensOverlay)) {
return;
}
// This may not have been initialized (e.g. for non-normal browser types).
if (!location_bar_) {
return;
}
CHECK(browser_window_interface_);
tabs::TabInterface* active_tab =
browser_window_interface_->GetActiveTabInterface();
// Possible during browser window initialization or teardown, or tab
// detachment.
// TODO(crbug.com/422807364): `UpdatePageActionState` shouldn't be called
// during tab destruction in the first place, but there are multiple
// TabFeatures that update UI (and therefore focus) during destruction of the
// tab. Once these TabFeatures are updated to only modify UI during
// `TabWillDetach` instead of the destructor, it should be safe to assume
// that TabFeatures exists for the active tab.
if (!active_tab || !active_tab->GetTabFeatures()) {
return;
}
page_actions::PageActionController* page_action_controller =
active_tab->GetTabFeatures()->page_action_controller();
CHECK(page_action_controller);
const actions::ActionId page_action_id =
kActionSidePanelShowLensOverlayResults;
if (!ShouldShowPageAction(active_tab)) {
page_action_controller->Hide(page_action_id);
return;
}
// No-ops if the overriding string is the same.
page_action_controller->OverrideText(
page_action_id,
l10n_util::GetStringUTF16(IDS_CONTENT_LENS_OVERLAY_ENTRYPOINT_LABEL));
page_action_controller->Show(page_action_id);
page_action_controller->ShowSuggestionChip(page_action_id,
{
.should_animate = false,
});
}
bool LensOverlayEntryPointController::IsOverlayActive() const {
// TODO(crbug.com/404941800): Rename this function to make it clear that it
// checks both the overlay and the side panel being active.
auto* active_tab = browser_window_interface_->GetActiveTabInterface();
if (!active_tab) {
return false;
}
LensSearchController* search_controller =
LensSearchController::From(active_tab);
LensOverlaySidePanelCoordinator* side_panel_coordinator =
search_controller->lens_overlay_side_panel_coordinator();
if (side_panel_coordinator && side_panel_coordinator->IsEntryShowing()) {
return true;
}
const auto* controller = search_controller->lens_overlay_controller();
return controller && controller->IsOverlayActive();
}
bool LensOverlayEntryPointController::ShouldShowPageAction(
tabs::TabInterface* active_tab) const {
if (!AreVisible()) {
return false;
}
// When the AIM page action is enabled, the Lens overlay entrypoint
// in the Omnibox should be suppressed.
const auto* aim_eligibility_service =
AimEligibilityServiceFactory::GetForProfile(
browser_window_interface_->GetProfile());
if (OmniboxFieldTrial::IsAimOmniboxEntrypointEnabled(
aim_eligibility_service)) {
return false;
}
if (!browser_window_interface_->GetProfile()->GetPrefs()->GetBoolean(
omnibox::kShowGoogleLensShortcut)) {
return false;
}
if (!features::IsOmniboxEntryPointEnabled()) {
return false;
}
if (!features::IsOmniboxEntrypointAlwaysVisible() &&
!location_bar_->Contains(
location_bar_->GetFocusManager()->GetFocusedView())) {
return false;
}
// The overlay is unavailable on the NTP as it is unlikely to be useful to
// users on the page. It would also appear immediately when a new tab or
// window is created due to focus immediately jumping into the location bar.
if (IsNewTabPage(active_tab->GetContents())) {
return false;
}
return true;
}
} // namespace lens