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