blob: f1ed3d8c2ee4ba3f243598e555aa9bf4d2f57bc5 [file] [log] [blame]
[email protected]c72ebfe2013-12-13 21:57:531// 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/extensions/extension_message_bubble_controller.h"
6
Jinho Bangb5216cec2018-01-17 19:43:117#include <memory>
Daniel Chengd78c0252019-04-26 17:22:118#include <utility>
Jinho Bangb5216cec2018-01-17 19:43:119
[email protected]c72ebfe2013-12-13 21:57:5310#include "base/bind.h"
rdevlin.cronin486b20e2015-11-20 17:20:3511#include "base/lazy_instance.h"
asvitkineaa060312016-09-01 22:44:1312#include "base/metrics/histogram_macros.h"
rdevlin.cronina28846d2015-04-30 23:12:1913#include "base/strings/string_number_conversions.h"
14#include "base/strings/string_util.h"
[email protected]c72ebfe2013-12-13 21:57:5315#include "base/strings/utf_string_conversions.h"
[email protected]f8454484652014-02-27 04:20:4216#include "chrome/browser/profiles/profile.h"
[email protected]c72ebfe2013-12-13 21:57:5317#include "chrome/browser/ui/browser.h"
rdevlin.cronin89370302016-04-19 14:24:4018#include "chrome/browser/ui/browser_list.h"
apaciblef9cfc4d2015-08-18 05:14:1419#include "chrome/browser/ui/toolbar/toolbar_actions_model.h"
[email protected]c72ebfe2013-12-13 21:57:5320#include "chrome/common/url_constants.h"
thestig4a2e88e2016-08-27 23:23:5121#include "components/strings/grit/components_strings.h"
merkulova9be182e2014-10-07 14:57:5022#include "extensions/browser/extension_prefs.h"
rdevlin.cronin0ba2a4c2015-08-06 18:40:1923#include "extensions/browser/extension_system.h"
merkulova9be182e2014-10-07 14:57:5024#include "ui/base/l10n/l10n_util.h"
[email protected]c72ebfe2013-12-13 21:57:5325
26namespace extensions {
27
rdevlin.cronina28846d2015-04-30 23:12:1928namespace {
rdevlin.cronin486b20e2015-11-20 17:20:3529
rdevlin.cronina28846d2015-04-30 23:12:1930// How many extensions to show in the bubble (max).
31const int kMaxExtensionsToShow = 7;
rdevlin.cronin0ba2a4c2015-08-06 18:40:1932
33// Whether or not to ignore the learn more link navigation for testing.
34bool g_should_ignore_learn_more_for_testing = false;
rdevlin.cronin486b20e2015-11-20 17:20:3535
rdevlin.cronin486b20e2015-11-20 17:20:3536} // namespace
rdevlin.cronina28846d2015-04-30 23:12:1937
[email protected]c72ebfe2013-12-13 21:57:5338////////////////////////////////////////////////////////////////////////////////
[email protected]4f2f353d2014-01-14 11:21:0939// ExtensionMessageBubbleController::Delegate
40
merkulova9be182e2014-10-07 14:57:5041ExtensionMessageBubbleController::Delegate::Delegate(Profile* profile)
rdevlin.cronin0ba2a4c2015-08-06 18:40:1942 : profile_(profile),
43 service_(ExtensionSystem::Get(profile)->extension_service()),
44 registry_(ExtensionRegistry::Get(profile)) {
[email protected]4f2f353d2014-01-14 11:21:0945}
46
Catherine Mullings478a0432017-11-22 07:06:0647ExtensionMessageBubbleController::Delegate::~Delegate() {}
[email protected]4f2f353d2014-01-14 11:21:0948
merkulova9be182e2014-10-07 14:57:5049base::string16 ExtensionMessageBubbleController::Delegate::GetLearnMoreLabel()
50 const {
51 return l10n_util::GetStringUTF16(IDS_LEARN_MORE);
52}
53
Catherine Mullings478a0432017-11-22 07:06:0654void ExtensionMessageBubbleController::Delegate::OnAction() {}
rdevlin.cronin486b20e2015-11-20 17:20:3555
merkulova9be182e2014-10-07 14:57:5056bool ExtensionMessageBubbleController::Delegate::HasBubbleInfoBeenAcknowledged(
57 const std::string& extension_id) {
58 std::string pref_name = get_acknowledged_flag_pref_name();
59 if (pref_name.empty())
60 return false;
61 bool pref_state = false;
62 extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
63 prefs->ReadPrefAsBoolean(extension_id, pref_name, &pref_state);
64 return pref_state;
65}
66
67void ExtensionMessageBubbleController::Delegate::SetBubbleInfoBeenAcknowledged(
68 const std::string& extension_id,
69 bool value) {
70 std::string pref_name = get_acknowledged_flag_pref_name();
71 if (pref_name.empty())
72 return;
73 extensions::ExtensionPrefs* prefs = extensions::ExtensionPrefs::Get(profile_);
vabrfb687dc2017-03-22 11:40:5774 prefs->UpdateExtensionPref(
75 extension_id, pref_name,
Jinho Bangb5216cec2018-01-17 19:43:1176 value ? std::make_unique<base::Value>(value) : nullptr);
merkulova9be182e2014-10-07 14:57:5077}
78
merkulova9be182e2014-10-07 14:57:5079std::string
80ExtensionMessageBubbleController::Delegate::get_acknowledged_flag_pref_name()
81 const {
82 return acknowledged_pref_name_;
83}
84
ki.stfuf38f9312015-09-27 14:44:3785void ExtensionMessageBubbleController::Delegate::
86 set_acknowledged_flag_pref_name(const std::string& pref_name) {
merkulova9be182e2014-10-07 14:57:5087 acknowledged_pref_name_ = pref_name;
88}
89
[email protected]4f2f353d2014-01-14 11:21:0990////////////////////////////////////////////////////////////////////////////////
[email protected]c72ebfe2013-12-13 21:57:5391// ExtensionMessageBubbleController
92
93ExtensionMessageBubbleController::ExtensionMessageBubbleController(
rdevlin.cronin0ba2a4c2015-08-06 18:40:1994 Delegate* delegate,
95 Browser* browser)
96 : browser_(browser),
rdevlin.croninaf64c8c2016-06-21 00:16:0097 model_(ToolbarActionsModel::Get(browser_->profile())),
[email protected]c72ebfe2013-12-13 21:57:5398 user_action_(ACTION_BOUNDARY),
99 delegate_(delegate),
rdevlin.cronin9bd430a2015-05-08 18:06:18100 initialized_(false),
rdevlin.croninaf64c8c2016-06-21 00:16:00101 is_highlighting_(false),
Evan Stade75872a62019-09-06 21:17:38102 is_active_bubble_(false) {
Catherine Mullings8fe05b62017-08-16 17:14:45103 extension_registry_observer_.Add(ExtensionRegistry::Get(browser_->profile()));
Evan Stade75872a62019-09-06 21:17:38104 BrowserList::AddObserver(this);
[email protected]c72ebfe2013-12-13 21:57:53105}
106
107ExtensionMessageBubbleController::~ExtensionMessageBubbleController() {
Evan Stade75872a62019-09-06 21:17:38108 BrowserList::RemoveObserver(this);
rdevlin.croninaf64c8c2016-06-21 00:16:00109 if (is_active_bubble_)
110 model_->set_has_active_bubble(false);
111 if (is_highlighting_)
112 model_->StopHighlighting();
[email protected]c72ebfe2013-12-13 21:57:53113}
114
rdevlin.cronin0ba2a4c2015-08-06 18:40:19115Profile* ExtensionMessageBubbleController::profile() {
116 return browser_->profile();
117}
118
rdevlin.cronincce78d02015-09-24 19:50:55119bool ExtensionMessageBubbleController::ShouldShow() {
catmullings6e557722016-12-17 02:55:26120 // In the case when there are multiple extensions in the list, we need to
121 // check if each extension entry is still installed, and, if not, remove it
122 // from the list.
123 UpdateExtensionIdList();
Catherine Mullings478a0432017-11-22 07:06:06124 return !GetExtensionIdList().empty() &&
rdevlin.croninaf64c8c2016-06-21 00:16:00125 (!model_->has_active_bubble() || is_active_bubble_) &&
Catherine Mullings478a0432017-11-22 07:06:06126 delegate_->ShouldShow(GetExtensionIdList());
rdevlin.cronincce78d02015-09-24 19:50:55127}
128
[email protected]4f58ac52013-12-17 12:00:42129std::vector<base::string16>
[email protected]c72ebfe2013-12-13 21:57:53130ExtensionMessageBubbleController::GetExtensionList() {
131 ExtensionIdList* list = GetOrCreateExtensionList();
132 if (list->empty())
[email protected]4f58ac52013-12-17 12:00:42133 return std::vector<base::string16>();
[email protected]c72ebfe2013-12-13 21:57:53134
rdevlin.cronin0ba2a4c2015-08-06 18:40:19135 ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
[email protected]4f58ac52013-12-17 12:00:42136 std::vector<base::string16> return_value;
rdevlin.cronincce78d02015-09-24 19:50:55137 for (const std::string& id : *list) {
[email protected]f47f7172014-03-19 19:27:10138 const Extension* extension =
rdevlin.cronincce78d02015-09-24 19:50:55139 registry->GetExtensionById(id, ExtensionRegistry::EVERYTHING);
catmullings6e557722016-12-17 02:55:26140 return_value.push_back(base::UTF8ToUTF16(extension->name()));
[email protected]c72ebfe2013-12-13 21:57:53141 }
142 return return_value;
143}
144
rdevlin.cronina28846d2015-04-30 23:12:19145base::string16 ExtensionMessageBubbleController::GetExtensionListForDisplay() {
146 if (!delegate_->ShouldShowExtensionList())
147 return base::string16();
148
149 std::vector<base::string16> extension_list = GetExtensionList();
150 if (extension_list.size() > kMaxExtensionsToShow) {
151 int old_size = extension_list.size();
152 extension_list.erase(extension_list.begin() + kMaxExtensionsToShow,
153 extension_list.end());
Raul Tambrefff51b752019-02-04 13:09:47154 extension_list.push_back(delegate_->GetOverflowText(
155 base::NumberToString16(old_size - kMaxExtensionsToShow)));
rdevlin.cronina28846d2015-04-30 23:12:19156 }
157 const base::char16 bullet_point = 0x2022;
158 base::string16 prefix = bullet_point + base::ASCIIToUTF16(" ");
159 for (base::string16& str : extension_list)
160 str.insert(0, prefix);
brettwd94a22142015-07-15 05:19:26161 return base::JoinString(extension_list, base::ASCIIToUTF16("\n"));
rdevlin.cronina28846d2015-04-30 23:12:19162}
163
[email protected]c72ebfe2013-12-13 21:57:53164const ExtensionIdList& ExtensionMessageBubbleController::GetExtensionIdList() {
165 return *GetOrCreateExtensionList();
166}
167
catmullings6e557722016-12-17 02:55:26168void ExtensionMessageBubbleController::UpdateExtensionIdList() {
169 ExtensionIdList* extension_ids = GetOrCreateExtensionList();
170 ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
171 int include_mask = delegate_->ShouldLimitToEnabledExtensions()
172 ? ExtensionRegistry::ENABLED
173 : ExtensionRegistry::EVERYTHING;
174 for (auto iter = extension_ids->begin(); iter != extension_ids->end();) {
175 const Extension* extension =
176 registry->GetExtensionById(*iter, include_mask);
177 if (extension)
178 ++iter;
179 else
180 iter = extension_ids->erase(iter);
181 }
182}
183
rdevlin.cronin690e44f2015-11-06 00:27:14184bool ExtensionMessageBubbleController::CloseOnDeactivate() {
185 return delegate_->ShouldCloseOnDeactivate();
186}
[email protected]94b8a51a2014-03-26 20:57:55187
rdevlin.cronin70fc6052015-04-15 17:49:06188void ExtensionMessageBubbleController::HighlightExtensionsIfNecessary() {
rdevlin.croninaf64c8c2016-06-21 00:16:00189 DCHECK(is_active_bubble_);
190 if (delegate_->ShouldHighlightExtensions() && !is_highlighting_) {
191 is_highlighting_ = true;
rdevlin.cronin70fc6052015-04-15 17:49:06192 const ExtensionIdList& extension_ids = GetExtensionIdList();
193 DCHECK(!extension_ids.empty());
rdevlin.croninaf64c8c2016-06-21 00:16:00194 model_->HighlightActions(extension_ids,
195 ToolbarActionsModel::HIGHLIGHT_WARNING);
rdevlin.cronin70fc6052015-04-15 17:49:06196 }
197}
rdevlin.cronin86261efe2015-04-13 17:42:30198
Catherine Mullings8fe05b62017-08-16 17:14:45199void ExtensionMessageBubbleController::OnShown(
200 const base::Closure& close_bubble_callback) {
201 close_bubble_callback_ = close_bubble_callback;
rdevlin.croninaf64c8c2016-06-21 00:16:00202 DCHECK(is_active_bubble_);
Catherine Mullings478a0432017-11-22 07:06:06203 delegate_->OnShown(GetExtensionIdList());
204
205 if (!extension_registry_observer_.IsObserving(
206 ExtensionRegistry::Get(browser_->profile()))) {
207 extension_registry_observer_.Add(
208 ExtensionRegistry::Get(browser_->profile()));
209 }
[email protected]c72ebfe2013-12-13 21:57:53210}
211
212void ExtensionMessageBubbleController::OnBubbleAction() {
Catherine Mullings8fe05b62017-08-16 17:14:45213 // In addition to closing the bubble, OnBubbleAction() may result in a removal
214 // or disabling of the extension. To prevent triggering OnExtensionUnloaded(),
215 // which will also try to close the bubble, the controller's extension
216 // registry observer is removed. Note, we do not remove the extension registry
217 // observer in the cases of OnBubbleDismiss() and OnLinkedClicked() since they
218 // do not result in extensions being unloaded.
219 extension_registry_observer_.RemoveAll();
[email protected]c72ebfe2013-12-13 21:57:53220 DCHECK_EQ(ACTION_BOUNDARY, user_action_);
221 user_action_ = ACTION_EXECUTE;
222
223 delegate_->LogAction(ACTION_EXECUTE);
224 delegate_->PerformAction(*GetOrCreateExtensionList());
rdevlin.cronin9bd430a2015-05-08 18:06:18225
226 OnClose();
[email protected]c72ebfe2013-12-13 21:57:53227}
228
rdevlin.cronin486b20e2015-11-20 17:20:35229void ExtensionMessageBubbleController::OnBubbleDismiss(
230 bool closed_by_deactivation) {
[email protected]a74569b2014-03-25 02:56:30231 // OnBubbleDismiss() can be called twice when we receive multiple
232 // "OnWidgetDestroying" notifications (this can at least happen when we close
233 // a window with a notification open). Handle this gracefully.
234 if (user_action_ != ACTION_BOUNDARY) {
rdevlin.cronin486b20e2015-11-20 17:20:35235 DCHECK(user_action_ == ACTION_DISMISS_USER_ACTION ||
236 user_action_ == ACTION_DISMISS_DEACTIVATION);
[email protected]a74569b2014-03-25 02:56:30237 return;
238 }
239
rdevlin.cronin486b20e2015-11-20 17:20:35240 user_action_ = closed_by_deactivation ? ACTION_DISMISS_DEACTIVATION
241 : ACTION_DISMISS_USER_ACTION;
[email protected]c72ebfe2013-12-13 21:57:53242
rdevlin.cronin486b20e2015-11-20 17:20:35243 delegate_->LogAction(user_action_);
rdevlin.cronin9bd430a2015-05-08 18:06:18244
245 OnClose();
[email protected]c72ebfe2013-12-13 21:57:53246}
247
248void ExtensionMessageBubbleController::OnLinkClicked() {
249 DCHECK_EQ(ACTION_BOUNDARY, user_action_);
250 user_action_ = ACTION_LEARN_MORE;
251
252 delegate_->LogAction(ACTION_LEARN_MORE);
rdevlin.croninbb826f12016-06-22 21:30:05253 // Opening a new tab for the learn more link can cause the bubble to close, so
254 // perform our cleanup here before opening the new tab.
255 OnClose();
rdevlin.cronin0ba2a4c2015-08-06 18:40:19256 if (!g_should_ignore_learn_more_for_testing) {
Tim Judkins0c0936f52019-08-05 19:29:25257 GURL learn_more_url = delegate_->GetLearnMoreUrl();
258 DCHECK(learn_more_url.is_valid());
259 browser_->OpenURL(
260 content::OpenURLParams(learn_more_url, content::Referrer(),
261 WindowOpenDisposition::NEW_FOREGROUND_TAB,
262 ui::PAGE_TRANSITION_LINK, false));
[email protected]c72ebfe2013-12-13 21:57:53263 }
rdevlin.croninbb826f12016-06-22 21:30:05264 // Warning: |this| may be deleted here!
[email protected]c72ebfe2013-12-13 21:57:53265}
266
rdevlin.croninaf64c8c2016-06-21 00:16:00267void ExtensionMessageBubbleController::SetIsActiveBubble() {
268 DCHECK(!is_active_bubble_);
269 DCHECK(!model_->has_active_bubble());
270 is_active_bubble_ = true;
271 model_->set_has_active_bubble(true);
272}
273
rdevlin.cronin0ba2a4c2015-08-06 18:40:19274// static
275void ExtensionMessageBubbleController::set_should_ignore_learn_more_for_testing(
276 bool should_ignore) {
277 g_should_ignore_learn_more_for_testing = should_ignore;
278}
279
Catherine Mullingsa8892782017-09-06 23:45:50280void ExtensionMessageBubbleController::HandleExtensionUnloadOrUninstall() {
Catherine Mullings8fe05b62017-08-16 17:14:45281 UpdateExtensionIdList();
282 // If the callback is set, then that means that OnShown() was called, and the
Catherine Mullings478a0432017-11-22 07:06:06283 // bubble was displayed.
Catherine Mullings8fe05b62017-08-16 17:14:45284 if (close_bubble_callback_ && GetExtensionIdList().empty()) {
Daniel Chengd78c0252019-04-26 17:22:11285 std::move(close_bubble_callback_).Run();
Catherine Mullings8fe05b62017-08-16 17:14:45286 }
287 // If the bubble refers to multiple extensions, we do not close the bubble.
288}
289
Catherine Mullingsa8892782017-09-06 23:45:50290void ExtensionMessageBubbleController::OnExtensionUnloaded(
291 content::BrowserContext* browser_context,
292 const Extension* extension,
293 UnloadedExtensionReason reason) {
294 HandleExtensionUnloadOrUninstall();
295}
296
297void ExtensionMessageBubbleController::OnExtensionUninstalled(
298 content::BrowserContext* browser_context,
299 const Extension* extension,
300 UninstallReason reason) {
301 HandleExtensionUnloadOrUninstall();
302}
303
Catherine Mullings8fe05b62017-08-16 17:14:45304void ExtensionMessageBubbleController::OnShutdown(ExtensionRegistry* registry) {
305 // It is possible that the extension registry is destroyed before the
306 // controller. In such case, the controller should no longer observe the
307 // registry.
308 extension_registry_observer_.Remove(registry);
309}
310
rdevlin.cronin89370302016-04-19 14:24:40311void ExtensionMessageBubbleController::OnBrowserRemoved(Browser* browser) {
Catherine Mullingsa2e975ff2017-08-29 00:45:06312 extension_registry_observer_.RemoveAll();
rdevlin.croninaf64c8c2016-06-21 00:16:00313 if (browser == browser_) {
314 if (is_highlighting_) {
315 model_->StopHighlighting();
316 is_highlighting_ = false;
317 }
318 if (is_active_bubble_) {
319 model_->set_has_active_bubble(false);
320 is_active_bubble_ = false;
321 }
rdevlin.cronin89370302016-04-19 14:24:40322 }
323}
324
[email protected]c72ebfe2013-12-13 21:57:53325void ExtensionMessageBubbleController::AcknowledgeExtensions() {
326 ExtensionIdList* list = GetOrCreateExtensionList();
327 for (ExtensionIdList::const_iterator it = list->begin();
328 it != list->end(); ++it)
329 delegate_->AcknowledgeExtension(*it, user_action_);
330}
331
332ExtensionIdList* ExtensionMessageBubbleController::GetOrCreateExtensionList() {
[email protected]c72ebfe2013-12-13 21:57:53333 if (!initialized_) {
rdevlin.cronincce78d02015-09-24 19:50:55334 ExtensionRegistry* registry = ExtensionRegistry::Get(profile());
dchengc963c7142016-04-08 03:55:22335 std::unique_ptr<const ExtensionSet> all_extensions;
rdevlin.cronincce78d02015-09-24 19:50:55336 if (!delegate_->ShouldLimitToEnabledExtensions())
337 all_extensions = registry->GenerateInstalledExtensionsSet();
338 const ExtensionSet& extensions_to_check =
339 all_extensions ? *all_extensions : registry->enabled_extensions();
340 for (const scoped_refptr<const Extension>& extension :
341 extensions_to_check) {
342 if (delegate_->ShouldIncludeExtension(extension.get()))
343 extension_list_.push_back(extension->id());
[email protected]c72ebfe2013-12-13 21:57:53344 }
345
346 delegate_->LogExtensionCount(extension_list_.size());
347 initialized_ = true;
348 }
349
350 return &extension_list_;
351}
352
rdevlin.cronin9bd430a2015-05-08 18:06:18353void ExtensionMessageBubbleController::OnClose() {
rdevlin.cronin486b20e2015-11-20 17:20:35354 DCHECK_NE(ACTION_BOUNDARY, user_action_);
355 // If the bubble was closed due to deactivation, don't treat it as
356 // acknowledgment so that the user will see the bubble again (until they
357 // explicitly take an action).
rdevlin.cronincf27f866a2017-02-01 16:34:39358 if (user_action_ != ACTION_DISMISS_DEACTIVATION ||
359 delegate_->ShouldAcknowledgeOnDeactivate()) {
rdevlin.cronin486b20e2015-11-20 17:20:35360 AcknowledgeExtensions();
Catherine Mullings478a0432017-11-22 07:06:06361 delegate_->OnAction();
rdevlin.cronin486b20e2015-11-20 17:20:35362 }
rdevlin.cronin9bd430a2015-05-08 18:06:18363
Catherine Mullings478a0432017-11-22 07:06:06364 extension_registry_observer_.RemoveAll();
rdevlin.cronin486b20e2015-11-20 17:20:35365}
366
[email protected]c72ebfe2013-12-13 21:57:53367} // namespace extensions