blob: 977ee9b8b902e524b4374856b53d1a713b371668 [file] [log] [blame]
// Copyright 2023 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/views/tabs/tab_search_container.h"
#include <memory>
#include "base/i18n/rtl.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/time/time.h"
#include "base/types/pass_key.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/tabs/organization/tab_declutter_controller.h"
#include "chrome/browser/ui/tabs/organization/tab_organization_service.h"
#include "chrome/browser/ui/tabs/organization/tab_organization_service_factory.h"
#include "chrome/browser/ui/tabs/organization/tab_organization_utils.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/tabs/tab_search_button.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/tab_strip_nudge_button.h"
#include "chrome/browser/ui/webui/tab_search/tab_search.mojom.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/mouse_watcher.h"
#include "ui/views/mouse_watcher_view_host.h"
#include "ui/views/view_class_properties.h"
namespace {
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
enum class TriggerOutcome {
kAccepted = 0,
kDismissed = 1,
kTimedOut = 2,
kMaxValue = kTimedOut,
};
constexpr base::TimeDelta kExpansionInDuration = base::Milliseconds(500);
constexpr base::TimeDelta kExpansionOutDuration = base::Milliseconds(250);
constexpr base::TimeDelta kFlatEdgeInDuration = base::Milliseconds(400);
constexpr base::TimeDelta kFlatEdgeOutDuration = base::Milliseconds(250);
constexpr base::TimeDelta kOpacityInDuration = base::Milliseconds(300);
constexpr base::TimeDelta kOpacityOutDuration = base::Milliseconds(100);
constexpr base::TimeDelta kOpacityDelay = base::Milliseconds(100);
constexpr base::TimeDelta kShowDuration = base::Seconds(16);
constexpr char kAutoTabGroupsTriggerOutcomeName[] =
"Tab.Organization.Trigger.Outcome";
constexpr char kDeclutterTriggerOutcomeName[] =
"Tab.Organization.Declutter.Trigger.Outcome";
constexpr char kDeclutterTriggerBucketedCTRName[] =
"Tab.Organization.Declutter.Trigger.BucketedCTR";
constexpr int kSpaceBetweenButtons = 2;
Edge GetFlatEdge(bool is_search_button, bool tab_search_before_chips) {
const bool is_rtl = base::i18n::IsRTL();
if ((!is_search_button && tab_search_before_chips) ||
(is_search_button && !tab_search_before_chips)) {
return is_rtl ? Edge::kRight : Edge::kLeft;
}
return is_rtl ? Edge::kLeft : Edge::kRight;
}
} // namespace
TabSearchContainer::TabOrganizationAnimationSession::
TabOrganizationAnimationSession(
TabStripNudgeButton* button,
TabSearchContainer* container,
AnimationSessionType session_type,
base::OnceCallback<void()> on_animation_ended)
: button_(button),
container_(container),
expansion_animation_(container),
flat_edge_animation_(container),
opacity_animation_(container),
session_type_(session_type),
on_animation_ended_(std::move(on_animation_ended)) {
if (session_type_ == AnimationSessionType::HIDE) {
expansion_animation_.Reset(1);
flat_edge_animation_.Reset(1);
opacity_animation_.Reset(1);
}
}
TabSearchContainer::TabOrganizationAnimationSession::
~TabOrganizationAnimationSession() = default;
void TabSearchContainer::TabOrganizationAnimationSession::Start() {
if (session_type_ ==
TabOrganizationAnimationSession::AnimationSessionType::SHOW) {
Show();
} else {
Hide();
}
}
void TabSearchContainer::TabOrganizationAnimationSession::
ResetExpansionAnimationForTesting(double value) {
expansion_animation_.Reset(value);
}
void TabSearchContainer::TabOrganizationAnimationSession::
ResetFlatEdgeAnimationForTesting(double value) {
flat_edge_animation_.Reset(value);
}
void TabSearchContainer::TabOrganizationAnimationSession::
ResetOpacityAnimationForTesting(double value) {
if (opacity_animation_delay_timer_.IsRunning()) {
opacity_animation_delay_timer_.FireNow();
}
opacity_animation_.Reset(value);
}
void TabSearchContainer::TabOrganizationAnimationSession::Show() {
expansion_animation_.SetTweenType(gfx::Tween::Type::ACCEL_20_DECEL_100);
opacity_animation_.SetTweenType(gfx::Tween::Type::LINEAR);
flat_edge_animation_.SetTweenType(gfx::Tween::Type::LINEAR);
expansion_animation_.SetSlideDuration(
gfx::Animation::RichAnimationDuration(kExpansionInDuration));
flat_edge_animation_.SetSlideDuration(
gfx::Animation::RichAnimationDuration(kFlatEdgeInDuration));
opacity_animation_.SetSlideDuration(
gfx::Animation::RichAnimationDuration(kOpacityInDuration));
expansion_animation_.Show();
flat_edge_animation_.Show();
const base::TimeDelta delay =
gfx::Animation::RichAnimationDuration(kOpacityDelay);
opacity_animation_delay_timer_.Start(
FROM_HERE, delay, this,
&TabSearchContainer::TabOrganizationAnimationSession::
ShowOpacityAnimation);
}
void TabSearchContainer::TabOrganizationAnimationSession::Hide() {
// Animate and hide existing chip.
if (session_type_ ==
TabOrganizationAnimationSession::AnimationSessionType::SHOW) {
if (opacity_animation_delay_timer_.IsRunning()) {
opacity_animation_delay_timer_.FireNow();
}
session_type_ = TabOrganizationAnimationSession::AnimationSessionType::HIDE;
}
expansion_animation_.SetTweenType(gfx::Tween::Type::ACCEL_20_DECEL_100);
opacity_animation_.SetTweenType(gfx::Tween::Type::LINEAR);
flat_edge_animation_.SetTweenType(gfx::Tween::Type::ACCEL_20_DECEL_100);
expansion_animation_.SetSlideDuration(
gfx::Animation::RichAnimationDuration(kExpansionOutDuration));
flat_edge_animation_.SetSlideDuration(
gfx::Animation::RichAnimationDuration(kFlatEdgeOutDuration));
opacity_animation_.SetSlideDuration(
gfx::Animation::RichAnimationDuration(kOpacityOutDuration));
expansion_animation_.Hide();
flat_edge_animation_.Hide();
opacity_animation_.Hide();
}
void TabSearchContainer::TabOrganizationAnimationSession::
ShowOpacityAnimation() {
opacity_animation_.Show();
}
void TabSearchContainer::TabOrganizationAnimationSession::ApplyAnimationValue(
const gfx::Animation* animation) {
float value = animation->GetCurrentValue();
if (animation == &expansion_animation_) {
button_->SetWidthFactor(value);
} else if (animation == &flat_edge_animation_) {
container_->tab_search_button()->SetFlatEdgeFactor(1 - value);
button_->SetFlatEdgeFactor(1 - value);
} else if (animation == &opacity_animation_) {
button_->SetOpacity(value);
}
}
void TabSearchContainer::TabOrganizationAnimationSession::MarkAnimationDone(
const gfx::Animation* animation) {
if (animation == &expansion_animation_) {
expansion_animation_done_ = true;
} else if (animation == &flat_edge_animation_) {
flat_edge_animation_done_ = true;
} else {
opacity_animation_done_ = true;
}
if (expansion_animation_done_ && flat_edge_animation_done_ &&
opacity_animation_done_) {
if (on_animation_ended_) {
std::move(on_animation_ended_).Run();
}
}
}
TabSearchContainer::TabSearchContainer(
TabStripController* tab_strip_controller,
TabStripModel* tab_strip_model,
bool tab_search_before_chips,
View* locked_expansion_view,
BrowserWindowInterface* browser_window_interface,
tabs::TabDeclutterController* tab_declutter_controller,
TabStrip* tab_strip)
: AnimationDelegateViews(this),
locked_expansion_view_(locked_expansion_view),
tab_declutter_controller_(tab_declutter_controller),
tab_strip_model_(tab_strip_model) {
SetProperty(views::kElementIdentifierKey, kTabSearchContainerElementId);
mouse_watcher_ = std::make_unique<views::MouseWatcher>(
std::make_unique<views::MouseWatcherViewHost>(locked_expansion_view,
gfx::Insets()),
this);
tab_organization_service_ = TabOrganizationServiceFactory::GetForProfile(
tab_strip_controller->GetProfile());
if (tab_organization_service_) {
tab_organization_observation_.Observe(tab_organization_service_);
}
// Edge adjacent to new tab button should be rounded and opposite edge
// should animate to flat on chip show.
std::unique_ptr<TabSearchButton> tab_search_button =
std::make_unique<TabSearchButton>(
tab_strip_controller, browser_window_interface, Edge::kNone,
GetFlatEdge(true, tab_search_before_chips), tab_strip);
tab_search_button->SetProperty(views::kCrossAxisAlignmentKey,
views::LayoutAlignment::kCenter);
tab_search_button_ = AddChildView(std::move(tab_search_button));
int tab_search_button_index = GetIndexOf(tab_search_button_).value();
int index = tab_search_before_chips ? tab_search_button_index + 1
: tab_search_button_index;
// TODO(crbug.com/40925230): Consider hiding the button when the request has
// started, vs. when the button as clicked.
auto_tab_group_button_ = AddChildViewAt(
CreateAutoTabGroupButton(tab_strip_controller, tab_search_before_chips),
index);
SetupButtonProperties(auto_tab_group_button_, tab_search_before_chips);
browser_ = tab_strip_controller->GetBrowser();
// `tab_declutter_controller_` will be null for some profile types and if
// feature is not enabled.
if (tab_declutter_controller_) {
tab_declutter_button_ = AddChildViewAt(
CreateTabDeclutterButton(tab_strip_controller, tab_search_before_chips),
index);
SetupButtonProperties(tab_declutter_button_, tab_search_before_chips);
tab_declutter_observation_.Observe(tab_declutter_controller_);
}
SetLayoutManager(std::make_unique<views::FlexLayout>());
}
TabSearchContainer::~TabSearchContainer() {
if (scoped_tab_strip_modal_ui_) {
scoped_tab_strip_modal_ui_.reset();
}
}
void TabSearchContainer::SetupButtonProperties(TabStripNudgeButton* button,
bool tab_search_before_chips) {
// Set the margins for the button
gfx::Insets margin;
if (tab_search_before_chips) {
margin.set_left(kSpaceBetweenButtons);
} else {
margin.set_right(kSpaceBetweenButtons);
}
button->SetProperty(views::kMarginsKey, margin);
// Set opacity for the button
button->SetOpacity(0);
}
std::unique_ptr<TabStripNudgeButton>
TabSearchContainer::CreateAutoTabGroupButton(
TabStripController* tab_strip_controller,
bool tab_search_before_chips) {
auto button = std::make_unique<TabStripNudgeButton>(
tab_strip_controller,
base::BindRepeating(&TabSearchContainer::OnAutoTabGroupButtonClicked,
base::Unretained(this)),
base::BindRepeating(&TabSearchContainer::OnAutoTabGroupButtonDismissed,
base::Unretained(this)),
l10n_util::GetStringUTF16(IDS_TAB_ORGANIZE), kAutoTabGroupButtonElementId,
GetFlatEdge(false, tab_search_before_chips), gfx::VectorIcon::EmptyIcon(),
/*show_close_button=*/true);
button->SetTooltipText(l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_ORGANIZE));
button->GetViewAccessibility().SetName(
l10n_util::GetStringUTF16(IDS_ACCNAME_TAB_ORGANIZE));
button->SetProperty(views::kCrossAxisAlignmentKey,
views::LayoutAlignment::kCenter);
return button;
}
std::unique_ptr<TabStripNudgeButton>
TabSearchContainer::CreateTabDeclutterButton(
TabStripController* tab_strip_controller,
bool tab_search_before_chips) {
auto button = std::make_unique<TabStripNudgeButton>(
tab_strip_controller,
base::BindRepeating(&TabSearchContainer::OnTabDeclutterButtonClicked,
base::Unretained(this)),
base::BindRepeating(&TabSearchContainer::OnTabDeclutterButtonDismissed,
base::Unretained(this)),
features::IsTabstripDedupeEnabled()
? l10n_util::GetStringUTF16(IDS_TAB_DECLUTTER)
: l10n_util::GetStringUTF16(IDS_TAB_DECLUTTER_NO_DEDUPE),
kTabDeclutterButtonElementId, GetFlatEdge(false, tab_search_before_chips),
gfx::VectorIcon::EmptyIcon(), /*show_close_button=*/true);
button->SetTooltipText(
features::IsTabstripDedupeEnabled()
? l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_DECLUTTER)
: l10n_util::GetStringUTF16(IDS_TOOLTIP_TAB_DECLUTTER_NO_DEDUPE));
button->GetViewAccessibility().SetName(
features::IsTabstripDedupeEnabled()
? l10n_util::GetStringUTF16(IDS_ACCNAME_TAB_DECLUTTER)
: l10n_util::GetStringUTF16(IDS_ACCNAME_TAB_DECLUTTER_NO_DEDUPE));
button->SetProperty(views::kCrossAxisAlignmentKey,
views::LayoutAlignment::kCenter);
return button;
}
void TabSearchContainer::ShowTabOrganization(TabStripNudgeButton* button) {
if (locked_expansion_view_->IsMouseHovered()) {
SetLockedExpansionMode(LockedExpansionMode::kWillShow, button);
}
if (locked_expansion_mode_ == LockedExpansionMode::kNone) {
ExecuteShowTabOrganization(button);
}
}
void TabSearchContainer::HideTabOrganization(TabStripNudgeButton* button) {
if (locked_expansion_view_->IsMouseHovered()) {
SetLockedExpansionMode(LockedExpansionMode::kWillHide, button);
}
if (locked_expansion_mode_ == LockedExpansionMode::kNone) {
ExecuteHideTabOrganization(button);
}
}
void TabSearchContainer::SetLockedExpansionModeForTesting(
LockedExpansionMode mode,
TabStripNudgeButton* button) {
SetLockedExpansionMode(mode, button);
}
void TabSearchContainer::OnAutoTabGroupButtonClicked() {
base::UmaHistogramEnumeration(kAutoTabGroupsTriggerOutcomeName,
TriggerOutcome::kAccepted);
tab_organization_service_->OnActionUIAccepted(browser_);
UMA_HISTOGRAM_BOOLEAN("Tab.Organization.AllEntrypoints.Clicked", true);
UMA_HISTOGRAM_BOOLEAN("Tab.Organization.Proactive.Clicked", true);
// Force hide the button when pressed, bypassing locked expansion mode.
ExecuteHideTabOrganization(auto_tab_group_button_);
}
void TabSearchContainer::OnAutoTabGroupButtonDismissed() {
base::UmaHistogramEnumeration(kAutoTabGroupsTriggerOutcomeName,
TriggerOutcome::kDismissed);
tab_organization_service_->OnActionUIDismissed(browser_);
UMA_HISTOGRAM_BOOLEAN("Tab.Organization.Proactive.Clicked", false);
// Force hide the button when pressed, bypassing locked expansion mode.
ExecuteHideTabOrganization(auto_tab_group_button_);
}
void TabSearchContainer::OnOrganizeButtonTimeout(TabStripNudgeButton* button) {
if (button == auto_tab_group_button_) {
base::UmaHistogramEnumeration(kAutoTabGroupsTriggerOutcomeName,
TriggerOutcome::kTimedOut);
UMA_HISTOGRAM_BOOLEAN("Tab.Organization.Proactive.Clicked", false);
} else if (button == tab_declutter_button_) {
base::UmaHistogramEnumeration(kDeclutterTriggerOutcomeName,
TriggerOutcome::kTimedOut);
}
// Hide the button if not pressed. Use locked expansion mode to avoid
// disrupting the user.
HideTabOrganization(button);
}
void TabSearchContainer::SetLockedExpansionMode(LockedExpansionMode mode,
TabStripNudgeButton* button) {
if (mode == LockedExpansionMode::kNone) {
if (locked_expansion_mode_ == LockedExpansionMode::kWillShow) {
ExecuteShowTabOrganization(locked_expansion_button_);
} else if (locked_expansion_mode_ == LockedExpansionMode::kWillHide) {
ExecuteHideTabOrganization(locked_expansion_button_);
}
locked_expansion_button_ = nullptr;
} else {
locked_expansion_button_ = button;
mouse_watcher_->Start(GetWidget()->GetNativeWindow());
}
locked_expansion_mode_ = mode;
}
void TabSearchContainer::ExecuteShowTabOrganization(
TabStripNudgeButton* button) {
if (browser_ && (button == auto_tab_group_button_) &&
!TabOrganizationUtils::GetInstance()->IsEnabled(browser_->profile())) {
return;
}
// If the tab strip already has a modal UI showing, exit early.
if (!tab_strip_model_->CanShowModalUI()) {
return;
}
scoped_tab_strip_modal_ui_ = tab_strip_model_->ShowModalUI();
animation_session_ = std::make_unique<TabOrganizationAnimationSession>(
button, this, TabOrganizationAnimationSession::AnimationSessionType::SHOW,
base::BindOnce(&TabSearchContainer::OnAnimationSessionEnded,
base::Unretained(this)));
animation_session_->Start();
hide_tab_organization_timer_.Start(
FROM_HERE, kShowDuration,
base::BindOnce(&TabSearchContainer::OnOrganizeButtonTimeout,
base::Unretained(this), button));
if (button == tab_declutter_button_) {
LogDeclutterTriggerBucket(false);
}
}
void TabSearchContainer::ExecuteHideTabOrganization(
TabStripNudgeButton* button) {
// Hide the current animation if the shown button is the same button. Do not
// create a new animation session.
if (animation_session_ &&
animation_session_->session_type() ==
TabOrganizationAnimationSession::AnimationSessionType::SHOW &&
animation_session_->button() == button) {
hide_tab_organization_timer_.Stop();
animation_session_->Hide();
return;
}
if (!button->GetVisible()) {
return;
}
// Stop the timer since the chip might be getting hidden on user actions like
// dismissal or click and not timeout.
hide_tab_organization_timer_.Stop();
animation_session_ = std::make_unique<TabOrganizationAnimationSession>(
button, this, TabOrganizationAnimationSession::AnimationSessionType::HIDE,
base::BindOnce(&TabSearchContainer::OnAnimationSessionEnded,
base::Unretained(this)));
animation_session_->Start();
}
void TabSearchContainer::MouseMovedOutOfHost() {
SetLockedExpansionMode(LockedExpansionMode::kNone, nullptr);
}
void TabSearchContainer::AnimationCanceled(const gfx::Animation* animation) {
AnimationEnded(animation);
}
void TabSearchContainer::AnimationEnded(const gfx::Animation* animation) {
animation_session_->ApplyAnimationValue(animation);
animation_session_->MarkAnimationDone(animation);
}
void TabSearchContainer::OnAnimationSessionEnded() {
// If the button went from shown -> hidden, unblock the tab strip from
// showing other modal UIs.
if (animation_session_->session_type() ==
TabOrganizationAnimationSession::AnimationSessionType::HIDE) {
scoped_tab_strip_modal_ui_.reset();
}
animation_session_.reset();
}
void TabSearchContainer::AnimationProgressed(const gfx::Animation* animation) {
animation_session_->ApplyAnimationValue(animation);
}
void TabSearchContainer::OnToggleActionUIState(const Browser* browser,
bool should_show) {
CHECK(tab_organization_service_);
if (locked_expansion_mode_ != LockedExpansionMode::kNone) {
return;
}
if (should_show && browser_ == browser) {
ShowTabOrganization(auto_tab_group_button_);
} else {
HideTabOrganization(auto_tab_group_button_);
}
}
void TabSearchContainer::OnTabDeclutterButtonClicked() {
tabs::TabDeclutterController::EmitEntryPointHistogram(
tab_search::mojom::TabDeclutterEntryPoint::kNudge);
base::UmaHistogramEnumeration(kDeclutterTriggerOutcomeName,
TriggerOutcome::kAccepted);
LogDeclutterTriggerBucket(true);
browser_->window()->CreateTabSearchBubble(
tab_search::mojom::TabSearchSection::kOrganize,
tab_search::mojom::TabOrganizationFeature::kDeclutter);
// Force hide the button when pressed, bypassing locked expansion mode.
ExecuteHideTabOrganization(tab_declutter_button_);
}
void TabSearchContainer::OnTabDeclutterButtonDismissed() {
base::UmaHistogramEnumeration(kDeclutterTriggerOutcomeName,
TriggerOutcome::kDismissed);
tab_declutter_controller_->OnActionUIDismissed(
base::PassKey<TabSearchContainer>());
// Force hide the button when pressed, bypassing locked expansion mode.
ExecuteHideTabOrganization(tab_declutter_button_);
}
void TabSearchContainer::OnTriggerDeclutterUIVisibility() {
CHECK(tab_declutter_controller_);
if (locked_expansion_mode_ != LockedExpansionMode::kNone) {
return;
}
ShowTabOrganization(tab_declutter_button_);
}
DeclutterTriggerCTRBucket TabSearchContainer::GetDeclutterTriggerBucket(
bool clicked) {
const auto total_tab_count =
tab_declutter_controller_->tab_strip_model()->GetTabCount();
const auto stale_tab_count = tab_declutter_controller_->GetStaleTabs().size();
if (total_tab_count < 15) {
return clicked ? DeclutterTriggerCTRBucket::kClickedUnder15Tabs
: DeclutterTriggerCTRBucket::kShownUnder15Tabs;
} else if (total_tab_count < 20) {
if (stale_tab_count < 2) {
return clicked ? DeclutterTriggerCTRBucket::kClicked15To19TabsUnder2Stale
: DeclutterTriggerCTRBucket::kShown15To19TabsUnder2Stale;
} else if (stale_tab_count < 5) {
return clicked ? DeclutterTriggerCTRBucket::kClicked15To19Tabs2To4Stale
: DeclutterTriggerCTRBucket::kShown15To19Tabs2To4Stale;
} else if (stale_tab_count < 8) {
return clicked ? DeclutterTriggerCTRBucket::kClicked15To19Tabs5To7Stale
: DeclutterTriggerCTRBucket::kShown15To19Tabs5To7Stale;
} else {
return clicked ? DeclutterTriggerCTRBucket::kClicked15To19TabsOver7Stale
: DeclutterTriggerCTRBucket::kShown15To19TabsOver7Stale;
}
} else if (total_tab_count < 25) {
if (stale_tab_count < 2) {
return clicked ? DeclutterTriggerCTRBucket::kClicked20To24TabsUnder2Stale
: DeclutterTriggerCTRBucket::kShown20To24TabsUnder2Stale;
} else if (stale_tab_count < 5) {
return clicked ? DeclutterTriggerCTRBucket::kClicked20To24Tabs2To4Stale
: DeclutterTriggerCTRBucket::kShown20To24Tabs2To4Stale;
} else if (stale_tab_count < 8) {
return clicked ? DeclutterTriggerCTRBucket::kClicked20To24Tabs5To7Stale
: DeclutterTriggerCTRBucket::kShown20To24Tabs5To7Stale;
} else {
return clicked ? DeclutterTriggerCTRBucket::kClicked20To24TabsOver7Stale
: DeclutterTriggerCTRBucket::kShown20To24TabsOver7Stale;
}
} else {
return clicked ? DeclutterTriggerCTRBucket::kClickedOver24Tabs
: DeclutterTriggerCTRBucket::kShownOver24Tabs;
}
}
void TabSearchContainer::LogDeclutterTriggerBucket(bool clicked) {
const DeclutterTriggerCTRBucket bucket = GetDeclutterTriggerBucket(clicked);
base::UmaHistogramEnumeration(kDeclutterTriggerBucketedCTRName, bucket);
}
BEGIN_METADATA(TabSearchContainer)
END_METADATA