1 // Copyright (c) 2012 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/exclusive_access_bubble_views.h"
6 
7 #include <utility>
8 
9 #include "base/i18n/case_conversion.h"
10 #include "base/location.h"
11 #include "base/macros.h"
12 #include "base/single_thread_task_runner.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/threading/thread_task_runner_handle.h"
15 #include "build/build_config.h"
16 #include "chrome/app/chrome_command_ids.h"
17 #include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
18 #include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
19 #include "chrome/browser/ui/views/exclusive_access_bubble_views_context.h"
20 #include "chrome/browser/ui/views/frame/immersive_mode_controller.h"
21 #include "chrome/browser/ui/views/frame/top_container_view.h"
22 #include "chrome/browser/ui/views/subtle_notification_view.h"
23 #include "chrome/grit/generated_resources.h"
24 #include "ui/accessibility/ax_enums.mojom.h"
25 #include "ui/accessibility/ax_node_data.h"
26 #include "ui/base/l10n/l10n_util.h"
27 #include "ui/events/keycodes/keyboard_codes.h"
28 #include "ui/gfx/animation/slide_animation.h"
29 #include "ui/gfx/canvas.h"
30 #include "ui/strings/grit/ui_strings.h"
31 #include "ui/views/bubble/bubble_border.h"
32 #include "ui/views/controls/link.h"
33 #include "ui/views/view.h"
34 #include "ui/views/widget/widget.h"
35 #include "url/gurl.h"
36 
37 #if defined(OS_WIN)
38 #include "ui/base/l10n/l10n_util_win.h"
39 #endif
40 
ExclusiveAccessBubbleViews(ExclusiveAccessBubbleViewsContext * context,const GURL & url,ExclusiveAccessBubbleType bubble_type,ExclusiveAccessBubbleHideCallback bubble_first_hide_callback)41 ExclusiveAccessBubbleViews::ExclusiveAccessBubbleViews(
42     ExclusiveAccessBubbleViewsContext* context,
43     const GURL& url,
44     ExclusiveAccessBubbleType bubble_type,
45     ExclusiveAccessBubbleHideCallback bubble_first_hide_callback)
46     : ExclusiveAccessBubble(context->GetExclusiveAccessManager(),
47                             url,
48                             bubble_type),
49       bubble_view_context_(context),
50       popup_(nullptr),
51       bubble_first_hide_callback_(std::move(bubble_first_hide_callback)),
52       animation_(new gfx::SlideAnimation(this)) {
53   // Create the contents view.
54   auto content_view = std::make_unique<SubtleNotificationView>();
55   view_ = content_view.get();
56 
57 #if defined(OS_CHROMEOS)
58   // Technically the exit fullscreen key on ChromeOS is F11 and the
59   // "Fullscreen" key on the keyboard is just translated to F11 or F4 (which
60   // is also a toggle-fullscreen command on ChromeOS). However most Chromebooks
61   // have media keys - including "fullscreen" - but not function keys, so
62   // instructing the user to "Press [F11] to exit fullscreen" isn't useful.
63   //
64   // An obvious solution might be to change the primary accelerator to the
65   // fullscreen key, but since translation to a function key is done at system
66   // level we can't actually do that. Instead we provide specific messaging for
67   // the platform here. (See crbug.com/1110468 for details.)
68   browser_fullscreen_exit_accelerator_ =
69       l10n_util::GetStringUTF16(IDS_APP_FULLSCREEN_KEY);
70 #else
71   ui::Accelerator accelerator(ui::VKEY_UNKNOWN, ui::EF_NONE);
72   bool got_accelerator =
73       bubble_view_context_->GetAcceleratorProvider()
74           ->GetAcceleratorForCommandId(IDC_FULLSCREEN, &accelerator);
75   DCHECK(got_accelerator);
76   browser_fullscreen_exit_accelerator_ = accelerator.GetShortcutText();
77 #endif
78 
79   UpdateViewContent(bubble_type_);
80 
81   // Initialize the popup.
82   popup_ = SubtleNotificationView::CreatePopupWidget(
83       bubble_view_context_->GetBubbleParentView(), std::move(content_view));
84 
85   gfx::Size size = GetPopupRect(true).size();
86   // Bounds are in screen coordinates.
87   popup_->SetBounds(GetPopupRect(false));
88   // Why is this special enough to require the "security surface" level? A
89   // decision was made a long time ago to not require confirmation when a site
90   // asks to go fullscreen, and that's not changing. However, a site going
91   // fullscreen is a big security risk, allowing phishing and other UI fakery.
92   // This bubble is the only defense that Chromium can provide against this
93   // attack, so it's important to order it above everything.
94   //
95   // On some platforms, pages can put themselves into fullscreen and then
96   // trigger other elements to cover up this bubble, elements that aren't fully
97   // under Chromium's control. See https://crbug.com/927150 for an example.
98   popup_->SetZOrderLevel(ui::ZOrderLevel::kSecuritySurface);
99   view_->SetBounds(0, 0, size.width(), size.height());
100   popup_->AddObserver(this);
101 
102   fullscreen_observer_.Add(bubble_view_context_->GetExclusiveAccessManager()
103                                ->fullscreen_controller());
104 
105   UpdateMouseWatcher();
106 }
107 
~ExclusiveAccessBubbleViews()108 ExclusiveAccessBubbleViews::~ExclusiveAccessBubbleViews() {
109   RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kInterrupted);
110 
111   popup_->RemoveObserver(this);
112 
113   // This is tricky.  We may be in an ATL message handler stack, in which case
114   // the popup cannot be deleted yet.  We also can't set the popup's ownership
115   // model to NATIVE_WIDGET_OWNS_WIDGET because if the user closed the last tab
116   // while in fullscreen mode, Windows has already destroyed the popup HWND by
117   // the time we get here, and thus either the popup will already have been
118   // deleted (if we set this in our constructor) or the popup will never get
119   // another OnFinalMessage() call (if not, as currently).  So instead, we tell
120   // the popup to synchronously hide, and then asynchronously close and delete
121   // itself.
122   popup_->Close();
123   base::ThreadTaskRunnerHandle::Get()->DeleteSoon(FROM_HERE, popup_);
124   CHECK(!views::WidgetObserver::IsInObserverList());
125 }
126 
UpdateContent(const GURL & url,ExclusiveAccessBubbleType bubble_type,ExclusiveAccessBubbleHideCallback bubble_first_hide_callback,bool force_update)127 void ExclusiveAccessBubbleViews::UpdateContent(
128     const GURL& url,
129     ExclusiveAccessBubbleType bubble_type,
130     ExclusiveAccessBubbleHideCallback bubble_first_hide_callback,
131     bool force_update) {
132   DCHECK_NE(EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE, bubble_type);
133   if (bubble_type_ == bubble_type && url_ == url && !force_update)
134     return;
135 
136   // Bubble maybe be re-used after timeout.
137   RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kInterrupted);
138 
139   bubble_first_hide_callback_ = std::move(bubble_first_hide_callback);
140 
141   url_ = url;
142   bubble_type_ = bubble_type;
143   UpdateViewContent(bubble_type_);
144 
145   gfx::Size size = GetPopupRect(true).size();
146   view_->SetSize(size);
147   popup_->SetBounds(GetPopupRect(false));
148   Show();
149 
150   // Stop watching the mouse even if UpdateMouseWatcher() will start watching
151   // it again so that the popup with the new content is visible for at least
152   // |kInitialDelayMs|.
153   StopWatchingMouse();
154 
155   UpdateMouseWatcher();
156 }
157 
RepositionIfVisible()158 void ExclusiveAccessBubbleViews::RepositionIfVisible() {
159 #if defined(OS_MAC)
160   // Due to a quirk on the Mac, the popup will not be visible for a short period
161   // of time after it is shown (it's asynchronous) so if we don't check the
162   // value of the animation we'll have a stale version of the bounds when we
163   // show it and it will appear in the wrong place - typically where the window
164   // was located before going to fullscreen.
165   if (popup_->IsVisible() || animation_->GetCurrentValue() > 0.0)
166 #else
167   if (popup_->IsVisible())
168 #endif
169     UpdateBounds();
170 }
171 
HideImmediately()172 void ExclusiveAccessBubbleViews::HideImmediately() {
173   if (!IsShowing() && !popup_->IsVisible())
174     return;
175 
176   RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kInterrupted);
177 
178   animation_->SetSlideDuration(base::TimeDelta::FromMilliseconds(150));
179   animation_->Hide();
180 }
181 
IsShowing() const182 bool ExclusiveAccessBubbleViews::IsShowing() const {
183   return animation_->is_animating() && animation_->IsShowing();
184 }
185 
GetView()186 views::View* ExclusiveAccessBubbleViews::GetView() {
187   return view_;
188 }
189 
UpdateMouseWatcher()190 void ExclusiveAccessBubbleViews::UpdateMouseWatcher() {
191   bool should_watch_mouse = popup_->IsVisible() || CanTriggerOnMouse();
192 
193   if (should_watch_mouse == IsWatchingMouse())
194     return;
195 
196   if (should_watch_mouse)
197     StartWatchingMouse();
198   else
199     StopWatchingMouse();
200 }
201 
UpdateBounds()202 void ExclusiveAccessBubbleViews::UpdateBounds() {
203   gfx::Rect popup_rect(GetPopupRect(false));
204   if (!popup_rect.IsEmpty()) {
205     popup_->SetBounds(popup_rect);
206     view_->SetY(popup_rect.height() - view_->height());
207   }
208 }
209 
UpdateViewContent(ExclusiveAccessBubbleType bubble_type)210 void ExclusiveAccessBubbleViews::UpdateViewContent(
211     ExclusiveAccessBubbleType bubble_type) {
212   DCHECK_NE(EXCLUSIVE_ACCESS_BUBBLE_TYPE_NONE, bubble_type);
213 
214   base::string16 accelerator;
215   if (bubble_type ==
216           EXCLUSIVE_ACCESS_BUBBLE_TYPE_BROWSER_FULLSCREEN_EXIT_INSTRUCTION ||
217       bubble_type ==
218           EXCLUSIVE_ACCESS_BUBBLE_TYPE_EXTENSION_FULLSCREEN_EXIT_INSTRUCTION) {
219     accelerator = browser_fullscreen_exit_accelerator_;
220   } else {
221     accelerator = l10n_util::GetStringUTF16(IDS_APP_ESC_KEY);
222   }
223 #if defined(OS_MAC)
224   // Mac keyboards use lowercase for everything except function keys, which are
225   // typically reserved for system use. Since |accelerator| is placed in a box
226   // to make it look like a keyboard key it looks weird to not follow suit.
227   accelerator = base::i18n::ToLower(accelerator);
228 #endif
229   view_->UpdateContent(GetInstructionText(accelerator));
230 }
231 
GetBrowserRootView() const232 views::View* ExclusiveAccessBubbleViews::GetBrowserRootView() const {
233   return bubble_view_context_->GetBubbleAssociatedWidget()->GetRootView();
234 }
235 
AnimationProgressed(const gfx::Animation * animation)236 void ExclusiveAccessBubbleViews::AnimationProgressed(
237     const gfx::Animation* animation) {
238   float opacity = static_cast<float>(animation_->CurrentValueBetween(0.0, 1.0));
239   if (opacity == 0) {
240     popup_->Hide();
241   } else {
242     popup_->Show();
243     popup_->SetOpacity(opacity);
244   }
245 }
246 
AnimationEnded(const gfx::Animation * animation)247 void ExclusiveAccessBubbleViews::AnimationEnded(
248     const gfx::Animation* animation) {
249   if (animation_->IsShowing())
250     GetView()->NotifyAccessibilityEvent(ax::mojom::Event::kAlert, true);
251   AnimationProgressed(animation);
252 }
253 
GetPopupRect(bool ignore_animation_state) const254 gfx::Rect ExclusiveAccessBubbleViews::GetPopupRect(
255     bool ignore_animation_state) const {
256   gfx::Size size(view_->GetPreferredSize());
257   gfx::Rect widget_bounds = bubble_view_context_->GetClientAreaBoundsInScreen();
258   int x = widget_bounds.x() + (widget_bounds.width() - size.width()) / 2;
259 
260   int top_container_bottom = widget_bounds.y();
261   if (bubble_view_context_->IsImmersiveModeEnabled()) {
262     // Skip querying the top container height in non-immersive fullscreen
263     // because:
264     // - The top container height is always zero in non-immersive fullscreen.
265     // - Querying the top container height may return the height before entering
266     //   fullscreen because layout is disabled while entering fullscreen.
267     // A visual glitch due to the delayed layout is avoided in immersive
268     // fullscreen because entering fullscreen starts with the top container
269     // revealed. When revealed, the top container has the same height as before
270     // entering fullscreen.
271     top_container_bottom =
272         bubble_view_context_->GetTopContainerBoundsInScreen().bottom();
273   }
274   // |desired_top| is the top of the bubble area including the shadow.
275   int desired_top = kSimplifiedPopupTopPx - view_->border()->GetInsets().top();
276   int y = top_container_bottom + desired_top;
277 
278   return gfx::Rect(gfx::Point(x, y), size);
279 }
280 
GetCursorScreenPoint()281 gfx::Point ExclusiveAccessBubbleViews::GetCursorScreenPoint() {
282   return bubble_view_context_->GetCursorPointInParent();
283 }
284 
WindowContainsPoint(gfx::Point pos)285 bool ExclusiveAccessBubbleViews::WindowContainsPoint(gfx::Point pos) {
286   return GetBrowserRootView()->HitTestPoint(pos);
287 }
288 
IsWindowActive()289 bool ExclusiveAccessBubbleViews::IsWindowActive() {
290   return bubble_view_context_->GetBubbleAssociatedWidget()->IsActive();
291 }
292 
Hide()293 void ExclusiveAccessBubbleViews::Hide() {
294   // This function is guarded by the |ExclusiveAccessBubble::hide_timeout_|
295   // timer, so the bubble has been displayed for at least
296   // |ExclusiveAccessBubble::kInitialDelayMs|.
297   DCHECK(!IsHideTimeoutRunning());
298   RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason::kTimeout);
299 
300   animation_->SetSlideDuration(base::TimeDelta::FromMilliseconds(700));
301   animation_->Hide();
302 }
303 
Show()304 void ExclusiveAccessBubbleViews::Show() {
305   if (animation_->IsShowing())
306     return;
307   animation_->SetSlideDuration(base::TimeDelta::FromMilliseconds(350));
308   animation_->Show();
309 }
310 
IsAnimating()311 bool ExclusiveAccessBubbleViews::IsAnimating() {
312   return animation_->is_animating();
313 }
314 
CanTriggerOnMouse() const315 bool ExclusiveAccessBubbleViews::CanTriggerOnMouse() const {
316   return bubble_view_context_->CanTriggerOnMouse();
317 }
318 
OnFullscreenStateChanged()319 void ExclusiveAccessBubbleViews::OnFullscreenStateChanged() {
320   UpdateMouseWatcher();
321 }
322 
OnWidgetDestroyed(views::Widget * widget)323 void ExclusiveAccessBubbleViews::OnWidgetDestroyed(views::Widget* widget) {
324   // Although SubtleNotificationView uses WIDGET_OWNS_NATIVE_WIDGET, a close can
325   // originate from the OS or some Chrome shutdown codepaths that bypass the
326   // destructor.
327   views::Widget* popup_on_stack = popup_;
328   DCHECK(popup_on_stack->HasObserver(this));
329 
330   // Get ourselves destroyed. Calling ExitExclusiveAccess() won't work because
331   // the parent window might be destroyed as well, so asking it to exit
332   // fullscreen would be a bad idea.
333   bubble_view_context_->DestroyAnyExclusiveAccessBubble();
334 
335   // Note: |this| is destroyed on the line above. Check that the destructor was
336   // invoked. This is safe to do since |popup_| is deleted via a posted task.
337   DCHECK(!popup_on_stack->HasObserver(this));
338 }
339 
OnWidgetVisibilityChanged(views::Widget * widget,bool visible)340 void ExclusiveAccessBubbleViews::OnWidgetVisibilityChanged(
341     views::Widget* widget,
342     bool visible) {
343   UpdateMouseWatcher();
344 }
345 
RunHideCallbackIfNeeded(ExclusiveAccessBubbleHideReason reason)346 void ExclusiveAccessBubbleViews::RunHideCallbackIfNeeded(
347     ExclusiveAccessBubbleHideReason reason) {
348   if (bubble_first_hide_callback_)
349     std::move(bubble_first_hide_callback_).Run(reason);
350 }
351