1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/ui/views/location_bar/cookie_controls_bubble_view.h"
6 
7 #include <memory>
8 #include "base/logging.h"
9 #include "base/metrics/user_metrics.h"
10 #include "base/metrics/user_metrics_action.h"
11 #include "base/strings/string16.h"
12 #include "chrome/browser/ui/tab_dialogs.h"
13 #include "chrome/browser/ui/views/accessibility/non_accessible_image_view.h"
14 #include "chrome/browser/ui/views/chrome_layout_provider.h"
15 #include "chrome/grit/generated_resources.h"
16 #include "chrome/grit/theme_resources.h"
17 #include "ui/base/l10n/l10n_util.h"
18 #include "ui/base/resource/resource_bundle.h"
19 #include "ui/base/ui_base_types.h"
20 #include "ui/gfx/text_utils.h"
21 #include "ui/views/background.h"
22 #include "ui/views/bubble/bubble_frame_view.h"
23 #include "ui/views/controls/image_view.h"
24 #include "ui/views/controls/link.h"
25 #include "ui/views/layout/box_layout.h"
26 #include "ui/views/widget/widget.h"
27 
28 using base::UserMetricsAction;
29 
30 namespace {
31 
32 // Singleton instance of the cookie bubble. The cookie bubble can only be
33 // shown on the active browser window, so there is no case in which it will be
34 // shown twice at the same time.
35 static CookieControlsBubbleView* g_instance;
36 
CreateInfoIcon()37 std::unique_ptr<views::TooltipIcon> CreateInfoIcon() {
38   auto explanation_tooltip = std::make_unique<views::TooltipIcon>(
39       l10n_util::GetStringUTF16(IDS_COOKIE_CONTROLS_HELP));
40   explanation_tooltip->set_bubble_width(
41       ChromeLayoutProvider::Get()->GetDistanceMetric(
42           views::DISTANCE_BUBBLE_PREFERRED_WIDTH));
43   explanation_tooltip->set_anchor_point_arrow(
44       views::BubbleBorder::Arrow::TOP_RIGHT);
45   return explanation_tooltip;
46 }
47 
48 }  // namespace
49 
50 // static
ShowBubble(views::View * anchor_view,views::Button * highlighted_button,content::WebContents * web_contents,content_settings::CookieControlsController * controller,CookieControlsStatus status)51 void CookieControlsBubbleView::ShowBubble(
52     views::View* anchor_view,
53     views::Button* highlighted_button,
54     content::WebContents* web_contents,
55     content_settings::CookieControlsController* controller,
56     CookieControlsStatus status) {
57   DCHECK(web_contents);
58   if (g_instance)
59     return;
60 
61   base::RecordAction(UserMetricsAction("CookieControls.Bubble.Opened"));
62   g_instance =
63       new CookieControlsBubbleView(anchor_view, web_contents, controller);
64   g_instance->SetHighlightedButton(highlighted_button);
65   views::Widget* bubble_widget =
66       views::BubbleDialogDelegateView::CreateBubble(g_instance);
67   controller->Update(web_contents);
68   bubble_widget->Show();
69 }
70 
71 // static
GetCookieBubble()72 CookieControlsBubbleView* CookieControlsBubbleView::GetCookieBubble() {
73   return g_instance;
74 }
75 
OnStatusChanged(CookieControlsStatus new_status,CookieControlsEnforcement new_enforcement,int allowed_cookies,int blocked_cookies)76 void CookieControlsBubbleView::OnStatusChanged(
77     CookieControlsStatus new_status,
78     CookieControlsEnforcement new_enforcement,
79     int allowed_cookies,
80     int blocked_cookies) {
81   if (status_ == new_status && enforcement_ == new_enforcement) {
82     OnCookiesCountChanged(allowed_cookies, blocked_cookies);
83     return;
84   }
85   if (new_status != CookieControlsStatus::kEnabled)
86     intermediate_step_ = IntermediateStep::kNone;
87   status_ = new_status;
88   enforcement_ = new_enforcement;
89   blocked_cookies_ = blocked_cookies;
90   UpdateUi();
91 }
92 
OnCookiesCountChanged(int allowed_cookies,int blocked_cookies)93 void CookieControlsBubbleView::OnCookiesCountChanged(int allowed_cookies,
94                                                      int blocked_cookies) {
95   // The blocked cookie count changes quite frequently, so avoid unnecessary
96   // UI updates if possible.
97   if (blocked_cookies_ == blocked_cookies)
98     return;
99 
100   blocked_cookies_ = blocked_cookies;
101   GetBubbleFrameView()->UpdateWindowTitle();
102 }
103 
CookieControlsBubbleView(views::View * anchor_view,content::WebContents * web_contents,content_settings::CookieControlsController * controller)104 CookieControlsBubbleView::CookieControlsBubbleView(
105     views::View* anchor_view,
106     content::WebContents* web_contents,
107     content_settings::CookieControlsController* controller)
108     : LocationBarBubbleDelegateView(anchor_view, web_contents),
109       controller_(controller) {
110   controller_observer_.Add(controller);
111   SetButtons(ui::DIALOG_BUTTON_NONE);
112 }
113 
114 CookieControlsBubbleView::~CookieControlsBubbleView() = default;
115 
UpdateUi()116 void CookieControlsBubbleView::UpdateUi() {
117   if (status_ == CookieControlsStatus::kDisabled) {
118     CloseBubble();
119     return;
120   }
121 
122   GetBubbleFrameView()->UpdateWindowTitle();
123   text_->SetVisible(false);
124   show_cookies_link_->SetVisible(false);
125   header_view_->SetVisible(false);
126 
127   if (intermediate_step_ == IntermediateStep::kTurnOffButton) {
128     text_->SetVisible(true);
129     text_->SetText(
130         l10n_util::GetStringUTF16(IDS_COOKIE_CONTROLS_NOT_WORKING_DESCRIPTION));
131     auto tooltip_icon = CreateInfoIcon();
132     tooltip_observer_.Add(tooltip_icon.get());
133     extra_view_ = SetExtraView(std::move(tooltip_icon));
134     show_cookies_link_->SetVisible(true);
135   } else if (status_ == CookieControlsStatus::kEnabled) {
136     header_view_->SetVisible(true);
137     header_view_->SetImage(
138         ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
139             IDR_COOKIE_BLOCKING_ON_HEADER));
140     text_->SetVisible(true);
141     text_->SetText(
142         l10n_util::GetStringUTF16(IDS_COOKIE_CONTROLS_BLOCKED_MESSAGE));
143     auto link = std::make_unique<views::Link>(
144         l10n_util::GetStringUTF16(IDS_COOKIE_CONTROLS_NOT_WORKING_TITLE));
145     link->SetCallback(
146         base::BindRepeating(&CookieControlsBubbleView::NotWorkingLinkClicked,
147                             base::Unretained(this)));
148     extra_view_ = SetExtraView(std::move(link));
149     blocked_cookies_.reset();
150   } else {
151     DCHECK_EQ(status_, CookieControlsStatus::kDisabledForSite);
152     header_view_->SetVisible(true);
153     header_view_->SetImage(
154         ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
155             IDR_COOKIE_BLOCKING_OFF_HEADER));
156     if (extra_view_)
157       extra_view_->SetVisible(false);
158   }
159 
160   SetButtonLabel(
161       ui::DIALOG_BUTTON_OK,
162       intermediate_step_ == IntermediateStep::kTurnOffButton
163           ? l10n_util::GetStringUTF16(IDS_COOKIE_CONTROLS_TURN_OFF_BUTTON)
164           : l10n_util::GetStringUTF16(IDS_COOKIE_CONTROLS_TURN_ON_BUTTON));
165   SetButtons((intermediate_step_ == IntermediateStep::kTurnOffButton ||
166               (status_ == CookieControlsStatus::kDisabledForSite &&
167                enforcement_ == CookieControlsEnforcement::kNoEnforcement))
168                  ? ui::DIALOG_BUTTON_OK
169                  : ui::DIALOG_BUTTON_NONE);
170   SetAcceptCallback(base::BindOnce(&CookieControlsBubbleView::OnDialogAccepted,
171                                    base::Unretained(this)));
172 
173   DialogModelChanged();
174   Layout();
175 
176   // The show_disable_cookie_blocking_ui_ state has a different title
177   // configuration. To avoid jumping UI, don't resize the bubble. This should be
178   // safe as the bubble in this state has less content than in Enabled state.
179   if (intermediate_step_ != IntermediateStep::kTurnOffButton)
180     SizeToContents();
181 }
182 
CloseBubble()183 void CookieControlsBubbleView::CloseBubble() {
184   // Widget's Close() is async, but we don't want to use cookie_bubble_ after
185   // this. Additionally web_contents() may have been destroyed.
186   g_instance = nullptr;
187   LocationBarBubbleDelegateView::CloseBubble();
188 }
189 
Init()190 void CookieControlsBubbleView::Init() {
191   const ChromeLayoutProvider* provider = ChromeLayoutProvider::Get();
192   SetLayoutManager(std::make_unique<views::BoxLayout>(
193       views::BoxLayout::Orientation::kVertical, gfx::Insets(),
194       provider->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_VERTICAL)));
195 
196   auto text = std::make_unique<views::Label>(base::string16(),
197                                              views::style::CONTEXT_LABEL,
198                                              views::style::STYLE_SECONDARY);
199   text->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
200   text->SetMultiLine(true);
201   text_ = AddChildView(std::move(text));
202 
203   auto cookie_link = std::make_unique<views::Link>(
204       l10n_util::GetStringUTF16(IDS_BLOCKED_COOKIES_INFO));
205   cookie_link->SetMultiLine(true);
206   cookie_link->SetCallback(
207       base::BindRepeating(&CookieControlsBubbleView::ShowCookiesLinkClicked,
208                           base::Unretained(this)));
209   cookie_link->SetHorizontalAlignment(gfx::ALIGN_LEFT);
210   show_cookies_link_ = AddChildView(std::move(cookie_link));
211 
212   // TODO(crbug.com/1013092): The bubble should display a header view with full
213   // width without having to tweak margins.
214   gfx::Insets insets = margins();
215   set_margins(gfx::Insets(insets.top(), 0, insets.bottom(), 0));
216   SetBorder(views::CreateEmptyBorder(0, insets.left(), 0, insets.right()));
217 }
218 
AddedToWidget()219 void CookieControlsBubbleView::AddedToWidget() {
220   auto header_view = std::make_unique<NonAccessibleImageView>();
221   header_view_ = header_view.get();
222   header_view_->SetBackground(views::CreateThemedSolidBackground(
223       header_view_, ui::NativeTheme::kColorId_BubbleFooterBackground));
224   GetBubbleFrameView()->SetHeaderView(std::move(header_view));
225 }
226 
CalculatePreferredSize() const227 gfx::Size CookieControlsBubbleView::CalculatePreferredSize() const {
228   // The total width of this view should always be identical to the width
229   // of the header images.
230   int width = ui::ResourceBundle::GetSharedInstance()
231                   .GetImageSkiaNamed(IDR_COOKIE_BLOCKING_ON_HEADER)
232                   ->width();
233   return gfx::Size{width, GetHeightForWidth(width)};
234 }
235 
GetWindowTitle() const236 base::string16 CookieControlsBubbleView::GetWindowTitle() const {
237   switch (intermediate_step_) {
238     case IntermediateStep::kTurnOffButton:
239       return l10n_util::GetStringUTF16(IDS_COOKIE_CONTROLS_NOT_WORKING_TITLE);
240     case IntermediateStep::kNone: {
241       // Determine title based on status_ instead.
242     }
243   }
244   switch (status_) {
245     case CookieControlsStatus::kEnabled:
246       return l10n_util::GetPluralStringFUTF16(IDS_COOKIE_CONTROLS_DIALOG_TITLE,
247                                               blocked_cookies_.value_or(0));
248     case CookieControlsStatus::kDisabledForSite:
249       return l10n_util::GetStringUTF16(IDS_COOKIE_CONTROLS_DIALOG_TITLE_OFF);
250     case CookieControlsStatus::kUninitialized:
251       return base::string16();
252     case CookieControlsStatus::kDisabled:
253       NOTREACHED();
254       return base::string16();
255   }
256 }
257 
ShouldShowWindowTitle() const258 bool CookieControlsBubbleView::ShouldShowWindowTitle() const {
259   return true;
260 }
261 
ShouldShowCloseButton() const262 bool CookieControlsBubbleView::ShouldShowCloseButton() const {
263   return true;
264 }
265 
WindowClosing()266 void CookieControlsBubbleView::WindowClosing() {
267   // |cookie_bubble_| can be a new bubble by this point (as Close(); doesn't
268   // call this right away). Only set to nullptr when it's this bubble.
269   bool this_bubble = g_instance == this;
270   if (this_bubble)
271     g_instance = nullptr;
272 
273   controller_->OnUiClosing();
274 }
275 
OnDialogAccepted()276 void CookieControlsBubbleView::OnDialogAccepted() {
277   if (intermediate_step_ == IntermediateStep::kTurnOffButton) {
278     controller_->OnCookieBlockingEnabledForSite(false);
279   } else {
280     DCHECK_EQ(status_, CookieControlsStatus::kDisabledForSite);
281     DCHECK_EQ(intermediate_step_, IntermediateStep::kNone);
282     controller_->OnCookieBlockingEnabledForSite(true);
283   }
284 }
285 
ShowCookiesLinkClicked()286 void CookieControlsBubbleView::ShowCookiesLinkClicked() {
287   base::RecordAction(UserMetricsAction("CookieControls.Bubble.CookiesInUse"));
288   TabDialogs::FromWebContents(web_contents())->ShowCollectedCookies();
289   GetWidget()->Close();
290 }
291 
NotWorkingLinkClicked()292 void CookieControlsBubbleView::NotWorkingLinkClicked() {
293   DCHECK_EQ(status_, CookieControlsStatus::kEnabled);
294   base::RecordAction(UserMetricsAction("CookieControls.Bubble.NotWorking"));
295   // Don't go through the controller as this is an intermediary state that
296   // is only relevant for the bubble UI.
297   intermediate_step_ = IntermediateStep::kTurnOffButton;
298   UpdateUi();
299 }
300 
OnTooltipBubbleShown(views::TooltipIcon * icon)301 void CookieControlsBubbleView::OnTooltipBubbleShown(views::TooltipIcon* icon) {
302   base::RecordAction(UserMetricsAction("CookieControls.Bubble.TooltipShown"));
303 }
304 
OnTooltipIconDestroying(views::TooltipIcon * icon)305 void CookieControlsBubbleView::OnTooltipIconDestroying(
306     views::TooltipIcon* icon) {
307   tooltip_observer_.Remove(icon);
308 }
309