extension_message_bubble_view.cc revision f8ee788a64d60abd8f2d742a5fdedde054ecd910
1// Copyright (c) 2013 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/extensions/extension_message_bubble_view.h"
6
7#include "base/strings/string_number_conversions.h"
8#include "base/strings/string_util.h"
9#include "base/strings/utf_string_conversions.h"
10#include "chrome/browser/extensions/dev_mode_bubble_controller.h"
11#include "chrome/browser/extensions/extension_action_manager.h"
12#include "chrome/browser/extensions/extension_message_bubble_controller.h"
13#include "chrome/browser/extensions/extension_service.h"
14#include "chrome/browser/extensions/proxy_overridden_bubble_controller.h"
15#include "chrome/browser/extensions/settings_api_bubble_controller.h"
16#include "chrome/browser/extensions/settings_api_helpers.h"
17#include "chrome/browser/extensions/suspicious_extension_bubble_controller.h"
18#include "chrome/browser/profiles/profile.h"
19#include "chrome/browser/ui/view_ids.h"
20#include "chrome/browser/ui/views/frame/browser_view.h"
21#include "chrome/browser/ui/views/toolbar/browser_actions_container.h"
22#include "chrome/browser/ui/views/toolbar/browser_actions_container_observer.h"
23#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
24#include "extensions/browser/extension_prefs.h"
25#include "extensions/browser/extension_system.h"
26#include "grit/locale_settings.h"
27#include "ui/accessibility/ax_view_state.h"
28#include "ui/base/resource/resource_bundle.h"
29#include "ui/views/controls/button/label_button.h"
30#include "ui/views/controls/label.h"
31#include "ui/views/controls/link.h"
32#include "ui/views/layout/grid_layout.h"
33#include "ui/views/view.h"
34#include "ui/views/widget/widget.h"
35
36namespace {
37
38base::LazyInstance<std::set<Profile*> > g_profiles_evaluated =
39    LAZY_INSTANCE_INITIALIZER;
40
41// Layout constants.
42const int kExtensionListPadding = 10;
43const int kInsetBottomRight = 13;
44const int kInsetLeft = 14;
45const int kInsetTop = 9;
46const int kHeadlineMessagePadding = 4;
47const int kHeadlineRowPadding = 10;
48const int kMessageBubblePadding = 11;
49
50// How many extensions to show in the bubble (max).
51const size_t kMaxExtensionsToShow = 7;
52
53// How long to wait until showing the bubble (in seconds).
54const int kBubbleAppearanceWaitTime = 5;
55
56}  // namespace
57
58namespace extensions {
59
60ExtensionMessageBubbleView::ExtensionMessageBubbleView(
61    views::View* anchor_view,
62    views::BubbleBorder::Arrow arrow_location,
63    scoped_ptr<extensions::ExtensionMessageBubbleController> controller)
64    : BubbleDelegateView(anchor_view, arrow_location),
65      weak_factory_(this),
66      controller_(controller.Pass()),
67      anchor_view_(anchor_view),
68      headline_(NULL),
69      learn_more_(NULL),
70      dismiss_button_(NULL),
71      link_clicked_(false),
72      action_taken_(false) {
73  DCHECK(anchor_view->GetWidget());
74  set_close_on_deactivate(controller_->CloseOnDeactivate());
75  set_close_on_esc(true);
76
77  // Compensate for built-in vertical padding in the anchor view's image.
78  set_anchor_view_insets(gfx::Insets(5, 0, 5, 0));
79}
80
81void ExtensionMessageBubbleView::OnActionButtonClicked(
82    const base::Closure& callback) {
83  action_callback_ = callback;
84}
85
86void ExtensionMessageBubbleView::OnDismissButtonClicked(
87    const base::Closure& callback) {
88  dismiss_callback_ = callback;
89}
90
91void ExtensionMessageBubbleView::OnLinkClicked(
92    const base::Closure& callback) {
93  link_callback_ = callback;
94}
95
96void ExtensionMessageBubbleView::Show() {
97  // Not showing the bubble right away (during startup) has a few benefits:
98  // We don't have to worry about focus being lost due to the Omnibox (or to
99  // other things that want focus at startup). This allows Esc to work to close
100  // the bubble and also solves the keyboard accessibility problem that comes
101  // with focus being lost (we don't have a good generic mechanism of injecting
102  // bubbles into the focus cycle). Another benefit of delaying the show is
103  // that fade-in works (the fade-in isn't apparent if the the bubble appears at
104  // startup).
105  base::MessageLoop::current()->PostDelayedTask(
106      FROM_HERE,
107      base::Bind(&ExtensionMessageBubbleView::ShowBubble,
108                 weak_factory_.GetWeakPtr()),
109      base::TimeDelta::FromSeconds(kBubbleAppearanceWaitTime));
110}
111
112void ExtensionMessageBubbleView::OnWidgetDestroying(views::Widget* widget) {
113  // To catch Esc, we monitor destroy message. Unless the link has been clicked,
114  // we assume Dismiss was the action taken.
115  if (!link_clicked_ && !action_taken_)
116    dismiss_callback_.Run();
117}
118
119////////////////////////////////////////////////////////////////////////////////
120// ExtensionMessageBubbleView - private.
121
122ExtensionMessageBubbleView::~ExtensionMessageBubbleView() {}
123
124void ExtensionMessageBubbleView::ShowBubble() {
125  GetWidget()->Show();
126}
127
128void ExtensionMessageBubbleView::Init() {
129  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
130
131  views::GridLayout* layout = views::GridLayout::CreatePanel(this);
132  layout->SetInsets(kInsetTop, kInsetLeft,
133                    kInsetBottomRight, kInsetBottomRight);
134  SetLayoutManager(layout);
135
136  ExtensionMessageBubbleController::Delegate* delegate =
137      controller_->delegate();
138
139  const int headline_column_set_id = 0;
140  views::ColumnSet* top_columns = layout->AddColumnSet(headline_column_set_id);
141  top_columns->AddColumn(views::GridLayout::LEADING, views::GridLayout::CENTER,
142                         0, views::GridLayout::USE_PREF, 0, 0);
143  top_columns->AddPaddingColumn(1, 0);
144  layout->StartRow(0, headline_column_set_id);
145
146  headline_ = new views::Label(delegate->GetTitle(),
147                               rb.GetFontList(ui::ResourceBundle::MediumFont));
148  layout->AddView(headline_);
149
150  layout->AddPaddingRow(0, kHeadlineRowPadding);
151
152  const int text_column_set_id = 1;
153  views::ColumnSet* upper_columns = layout->AddColumnSet(text_column_set_id);
154  upper_columns->AddColumn(
155      views::GridLayout::LEADING, views::GridLayout::LEADING,
156      0, views::GridLayout::USE_PREF, 0, 0);
157  layout->StartRow(0, text_column_set_id);
158
159  views::Label* message = new views::Label();
160  message->SetMultiLine(true);
161  message->SetHorizontalAlignment(gfx::ALIGN_LEFT);
162  message->SetText(delegate->GetMessageBody(
163      anchor_view_->id() == VIEW_ID_BROWSER_ACTION));
164  message->SizeToFit(views::Widget::GetLocalizedContentsWidth(
165      IDS_EXTENSION_WIPEOUT_BUBBLE_WIDTH_CHARS));
166  layout->AddView(message);
167
168  if (delegate->ShouldShowExtensionList()) {
169    const int extension_list_column_set_id = 2;
170    views::ColumnSet* middle_columns =
171        layout->AddColumnSet(extension_list_column_set_id);
172    middle_columns->AddPaddingColumn(0, kExtensionListPadding);
173    middle_columns->AddColumn(
174        views::GridLayout::LEADING, views::GridLayout::CENTER,
175        0, views::GridLayout::USE_PREF, 0, 0);
176
177    layout->StartRowWithPadding(0, extension_list_column_set_id,
178        0, kHeadlineMessagePadding);
179    views::Label* extensions = new views::Label();
180    extensions->SetMultiLine(true);
181    extensions->SetHorizontalAlignment(gfx::ALIGN_LEFT);
182
183    std::vector<base::string16> extension_list;
184    base::char16 bullet_point = 0x2022;
185
186    std::vector<base::string16> suspicious = controller_->GetExtensionList();
187    size_t i = 0;
188    for (; i < suspicious.size() && i < kMaxExtensionsToShow; ++i) {
189      // Add each extension with bullet point.
190      extension_list.push_back(
191          bullet_point + base::ASCIIToUTF16(" ") + suspicious[i]);
192    }
193
194    if (i > kMaxExtensionsToShow) {
195      base::string16 difference = base::IntToString16(i - kMaxExtensionsToShow);
196      extension_list.push_back(bullet_point + base::ASCIIToUTF16(" ") +
197          delegate->GetOverflowText(difference));
198    }
199
200    extensions->SetText(JoinString(extension_list, base::ASCIIToUTF16("\n")));
201    extensions->SizeToFit(views::Widget::GetLocalizedContentsWidth(
202        IDS_EXTENSION_WIPEOUT_BUBBLE_WIDTH_CHARS));
203    layout->AddView(extensions);
204  }
205
206  base::string16 action_button = delegate->GetActionButtonLabel();
207
208  const int action_row_column_set_id = 3;
209  views::ColumnSet* bottom_columns =
210      layout->AddColumnSet(action_row_column_set_id);
211  bottom_columns->AddColumn(views::GridLayout::LEADING,
212      views::GridLayout::CENTER, 0, views::GridLayout::USE_PREF, 0, 0);
213  bottom_columns->AddPaddingColumn(1, 0);
214  bottom_columns->AddColumn(views::GridLayout::TRAILING,
215      views::GridLayout::CENTER, 0, views::GridLayout::USE_PREF, 0, 0);
216  if (!action_button.empty()) {
217    bottom_columns->AddColumn(views::GridLayout::TRAILING,
218        views::GridLayout::CENTER, 0, views::GridLayout::USE_PREF, 0, 0);
219  }
220  layout->StartRowWithPadding(0, action_row_column_set_id,
221                              0, kMessageBubblePadding);
222
223  learn_more_ = new views::Link(delegate->GetLearnMoreLabel());
224  learn_more_->set_listener(this);
225  layout->AddView(learn_more_);
226
227  if (!action_button.empty()) {
228    action_button_ = new views::LabelButton(this, action_button.c_str());
229    action_button_->SetStyle(views::Button::STYLE_BUTTON);
230    layout->AddView(action_button_);
231  }
232
233  dismiss_button_ = new views::LabelButton(this,
234      delegate->GetDismissButtonLabel());
235  dismiss_button_->SetStyle(views::Button::STYLE_BUTTON);
236  layout->AddView(dismiss_button_);
237}
238
239void ExtensionMessageBubbleView::ButtonPressed(views::Button* sender,
240                                               const ui::Event& event) {
241  if (sender == action_button_) {
242    action_taken_ = true;
243    action_callback_.Run();
244  } else {
245    DCHECK_EQ(dismiss_button_, sender);
246  }
247  GetWidget()->Close();
248}
249
250void ExtensionMessageBubbleView::LinkClicked(views::Link* source,
251                                             int event_flags) {
252  DCHECK_EQ(learn_more_, source);
253  link_clicked_ = true;
254  link_callback_.Run();
255  GetWidget()->Close();
256}
257
258void ExtensionMessageBubbleView::GetAccessibleState(
259    ui::AXViewState* state) {
260  state->role = ui::AX_ROLE_ALERT;
261}
262
263void ExtensionMessageBubbleView::ViewHierarchyChanged(
264    const ViewHierarchyChangedDetails& details) {
265  if (details.is_add && details.child == this)
266    NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);
267}
268
269////////////////////////////////////////////////////////////////////////////////
270// ExtensionMessageBubbleFactory
271
272ExtensionMessageBubbleFactory::ExtensionMessageBubbleFactory(
273    Profile* profile,
274    ToolbarView* toolbar_view)
275    : profile_(profile),
276      toolbar_view_(toolbar_view),
277      shown_suspicious_extensions_bubble_(false),
278      shown_startup_override_extensions_bubble_(false),
279      shown_proxy_override_extensions_bubble_(false),
280      shown_dev_mode_extensions_bubble_(false),
281      is_observing_(false),
282      stage_(STAGE_START),
283      container_(NULL),
284      anchor_view_(NULL) {}
285
286ExtensionMessageBubbleFactory::~ExtensionMessageBubbleFactory() {
287  MaybeStopObserving();
288}
289
290void ExtensionMessageBubbleFactory::MaybeShow(views::View* anchor_view) {
291#if defined(OS_WIN)
292  bool is_initial_check = IsInitialProfileCheck(profile_->GetOriginalProfile());
293  RecordProfileCheck(profile_->GetOriginalProfile());
294
295  // The list of suspicious extensions takes priority over the dev mode bubble
296  // and the settings API bubble, since that needs to be shown as soon as we
297  // disable something. The settings API bubble is shown on first startup after
298  // an extension has changed the startup pages and it is acceptable if that
299  // waits until the next startup because of the suspicious extension bubble.
300  // The dev mode bubble is not time sensitive like the other two so we'll catch
301  // the dev mode extensions on the next startup/next window that opens. That
302  // way, we're not too spammy with the bubbles.
303  if (!shown_suspicious_extensions_bubble_ &&
304      MaybeShowSuspiciousExtensionsBubble(anchor_view))
305    return;
306
307  if (!shown_startup_override_extensions_bubble_ &&
308      is_initial_check &&
309      MaybeShowStartupOverrideExtensionsBubble(anchor_view))
310    return;
311
312  if (!shown_proxy_override_extensions_bubble_ &&
313      MaybeShowProxyOverrideExtensionsBubble(anchor_view))
314    return;
315
316  if (!shown_dev_mode_extensions_bubble_)
317    MaybeShowDevModeExtensionsBubble(anchor_view);
318#endif  // OS_WIN
319}
320
321bool ExtensionMessageBubbleFactory::MaybeShowSuspiciousExtensionsBubble(
322    views::View* anchor_view) {
323  DCHECK(!shown_suspicious_extensions_bubble_);
324
325  scoped_ptr<SuspiciousExtensionBubbleController> suspicious_extensions(
326      new SuspiciousExtensionBubbleController(profile_));
327  if (!suspicious_extensions->ShouldShow())
328    return false;
329
330  shown_suspicious_extensions_bubble_ = true;
331  SuspiciousExtensionBubbleController* weak_controller =
332      suspicious_extensions.get();
333  ExtensionMessageBubbleView* bubble_delegate = new ExtensionMessageBubbleView(
334      anchor_view,
335      views::BubbleBorder::TOP_RIGHT,
336      suspicious_extensions.PassAs<ExtensionMessageBubbleController>());
337
338  views::BubbleDelegateView::CreateBubble(bubble_delegate);
339  weak_controller->Show(bubble_delegate);
340
341  return true;
342}
343
344bool ExtensionMessageBubbleFactory::MaybeShowStartupOverrideExtensionsBubble(
345    views::View* anchor_view) {
346#if !defined(OS_WIN)
347  return false;
348#else
349  DCHECK(!shown_startup_override_extensions_bubble_);
350
351  const Extension* extension = GetExtensionOverridingStartupPages(profile_);
352  if (!extension)
353    return false;
354
355  scoped_ptr<SettingsApiBubbleController> settings_api_bubble(
356      new SettingsApiBubbleController(profile_,
357                                      BUBBLE_TYPE_STARTUP_PAGES));
358  if (!settings_api_bubble->ShouldShow(extension->id()))
359    return false;
360
361  shown_startup_override_extensions_bubble_ = true;
362  PrepareToHighlightExtensions(
363      settings_api_bubble.PassAs<ExtensionMessageBubbleController>(),
364      anchor_view);
365  return true;
366#endif
367}
368
369bool ExtensionMessageBubbleFactory::MaybeShowProxyOverrideExtensionsBubble(
370    views::View* anchor_view) {
371#if !defined(OS_WIN)
372  return false;
373#else
374  DCHECK(!shown_proxy_override_extensions_bubble_);
375
376  const Extension* extension = GetExtensionOverridingProxy(profile_);
377  if (!extension)
378    return false;
379
380  scoped_ptr<ProxyOverriddenBubbleController> proxy_bubble(
381      new ProxyOverriddenBubbleController(profile_));
382  if (!proxy_bubble->ShouldShow(extension->id()))
383    return false;
384
385  shown_proxy_override_extensions_bubble_ = true;
386  PrepareToHighlightExtensions(
387      proxy_bubble.PassAs<ExtensionMessageBubbleController>(), anchor_view);
388  return true;
389#endif
390}
391
392bool ExtensionMessageBubbleFactory::MaybeShowDevModeExtensionsBubble(
393    views::View* anchor_view) {
394  DCHECK(!shown_dev_mode_extensions_bubble_);
395
396  // Check the Developer Mode extensions.
397  scoped_ptr<DevModeBubbleController> dev_mode_extensions(
398      new DevModeBubbleController(profile_));
399
400  // Return early if we have none to show.
401  if (!dev_mode_extensions->ShouldShow())
402    return false;
403
404  shown_dev_mode_extensions_bubble_ = true;
405  PrepareToHighlightExtensions(
406      dev_mode_extensions.PassAs<ExtensionMessageBubbleController>(),
407      anchor_view);
408  return true;
409}
410
411void ExtensionMessageBubbleFactory::MaybeObserve() {
412  if (!is_observing_) {
413    is_observing_ = true;
414    container_->AddObserver(this);
415  }
416}
417
418void ExtensionMessageBubbleFactory::MaybeStopObserving() {
419  if (is_observing_) {
420    is_observing_ = false;
421    container_->RemoveObserver(this);
422  }
423}
424
425void ExtensionMessageBubbleFactory::RecordProfileCheck(Profile* profile) {
426  g_profiles_evaluated.Get().insert(profile);
427}
428
429bool ExtensionMessageBubbleFactory::IsInitialProfileCheck(Profile* profile) {
430  return g_profiles_evaluated.Get().count(profile) == 0;
431}
432
433void ExtensionMessageBubbleFactory::OnBrowserActionsContainerAnimationEnded() {
434  MaybeStopObserving();
435  if (stage_ == STAGE_START) {
436    HighlightExtensions();
437  } else if (stage_ == STAGE_HIGHLIGHTED) {
438    ShowHighlightingBubble();
439  } else {  // We shouldn't be observing if we've completed the process.
440    NOTREACHED();
441    Finish();
442  }
443}
444
445void ExtensionMessageBubbleFactory::OnBrowserActionsContainerDestroyed() {
446  // If the container associated with the bubble is destroyed, abandon the
447  // process.
448  Finish();
449}
450
451void ExtensionMessageBubbleFactory::PrepareToHighlightExtensions(
452    scoped_ptr<ExtensionMessageBubbleController> controller,
453    views::View* anchor_view) {
454  // We should be in the start stage (i.e., should not have a pending attempt to
455  // show a bubble).
456  DCHECK_EQ(stage_, STAGE_START);
457
458  // Prepare to display and highlight the extensions before showing the bubble.
459  // Since this is an asynchronous process, set member variables for later use.
460  controller_ = controller.Pass();
461  anchor_view_ = anchor_view;
462  container_ = toolbar_view_->browser_actions();
463
464  if (container_->animating())
465    MaybeObserve();
466  else
467    HighlightExtensions();
468}
469
470void ExtensionMessageBubbleFactory::HighlightExtensions() {
471  DCHECK_EQ(STAGE_START, stage_);
472  stage_ = STAGE_HIGHLIGHTED;
473
474  const ExtensionIdList extension_list = controller_->GetExtensionIdList();
475  DCHECK(!extension_list.empty());
476  ExtensionToolbarModel::Get(profile_)->HighlightExtensions(extension_list);
477  if (container_->animating())
478    MaybeObserve();
479  else
480    ShowHighlightingBubble();
481}
482
483void ExtensionMessageBubbleFactory::ShowHighlightingBubble() {
484  DCHECK_EQ(stage_, STAGE_HIGHLIGHTED);
485  stage_ = STAGE_COMPLETE;
486
487  views::View* reference_view = NULL;
488  if (container_->num_browser_actions() > 0)
489    reference_view = container_->GetBrowserActionViewAt(0);
490  if (reference_view && reference_view->visible())
491    anchor_view_ = reference_view;
492
493  ExtensionMessageBubbleController* weak_controller = controller_.get();
494  ExtensionMessageBubbleView* bubble_delegate =
495      new ExtensionMessageBubbleView(
496          anchor_view_,
497          views::BubbleBorder::TOP_RIGHT,
498          scoped_ptr<ExtensionMessageBubbleController>(
499              controller_.release()));
500  views::BubbleDelegateView::CreateBubble(bubble_delegate);
501  weak_controller->Show(bubble_delegate);
502
503  Finish();
504}
505
506void ExtensionMessageBubbleFactory::Finish() {
507  MaybeStopObserving();
508  controller_.reset();
509  anchor_view_ = NULL;
510  container_ = NULL;
511}
512
513}  // namespace extensions
514