1 // Copyright 2020 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/permission_chip.h"
6 
7 #include "base/command_line.h"
8 #include "base/location.h"
9 #include "base/metrics/histogram_functions.h"
10 #include "base/strings/string_number_conversions.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "base/time/time.h"
13 #include "chrome/browser/ui/layout_constants.h"
14 #include "chrome/browser/ui/views/chrome_layout_provider.h"
15 #include "chrome/browser/ui/views/permission_bubble/permission_prompt_bubble_view.h"
16 #include "chrome/browser/ui/views/permission_bubble/permission_prompt_style.h"
17 #include "components/permissions/permission_request.h"
18 #include "components/strings/grit/components_strings.h"
19 #include "components/vector_icons/vector_icons.h"
20 #include "third_party/skia/include/core/SkColor.h"
21 #include "ui/base/l10n/l10n_util.h"
22 #include "ui/events/event.h"
23 #include "ui/gfx/color_palette.h"
24 #include "ui/gfx/color_utils.h"
25 #include "ui/gfx/favicon_size.h"
26 #include "ui/gfx/paint_vector_icon.h"
27 #include "ui/views/background.h"
28 #include "ui/views/controls/button/button_controller.h"
29 #include "ui/views/controls/image_view.h"
30 #include "ui/views/controls/label.h"
31 #include "ui/views/layout/fill_layout.h"
32 #include "ui/views/layout/flex_layout.h"
33 #include "ui/views/layout/layout_provider.h"
34 #include "ui/views/style/typography.h"
35 #include "ui/views/widget/widget.h"
36 
37 namespace {
IsCameraPermission(permissions::PermissionRequestType type)38 bool IsCameraPermission(permissions::PermissionRequestType type) {
39   return type ==
40          permissions::PermissionRequestType::PERMISSION_MEDIASTREAM_CAMERA;
41 }
42 
IsCameraOrMicPermission(permissions::PermissionRequestType type)43 bool IsCameraOrMicPermission(permissions::PermissionRequestType type) {
44   return IsCameraPermission(type) ||
45          type == permissions::PermissionRequestType::PERMISSION_MEDIASTREAM_MIC;
46 }
47 }  // namespace
48 
49 // ButtonController that NotifyClick from being called when the
50 // BubbleOwnerDelegate's bubble is showing. Otherwise the bubble will show again
51 // immediately after being closed via losing focus.
52 class BubbleButtonController : public views::ButtonController {
53  public:
BubbleButtonController(views::Button * button,BubbleOwnerDelegate * bubble_owner,std::unique_ptr<views::ButtonControllerDelegate> delegate)54   BubbleButtonController(
55       views::Button* button,
56       BubbleOwnerDelegate* bubble_owner,
57       std::unique_ptr<views::ButtonControllerDelegate> delegate)
58       : views::ButtonController(button, std::move(delegate)),
59         bubble_owner_(bubble_owner) {}
60 
OnMousePressed(const ui::MouseEvent & event)61   bool OnMousePressed(const ui::MouseEvent& event) override {
62     suppress_button_release_ = bubble_owner_->IsBubbleShowing();
63     return views::ButtonController::OnMousePressed(event);
64   }
65 
IsTriggerableEvent(const ui::Event & event)66   bool IsTriggerableEvent(const ui::Event& event) override {
67     // TODO(olesiamarukhno): There is the same logic in IconLabelBubbleView,
68     // this class should be reused in the future to avoid duplication.
69     if (event.IsMouseEvent())
70       return !bubble_owner_->IsBubbleShowing() && !suppress_button_release_;
71 
72     return views::ButtonController::IsTriggerableEvent(event);
73   }
74 
75  private:
76   bool suppress_button_release_ = false;
77   BubbleOwnerDelegate* bubble_owner_ = nullptr;
78 };
79 
PermissionChip(Browser * browser)80 PermissionChip::PermissionChip(Browser* browser)
81     : views::AnimationDelegateViews(this), browser_(browser) {
82   SetLayoutManager(std::make_unique<views::FillLayout>());
83   SetVisible(false);
84 
85   chip_button_ =
86       AddChildView(std::make_unique<views::MdTextButton>(base::BindRepeating(
87           &PermissionChip::ChipButtonPressed, base::Unretained(this))));
88   chip_button_->SetProminent(true);
89   chip_button_->SetCornerRadius(GetIconSize());
90   chip_button_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
91   chip_button_->SetElideBehavior(gfx::ElideBehavior::FADE_TAIL);
92   chip_button_->SetFocusBehavior(views::View::FocusBehavior::ALWAYS);
93   // Equalizing padding on the left, right and between icon and label.
94   chip_button_->SetImageLabelSpacing(
95       GetLayoutInsets(LOCATION_BAR_ICON_INTERIOR_PADDING).left());
96   chip_button_->SetCustomPadding(
97       gfx::Insets(GetLayoutConstant(LOCATION_BAR_CHILD_INTERIOR_PADDING),
98                   GetLayoutInsets(LOCATION_BAR_ICON_INTERIOR_PADDING).left()));
99 
100   chip_button_->SetButtonController(std::make_unique<BubbleButtonController>(
101       chip_button_, this,
102       std::make_unique<views::Button::DefaultButtonControllerDelegate>(
103           chip_button_)));
104 
105   constexpr auto kAnimationDuration = base::TimeDelta::FromMilliseconds(350);
106   animation_ = std::make_unique<gfx::SlideAnimation>(this);
107   animation_->SetSlideDuration(kAnimationDuration);
108 }
109 
~PermissionChip()110 PermissionChip::~PermissionChip() {
111   if (prompt_bubble_)
112     prompt_bubble_->GetWidget()->Close();
113   CHECK(!IsInObserverList());
114 }
115 
Show(permissions::PermissionPrompt::Delegate * delegate)116 void PermissionChip::Show(permissions::PermissionPrompt::Delegate* delegate) {
117   DCHECK(delegate);
118   delegate_ = delegate;
119 
120   const std::vector<permissions::PermissionRequest*>& requests =
121       delegate_->Requests();
122 
123   // TODO(olesiamarukhno): Add combined camera & microphone permission and
124   // update delegate to contain only one request at a time.
125   DCHECK(requests.size() == 1u || requests.size() == 2u);
126   if (requests.size() == 2) {
127     DCHECK(IsCameraOrMicPermission(requests[0]->GetPermissionRequestType()));
128     DCHECK(IsCameraOrMicPermission(requests[1]->GetPermissionRequestType()));
129     DCHECK_NE(requests[0]->GetPermissionRequestType(),
130               requests[1]->GetPermissionRequestType());
131   }
132 
133   chip_button_->SetText(GetPermissionMessage());
134   UpdatePermissionIconAndTextColor();
135 
136   SetVisible(true);
137   // TODO(olesiamarukhno): Add tests for animation logic.
138   animation_->Reset();
139   if (!delegate_->WasCurrentRequestAlreadyDisplayed())
140     animation_->Show();
141   requested_time_ = base::TimeTicks::Now();
142   PreferredSizeChanged();
143 }
144 
Hide()145 void PermissionChip::Hide() {
146   SetVisible(false);
147   timer_.AbandonAndStop();
148   delegate_ = nullptr;
149   if (prompt_bubble_)
150     prompt_bubble_->GetWidget()->Close();
151   already_recorded_interaction_ = false;
152   PreferredSizeChanged();
153 }
154 
CalculatePreferredSize() const155 gfx::Size PermissionChip::CalculatePreferredSize() const {
156   const int fixed_width = GetIconSize() + chip_button_->GetInsets().width();
157   const int collapsable_width =
158       chip_button_->GetPreferredSize().width() - fixed_width;
159   const int width =
160       std::round(collapsable_width * animation_->GetCurrentValue()) +
161       fixed_width;
162   return gfx::Size(width, GetHeightForWidth(width));
163 }
164 
OnMouseEntered(const ui::MouseEvent & event)165 void PermissionChip::OnMouseEntered(const ui::MouseEvent& event) {
166   // Restart the timer after user hovers the view.
167   StartCollapseTimer();
168 }
169 
OnThemeChanged()170 void PermissionChip::OnThemeChanged() {
171   View::OnThemeChanged();
172   UpdatePermissionIconAndTextColor();
173 }
174 
AnimationEnded(const gfx::Animation * animation)175 void PermissionChip::AnimationEnded(const gfx::Animation* animation) {
176   DCHECK_EQ(animation, animation_.get());
177   if (animation->GetCurrentValue() == 1.0)
178     StartCollapseTimer();
179 }
180 
AnimationProgressed(const gfx::Animation * animation)181 void PermissionChip::AnimationProgressed(const gfx::Animation* animation) {
182   DCHECK_EQ(animation, animation_.get());
183   PreferredSizeChanged();
184 }
185 
OnWidgetDestroying(views::Widget * widget)186 void PermissionChip::OnWidgetDestroying(views::Widget* widget) {
187   DCHECK_EQ(widget, prompt_bubble_->GetWidget());
188   widget->RemoveObserver(this);
189   prompt_bubble_ = nullptr;
190   animation_->Hide();
191 }
192 
IsBubbleShowing() const193 bool PermissionChip::IsBubbleShowing() const {
194   return prompt_bubble_ != nullptr;
195 }
196 
ChipButtonPressed()197 void PermissionChip::ChipButtonPressed() {
198   // The prompt bubble is either not opened yet or already closed on
199   // deactivation.
200   DCHECK(!prompt_bubble_);
201 
202   prompt_bubble_ = new PermissionPromptBubbleView(
203       browser_, delegate_, requested_time_, PermissionPromptStyle::kChip);
204   prompt_bubble_->Show();
205   prompt_bubble_->GetWidget()->AddObserver(this);
206   // Restart the timer after user clicks on the chip to open the bubble.
207   StartCollapseTimer();
208   if (!already_recorded_interaction_) {
209     base::UmaHistogramLongTimes("Permissions.Chip.TimeToInteraction",
210                                 base::TimeTicks::Now() - requested_time_);
211     already_recorded_interaction_ = true;
212   }
213 }
214 
Collapse()215 void PermissionChip::Collapse() {
216   if (IsMouseHovered() || prompt_bubble_) {
217     StartCollapseTimer();
218   } else {
219     animation_->Hide();
220   }
221 }
222 
StartCollapseTimer()223 void PermissionChip::StartCollapseTimer() {
224   constexpr auto kDelayBeforeCollapsingChip =
225       base::TimeDelta::FromMilliseconds(8000);
226   timer_.Start(FROM_HERE, kDelayBeforeCollapsingChip, this,
227                &PermissionChip::Collapse);
228 }
229 
GetIconSize() const230 int PermissionChip::GetIconSize() const {
231   return GetLayoutConstant(LOCATION_BAR_ICON_SIZE);
232 }
233 
UpdatePermissionIconAndTextColor()234 void PermissionChip::UpdatePermissionIconAndTextColor() {
235   if (!delegate_)
236     return;
237 
238   // Set label and icon color to be the same color.
239   SkColor enabled_text_color =
240       views::style::GetColor(*chip_button_, views::style::CONTEXT_BUTTON_MD,
241                              views::style::STYLE_DIALOG_BUTTON_DEFAULT);
242 
243   chip_button_->SetEnabledTextColors(enabled_text_color);
244   chip_button_->SetImageModel(
245       views::Button::STATE_NORMAL,
246       ui::ImageModel::FromVectorIcon(GetPermissionIconId(), enabled_text_color,
247                                      GetIconSize()));
248 }
249 
GetPermissionIconId()250 const gfx::VectorIcon& PermissionChip::GetPermissionIconId() {
251   auto requests = delegate_->Requests();
252   if (requests.size() == 1)
253     return requests[0]->GetIconId();
254 
255   // When we have two requests, it must be microphone & camera. Then we need to
256   // use the icon from the camera request.
257   return IsCameraPermission(requests[0]->GetPermissionRequestType())
258              ? requests[0]->GetIconId()
259              : requests[1]->GetIconId();
260 }
261 
GetPermissionMessage()262 base::string16 PermissionChip::GetPermissionMessage() {
263   auto requests = delegate_->Requests();
264 
265   return requests.size() == 1
266              ? requests[0]->GetChipText().value()
267              : l10n_util::GetStringUTF16(
268                    IDS_MEDIA_CAPTURE_VIDEO_AND_AUDIO_PERMISSION_CHIP);
269 }
270