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