blob: 4034bbbb9ee5c94a59641fe9fa1713521486218d [file] [log] [blame]
// 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