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