| // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/extensions/extension_message_bubble_controller.h" |
| |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/lazy_instance.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/browser_list.h" |
| #include "chrome/browser/ui/toolbar/toolbar_actions_model.h" |
| #include "chrome/common/url_constants.h" |
| #include "components/strings/grit/components_strings.h" |
| #include "extensions/browser/extension_prefs.h" |
| #include "extensions/browser/extension_registry.h" |
| #include "extensions/browser/extension_system.h" |
| #include "ui/base/l10n/l10n_util.h" |
| |
| namespace extensions { |
| |
| namespace { |
| |
| // How many extensions to show in the bubble (max). |
| const int kMaxExtensionsToShow = 7; |
| |
| // Whether or not to ignore the learn more link navigation for testing. |
| bool g_should_ignore_learn_more_for_testing = false; |
| |
| } // namespace |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ExtensionMessageBubbleController::Delegate |
| |
| ExtensionMessageBubbleController::Delegate::Delegate(Profile* profile) |
| : profile_(profile), |
| service_(ExtensionSystem::Get(profile)->extension_service()), |
| registry_(ExtensionRegistry::Get(profile)) { |
| } |
| |
| ExtensionMessageBubbleController::Delegate::~Delegate() {} |
| |
| base::string16 ExtensionMessageBubbleController::Delegate::GetLearnMoreLabel() |
| const { |
| return l10n_util::GetStringUTF16(IDS_LEARN_MORE); |
| } |
| |
| void ExtensionMessageBubbleController::Delegate::OnAction() {} |
| |
| bool ExtensionMessageBubbleController::Delegate::HasBubbleInfoBeenAcknowledged( |
| const std::string& extension_id) { |
| std::string pref_name = get_acknowledged_flag_pref_name(); |
| if (pref_name.empty()) |
| return false; |
| bool pref_state = false; |
| extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_); |
| prefs->ReadPrefAsBoolean(extension_id, pref_name, &pref_state); |
| return pref_state; |
| } |
| |
| void ExtensionMessageBubbleController::Delegate::SetBubbleInfoBeenAcknowledged( |
| const std::string& extension_id, |
| bool value) { |
| std::string pref_name = get_acknowledged_flag_pref_name(); |
| if (pref_name.empty()) |
| return; |
| extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_); |
| prefs->UpdateExtensionPref( |
| extension_id, pref_name, |
| value ? std::make_unique<base::Value>(value) : nullptr); |
| } |
| |
| std::string |
| ExtensionMessageBubbleController::Delegate::get_acknowledged_flag_pref_name() |
| const { |
| return acknowledged_pref_name_; |
| } |
| |
| void ExtensionMessageBubbleController::Delegate:: |
| set_acknowledged_flag_pref_name(const std::string& pref_name) { |
| acknowledged_pref_name_ = pref_name; |
| } |
| |
| //////////////////////////////////////////////////////////////////////////////// |
| // ExtensionMessageBubbleController |
| |
| ExtensionMessageBubbleController::ExtensionMessageBubbleController( |
| Delegate* delegate, |
| Browser* browser) |
| : browser_(browser), |
| model_(ToolbarActionsModel::Get(browser_->profile())), |
| user_action_(ACTION_BOUNDARY), |
| delegate_(delegate), |
| initialized_(false), |
| is_highlighting_(false), |
| is_active_bubble_(false), |
| extension_registry_observer_(this), |
| browser_list_observer_(this) { |
| extension_registry_observer_.Add(ExtensionRegistry::Get(browser_->profile())); |
| browser_list_observer_.Add(BrowserList::GetInstance()); |
| } |
| |
| ExtensionMessageBubbleController::~ExtensionMessageBubbleController() { |
| if (is_active_bubble_) |
| model_->set_has_active_bubble(false); |
| if (is_highlighting_) |
| model_->StopHighlighting(); |
| } |
| |
| Profile* ExtensionMessageBubbleController::profile() { |
| return browser_->profile(); |
| } |
| |
| bool ExtensionMessageBubbleController::ShouldShow() { |
| // In the case when there are multiple extensions in the list, we need to |
| // check if each extension entry is still installed, and, if not, remove it |
| // from the list. |
| UpdateExtensionIdList(); |
| return !GetExtensionIdList().empty() && |
| (!model_->has_active_bubble() || is_active_bubble_) && |
| delegate_->ShouldShow(GetExtensionIdList()); |
| } |
| |
| std::vector<base::string16> |
| ExtensionMessageBubbleController::GetExtensionList() { |
| ExtensionIdList* list = GetOrCreateExtensionList(); |
| if (list->empty()) |
| return std::vector<base::string16>(); |
| |
| ExtensionRegistry* registry = ExtensionRegistry::Get(profile()); |
| std::vector<base::string16> return_value; |
| for (const std::string& id : *list) { |
| const Extension* extension = |
| registry->GetExtensionById(id, ExtensionRegistry::EVERYTHING); |
| return_value.push_back(base::UTF8ToUTF16(extension->name())); |
| } |
| return return_value; |
| } |
| |
| base::string16 ExtensionMessageBubbleController::GetExtensionListForDisplay() { |
| if (!delegate_->ShouldShowExtensionList()) |
| return base::string16(); |
| |
| std::vector<base::string16> extension_list = GetExtensionList(); |
| if (extension_list.size() > kMaxExtensionsToShow) { |
| int old_size = extension_list.size(); |
| extension_list.erase(extension_list.begin() + kMaxExtensionsToShow, |
| extension_list.end()); |
| extension_list.push_back(delegate_->GetOverflowText(base::IntToString16( |
| old_size - kMaxExtensionsToShow))); |
| } |
| const base::char16 bullet_point = 0x2022; |
| base::string16 prefix = bullet_point + base::ASCIIToUTF16(" "); |
| for (base::string16& str : extension_list) |
| str.insert(0, prefix); |
| return base::JoinString(extension_list, base::ASCIIToUTF16("\n")); |
| } |
| |
| const ExtensionIdList& ExtensionMessageBubbleController::GetExtensionIdList() { |
| return *GetOrCreateExtensionList(); |
| } |
| |
| void ExtensionMessageBubbleController::UpdateExtensionIdList() { |
| ExtensionIdList* extension_ids = GetOrCreateExtensionList(); |
| ExtensionRegistry* registry = ExtensionRegistry::Get(profile()); |
| int include_mask = delegate_->ShouldLimitToEnabledExtensions() |
| ? ExtensionRegistry::ENABLED |
| : ExtensionRegistry::EVERYTHING; |
| for (auto iter = extension_ids->begin(); iter != extension_ids->end();) { |
| const Extension* extension = |
| registry->GetExtensionById(*iter, include_mask); |
| if (extension) |
| ++iter; |
| else |
| iter = extension_ids->erase(iter); |
| } |
| } |
| |
| bool ExtensionMessageBubbleController::CloseOnDeactivate() { |
| return delegate_->ShouldCloseOnDeactivate(); |
| } |
| |
| void ExtensionMessageBubbleController::HighlightExtensionsIfNecessary() { |
| DCHECK(is_active_bubble_); |
| if (delegate_->ShouldHighlightExtensions() && !is_highlighting_) { |
| is_highlighting_ = true; |
| const ExtensionIdList& extension_ids = GetExtensionIdList(); |
| DCHECK(!extension_ids.empty()); |
| model_->HighlightActions(extension_ids, |
| ToolbarActionsModel::HIGHLIGHT_WARNING); |
| } |
| } |
| |
| void ExtensionMessageBubbleController::OnShown( |
| const base::Closure& close_bubble_callback) { |
| close_bubble_callback_ = close_bubble_callback; |
| DCHECK(is_active_bubble_); |
| delegate_->OnShown(GetExtensionIdList()); |
| |
| if (!extension_registry_observer_.IsObserving( |
| ExtensionRegistry::Get(browser_->profile()))) { |
| extension_registry_observer_.Add( |
| ExtensionRegistry::Get(browser_->profile())); |
| } |
| } |
| |
| void ExtensionMessageBubbleController::OnBubbleAction() { |
| // In addition to closing the bubble, OnBubbleAction() may result in a removal |
| // or disabling of the extension. To prevent triggering OnExtensionUnloaded(), |
| // which will also try to close the bubble, the controller's extension |
| // registry observer is removed. Note, we do not remove the extension registry |
| // observer in the cases of OnBubbleDismiss() and OnLinkedClicked() since they |
| // do not result in extensions being unloaded. |
| extension_registry_observer_.RemoveAll(); |
| DCHECK_EQ(ACTION_BOUNDARY, user_action_); |
| user_action_ = ACTION_EXECUTE; |
| |
| delegate_->LogAction(ACTION_EXECUTE); |
| delegate_->PerformAction(*GetOrCreateExtensionList()); |
| |
| OnClose(); |
| } |
| |
| void ExtensionMessageBubbleController::OnBubbleDismiss( |
| bool closed_by_deactivation) { |
| // OnBubbleDismiss() can be called twice when we receive multiple |
| // "OnWidgetDestroying" notifications (this can at least happen when we close |
| // a window with a notification open). Handle this gracefully. |
| if (user_action_ != ACTION_BOUNDARY) { |
| DCHECK(user_action_ == ACTION_DISMISS_USER_ACTION || |
| user_action_ == ACTION_DISMISS_DEACTIVATION); |
| return; |
| } |
| |
| user_action_ = closed_by_deactivation ? ACTION_DISMISS_DEACTIVATION |
| : ACTION_DISMISS_USER_ACTION; |
| |
| delegate_->LogAction(user_action_); |
| |
| OnClose(); |
| } |
| |
| void ExtensionMessageBubbleController::OnLinkClicked() { |
| DCHECK_EQ(ACTION_BOUNDARY, user_action_); |
| user_action_ = ACTION_LEARN_MORE; |
| |
| delegate_->LogAction(ACTION_LEARN_MORE); |
| // Opening a new tab for the learn more link can cause the bubble to close, so |
| // perform our cleanup here before opening the new tab. |
| OnClose(); |
| if (!g_should_ignore_learn_more_for_testing) { |
| browser_->OpenURL(content::OpenURLParams( |
| delegate_->GetLearnMoreUrl(), content::Referrer(), |
| WindowOpenDisposition::NEW_FOREGROUND_TAB, ui::PAGE_TRANSITION_LINK, |
| false)); |
| } |
| // Warning: |this| may be deleted here! |
| } |
| |
| void ExtensionMessageBubbleController::SetIsActiveBubble() { |
| DCHECK(!is_active_bubble_); |
| DCHECK(!model_->has_active_bubble()); |
| is_active_bubble_ = true; |
| model_->set_has_active_bubble(true); |
| } |
| |
| // static |
| void ExtensionMessageBubbleController::set_should_ignore_learn_more_for_testing( |
| bool should_ignore) { |
| g_should_ignore_learn_more_for_testing = should_ignore; |
| } |
| |
| void ExtensionMessageBubbleController::HandleExtensionUnloadOrUninstall() { |
| UpdateExtensionIdList(); |
| // If the callback is set, then that means that OnShown() was called, and the |
| // bubble was displayed. |
| if (close_bubble_callback_ && GetExtensionIdList().empty()) { |
| base::ResetAndReturn(&close_bubble_callback_).Run(); |
| } |
| // If the bubble refers to multiple extensions, we do not close the bubble. |
| } |
| |
| void ExtensionMessageBubbleController::OnExtensionUnloaded( |
| content::BrowserContext* browser_context, |
| const Extension* extension, |
| UnloadedExtensionReason reason) { |
| HandleExtensionUnloadOrUninstall(); |
| } |
| |
| void ExtensionMessageBubbleController::OnExtensionUninstalled( |
| content::BrowserContext* browser_context, |
| const Extension* extension, |
| UninstallReason reason) { |
| HandleExtensionUnloadOrUninstall(); |
| } |
| |
| void ExtensionMessageBubbleController::OnShutdown(ExtensionRegistry* registry) { |
| // It is possible that the extension registry is destroyed before the |
| // controller. In such case, the controller should no longer observe the |
| // registry. |
| extension_registry_observer_.Remove(registry); |
| } |
| |
| void ExtensionMessageBubbleController::OnBrowserRemoved(Browser* browser) { |
| extension_registry_observer_.RemoveAll(); |
| if (browser == browser_) { |
| if (is_highlighting_) { |
| model_->StopHighlighting(); |
| is_highlighting_ = false; |
| } |
| if (is_active_bubble_) { |
| model_->set_has_active_bubble(false); |
| is_active_bubble_ = false; |
| } |
| } |
| } |
| |
| void ExtensionMessageBubbleController::AcknowledgeExtensions() { |
| ExtensionIdList* list = GetOrCreateExtensionList(); |
| for (ExtensionIdList::const_iterator it = list->begin(); |
| it != list->end(); ++it) |
| delegate_->AcknowledgeExtension(*it, user_action_); |
| } |
| |
| ExtensionIdList* ExtensionMessageBubbleController::GetOrCreateExtensionList() { |
| if (!initialized_) { |
| ExtensionRegistry* registry = ExtensionRegistry::Get(profile()); |
| std::unique_ptr<const ExtensionSet> all_extensions; |
| if (!delegate_->ShouldLimitToEnabledExtensions()) |
| all_extensions = registry->GenerateInstalledExtensionsSet(); |
| const ExtensionSet& extensions_to_check = |
| all_extensions ? *all_extensions : registry->enabled_extensions(); |
| for (const scoped_refptr<const Extension>& extension : |
| extensions_to_check) { |
| if (delegate_->ShouldIncludeExtension(extension.get())) |
| extension_list_.push_back(extension->id()); |
| } |
| |
| delegate_->LogExtensionCount(extension_list_.size()); |
| initialized_ = true; |
| } |
| |
| return &extension_list_; |
| } |
| |
| void ExtensionMessageBubbleController::OnClose() { |
| DCHECK_NE(ACTION_BOUNDARY, user_action_); |
| // If the bubble was closed due to deactivation, don't treat it as |
| // acknowledgment so that the user will see the bubble again (until they |
| // explicitly take an action). |
| if (user_action_ != ACTION_DISMISS_DEACTIVATION || |
| delegate_->ShouldAcknowledgeOnDeactivate()) { |
| AcknowledgeExtensions(); |
| delegate_->OnAction(); |
| } |
| |
| extension_registry_observer_.RemoveAll(); |
| } |
| |
| } // namespace extensions |