1 // Copyright (c) 2012 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_toolbar_model.h"
6
7 #include <string>
8
9 #include "base/metrics/histogram.h"
10 #include "base/metrics/histogram_base.h"
11 #include "base/prefs/pref_service.h"
12 #include "chrome/browser/chrome_notification_types.h"
13 #include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
14 #include "chrome/browser/extensions/extension_action.h"
15 #include "chrome/browser/extensions/extension_action_manager.h"
16 #include "chrome/browser/extensions/extension_tab_util.h"
17 #include "chrome/browser/extensions/extension_toolbar_model_factory.h"
18 #include "chrome/browser/extensions/extension_util.h"
19 #include "chrome/browser/extensions/tab_helper.h"
20 #include "chrome/browser/profiles/profile.h"
21 #include "chrome/browser/ui/browser.h"
22 #include "chrome/browser/ui/tabs/tab_strip_model.h"
23 #include "chrome/common/pref_names.h"
24 #include "content/public/browser/notification_details.h"
25 #include "content/public/browser/notification_source.h"
26 #include "content/public/browser/web_contents.h"
27 #include "extensions/browser/extension_prefs.h"
28 #include "extensions/browser/extension_registry.h"
29 #include "extensions/browser/extension_system.h"
30 #include "extensions/browser/pref_names.h"
31 #include "extensions/common/extension.h"
32 #include "extensions/common/extension_set.h"
33 #include "extensions/common/feature_switch.h"
34 #include "extensions/common/one_shot_event.h"
35
36 namespace extensions {
37
BrowserActionShowPopup(const Extension * extension)38 bool ExtensionToolbarModel::Observer::BrowserActionShowPopup(
39 const Extension* extension) {
40 return false;
41 }
42
ExtensionToolbarModel(Profile * profile,ExtensionPrefs * extension_prefs)43 ExtensionToolbarModel::ExtensionToolbarModel(Profile* profile,
44 ExtensionPrefs* extension_prefs)
45 : profile_(profile),
46 extension_prefs_(extension_prefs),
47 prefs_(profile_->GetPrefs()),
48 extensions_initialized_(false),
49 is_highlighting_(false),
50 extension_registry_observer_(this),
51 weak_ptr_factory_(this) {
52 ExtensionSystem::Get(profile_)->ready().Post(
53 FROM_HERE,
54 base::Bind(&ExtensionToolbarModel::OnReady,
55 weak_ptr_factory_.GetWeakPtr()));
56 visible_icon_count_ = prefs_->GetInteger(pref_names::kToolbarSize);
57 pref_change_registrar_.Init(prefs_);
58 pref_change_callback_ =
59 base::Bind(&ExtensionToolbarModel::OnExtensionToolbarPrefChange,
60 base::Unretained(this));
61 pref_change_registrar_.Add(pref_names::kToolbar, pref_change_callback_);
62 }
63
~ExtensionToolbarModel()64 ExtensionToolbarModel::~ExtensionToolbarModel() {
65 }
66
67 // static
Get(Profile * profile)68 ExtensionToolbarModel* ExtensionToolbarModel::Get(Profile* profile) {
69 return ExtensionToolbarModelFactory::GetForProfile(profile);
70 }
71
AddObserver(Observer * observer)72 void ExtensionToolbarModel::AddObserver(Observer* observer) {
73 observers_.AddObserver(observer);
74 }
75
RemoveObserver(Observer * observer)76 void ExtensionToolbarModel::RemoveObserver(Observer* observer) {
77 observers_.RemoveObserver(observer);
78 }
79
MoveBrowserAction(const Extension * extension,int index)80 void ExtensionToolbarModel::MoveBrowserAction(const Extension* extension,
81 int index) {
82 ExtensionList::iterator pos = std::find(toolbar_items_.begin(),
83 toolbar_items_.end(), extension);
84 if (pos == toolbar_items_.end()) {
85 NOTREACHED();
86 return;
87 }
88 toolbar_items_.erase(pos);
89
90 ExtensionIdList::iterator pos_id;
91 pos_id = std::find(last_known_positions_.begin(),
92 last_known_positions_.end(), extension->id());
93 if (pos_id != last_known_positions_.end())
94 last_known_positions_.erase(pos_id);
95
96 int i = 0;
97 bool inserted = false;
98 for (ExtensionList::iterator iter = toolbar_items_.begin();
99 iter != toolbar_items_.end();
100 ++iter, ++i) {
101 if (i == index) {
102 pos_id = std::find(last_known_positions_.begin(),
103 last_known_positions_.end(), (*iter)->id());
104 last_known_positions_.insert(pos_id, extension->id());
105
106 toolbar_items_.insert(iter, make_scoped_refptr(extension));
107 inserted = true;
108 break;
109 }
110 }
111
112 if (!inserted) {
113 DCHECK_EQ(index, static_cast<int>(toolbar_items_.size()));
114 index = toolbar_items_.size();
115
116 toolbar_items_.push_back(make_scoped_refptr(extension));
117 last_known_positions_.push_back(extension->id());
118 }
119
120 FOR_EACH_OBSERVER(Observer, observers_, BrowserActionMoved(extension, index));
121
122 UpdatePrefs();
123 }
124
ExecuteBrowserAction(const Extension * extension,Browser * browser,GURL * popup_url_out,bool should_grant)125 ExtensionToolbarModel::Action ExtensionToolbarModel::ExecuteBrowserAction(
126 const Extension* extension,
127 Browser* browser,
128 GURL* popup_url_out,
129 bool should_grant) {
130 content::WebContents* web_contents = NULL;
131 int tab_id = 0;
132 if (!ExtensionTabUtil::GetDefaultTab(browser, &web_contents, &tab_id)) {
133 return ACTION_NONE;
134 }
135
136 ExtensionAction* browser_action =
137 ExtensionActionManager::Get(profile_)->GetBrowserAction(*extension);
138
139 // For browser actions, visibility == enabledness.
140 if (!browser_action->GetIsVisible(tab_id))
141 return ACTION_NONE;
142
143 if (should_grant) {
144 TabHelper::FromWebContents(web_contents)
145 ->active_tab_permission_granter()
146 ->GrantIfRequested(extension);
147 }
148
149 if (browser_action->HasPopup(tab_id)) {
150 if (popup_url_out)
151 *popup_url_out = browser_action->GetPopupUrl(tab_id);
152 return ACTION_SHOW_POPUP;
153 }
154
155 ExtensionActionAPI::BrowserActionExecuted(
156 browser->profile(), *browser_action, web_contents);
157 return ACTION_NONE;
158 }
159
SetVisibleIconCount(int count)160 void ExtensionToolbarModel::SetVisibleIconCount(int count) {
161 visible_icon_count_ =
162 count == static_cast<int>(toolbar_items_.size()) ? -1 : count;
163 // Only set the prefs if we're not in highlight mode. Highlight mode is
164 // designed to be a transitory state, and should not persist across browser
165 // restarts (though it may be re-entered).
166 if (!is_highlighting_)
167 prefs_->SetInteger(pref_names::kToolbarSize, visible_icon_count_);
168 }
169
OnExtensionLoaded(content::BrowserContext * browser_context,const Extension * extension)170 void ExtensionToolbarModel::OnExtensionLoaded(
171 content::BrowserContext* browser_context,
172 const Extension* extension) {
173 // We don't want to add the same extension twice. It may have already been
174 // added by EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED below, if the user
175 // hides the browser action and then disables and enables the extension.
176 for (size_t i = 0; i < toolbar_items_.size(); i++) {
177 if (toolbar_items_[i].get() == extension)
178 return;
179 }
180 if (ExtensionActionAPI::GetBrowserActionVisibility(extension_prefs_,
181 extension->id())) {
182 AddExtension(extension);
183 }
184 }
185
OnExtensionUnloaded(content::BrowserContext * browser_context,const Extension * extension,UnloadedExtensionInfo::Reason reason)186 void ExtensionToolbarModel::OnExtensionUnloaded(
187 content::BrowserContext* browser_context,
188 const Extension* extension,
189 UnloadedExtensionInfo::Reason reason) {
190 RemoveExtension(extension);
191 }
192
OnExtensionUninstalled(content::BrowserContext * browser_context,const Extension * extension)193 void ExtensionToolbarModel::OnExtensionUninstalled(
194 content::BrowserContext* browser_context,
195 const Extension* extension) {
196 // Remove the extension id from the ordered list, if it exists (the extension
197 // might not be represented in the list because it might not have an icon).
198 ExtensionIdList::iterator pos =
199 std::find(last_known_positions_.begin(),
200 last_known_positions_.end(), extension->id());
201
202 if (pos != last_known_positions_.end()) {
203 last_known_positions_.erase(pos);
204 UpdatePrefs();
205 }
206 }
207
Observe(int type,const content::NotificationSource & source,const content::NotificationDetails & details)208 void ExtensionToolbarModel::Observe(
209 int type,
210 const content::NotificationSource& source,
211 const content::NotificationDetails& details) {
212 DCHECK_EQ(chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED,
213 type);
214 const Extension* extension =
215 ExtensionRegistry::Get(profile_)->GetExtensionById(
216 *content::Details<const std::string>(details).ptr(),
217 ExtensionRegistry::EVERYTHING);
218 if (ExtensionActionAPI::GetBrowserActionVisibility(extension_prefs_,
219 extension->id()))
220 AddExtension(extension);
221 else
222 RemoveExtension(extension);
223 }
224
OnReady()225 void ExtensionToolbarModel::OnReady() {
226 ExtensionRegistry* registry = ExtensionRegistry::Get(profile_);
227 InitializeExtensionList(registry->enabled_extensions());
228 // Wait until the extension system is ready before observing any further
229 // changes so that the toolbar buttons can be shown in their stable ordering
230 // taken from prefs.
231 extension_registry_observer_.Add(registry);
232 registrar_.Add(
233 this,
234 chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED,
235 content::Source<ExtensionPrefs>(extension_prefs_));
236 }
237
FindNewPositionFromLastKnownGood(const Extension * extension)238 size_t ExtensionToolbarModel::FindNewPositionFromLastKnownGood(
239 const Extension* extension) {
240 // See if we have last known good position for this extension.
241 size_t new_index = 0;
242 // Loop through the ID list of known positions, to count the number of visible
243 // browser action icons preceding |extension|.
244 for (ExtensionIdList::const_iterator iter_id = last_known_positions_.begin();
245 iter_id < last_known_positions_.end(); ++iter_id) {
246 if ((*iter_id) == extension->id())
247 return new_index; // We've found the right position.
248 // Found an id, need to see if it is visible.
249 for (ExtensionList::const_iterator iter_ext = toolbar_items_.begin();
250 iter_ext < toolbar_items_.end(); ++iter_ext) {
251 if ((*iter_ext)->id().compare(*iter_id) == 0) {
252 // This extension is visible, update the index value.
253 ++new_index;
254 break;
255 }
256 }
257 }
258
259 return -1;
260 }
261
AddExtension(const Extension * extension)262 void ExtensionToolbarModel::AddExtension(const Extension* extension) {
263 // We only care about extensions with browser actions.
264 if (!ExtensionActionManager::Get(profile_)->GetBrowserAction(*extension))
265 return;
266
267 size_t new_index = -1;
268
269 // See if we have a last known good position for this extension.
270 ExtensionIdList::iterator last_pos = std::find(last_known_positions_.begin(),
271 last_known_positions_.end(),
272 extension->id());
273 if (last_pos != last_known_positions_.end()) {
274 new_index = FindNewPositionFromLastKnownGood(extension);
275 if (new_index != toolbar_items_.size()) {
276 toolbar_items_.insert(toolbar_items_.begin() + new_index,
277 make_scoped_refptr(extension));
278 } else {
279 toolbar_items_.push_back(make_scoped_refptr(extension));
280 }
281 } else {
282 // This is a never before seen extension, that was added to the end. Make
283 // sure to reflect that.
284 toolbar_items_.push_back(make_scoped_refptr(extension));
285 last_known_positions_.push_back(extension->id());
286 new_index = toolbar_items_.size() - 1;
287 UpdatePrefs();
288 }
289
290 // If we're currently highlighting, then even though we add a browser action
291 // to the full list (|toolbar_items_|, there won't be another *visible*
292 // browser action, which was what the observers care about.
293 if (!is_highlighting_) {
294 FOR_EACH_OBSERVER(Observer, observers_,
295 BrowserActionAdded(extension, new_index));
296 }
297 }
298
RemoveExtension(const Extension * extension)299 void ExtensionToolbarModel::RemoveExtension(const Extension* extension) {
300 ExtensionList::iterator pos =
301 std::find(toolbar_items_.begin(), toolbar_items_.end(), extension);
302 if (pos == toolbar_items_.end())
303 return;
304
305 toolbar_items_.erase(pos);
306
307 // If we're in highlight mode, we also have to remove the extension from
308 // the highlighted list.
309 if (is_highlighting_) {
310 pos = std::find(highlighted_items_.begin(),
311 highlighted_items_.end(),
312 extension);
313 if (pos != highlighted_items_.end()) {
314 highlighted_items_.erase(pos);
315 FOR_EACH_OBSERVER(Observer, observers_, BrowserActionRemoved(extension));
316 // If the highlighted list is now empty, we stop highlighting.
317 if (highlighted_items_.empty())
318 StopHighlighting();
319 }
320 } else {
321 FOR_EACH_OBSERVER(Observer, observers_, BrowserActionRemoved(extension));
322 }
323
324 UpdatePrefs();
325 }
326
327 // Combine the currently enabled extensions that have browser actions (which
328 // we get from the ExtensionRegistry) with the ordering we get from the
329 // pref service. For robustness we use a somewhat inefficient process:
330 // 1. Create a vector of extensions sorted by their pref values. This vector may
331 // have holes.
332 // 2. Create a vector of extensions that did not have a pref value.
333 // 3. Remove holes from the sorted vector and append the unsorted vector.
InitializeExtensionList(const ExtensionSet & extensions)334 void ExtensionToolbarModel::InitializeExtensionList(
335 const ExtensionSet& extensions) {
336 last_known_positions_ = extension_prefs_->GetToolbarOrder();
337 Populate(last_known_positions_, extensions);
338
339 extensions_initialized_ = true;
340 FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
341 }
342
Populate(const ExtensionIdList & positions,const ExtensionSet & extensions)343 void ExtensionToolbarModel::Populate(const ExtensionIdList& positions,
344 const ExtensionSet& extensions) {
345 // Items that have explicit positions.
346 ExtensionList sorted;
347 sorted.resize(positions.size(), NULL);
348 // The items that don't have explicit positions.
349 ExtensionList unsorted;
350
351 ExtensionActionManager* extension_action_manager =
352 ExtensionActionManager::Get(profile_);
353
354 // Create the lists.
355 int hidden = 0;
356 for (ExtensionSet::const_iterator it = extensions.begin();
357 it != extensions.end();
358 ++it) {
359 const Extension* extension = it->get();
360 if (!extension_action_manager->GetBrowserAction(*extension))
361 continue;
362 if (!ExtensionActionAPI::GetBrowserActionVisibility(
363 extension_prefs_, extension->id())) {
364 ++hidden;
365 continue;
366 }
367
368 ExtensionIdList::const_iterator pos =
369 std::find(positions.begin(), positions.end(), extension->id());
370 if (pos != positions.end())
371 sorted[pos - positions.begin()] = extension;
372 else
373 unsorted.push_back(make_scoped_refptr(extension));
374 }
375
376 size_t items_count = toolbar_items_.size();
377 for (size_t i = 0; i < items_count; i++) {
378 const Extension* extension = toolbar_items_.back();
379 // By popping the extension here (before calling BrowserActionRemoved),
380 // we will not shrink visible count by one after BrowserActionRemoved
381 // calls SetVisibleCount.
382 toolbar_items_.pop_back();
383 FOR_EACH_OBSERVER(
384 Observer, observers_, BrowserActionRemoved(extension));
385 }
386 DCHECK(toolbar_items_.empty());
387
388 // Merge the lists.
389 toolbar_items_.reserve(sorted.size() + unsorted.size());
390
391 for (ExtensionList::const_iterator iter = sorted.begin();
392 iter != sorted.end(); ++iter) {
393 // It's possible for the extension order to contain items that aren't
394 // actually loaded on this machine. For example, when extension sync is on,
395 // we sync the extension order as-is but double-check with the user before
396 // syncing NPAPI-containing extensions, so if one of those is not actually
397 // synced, we'll get a NULL in the list. This sort of case can also happen
398 // if some error prevents an extension from loading.
399 if (iter->get() != NULL) {
400 toolbar_items_.push_back(*iter);
401 FOR_EACH_OBSERVER(
402 Observer, observers_, BrowserActionAdded(
403 *iter, toolbar_items_.size() - 1));
404 }
405 }
406 for (ExtensionList::const_iterator iter = unsorted.begin();
407 iter != unsorted.end(); ++iter) {
408 if (iter->get() != NULL) {
409 toolbar_items_.push_back(*iter);
410 FOR_EACH_OBSERVER(
411 Observer, observers_, BrowserActionAdded(
412 *iter, toolbar_items_.size() - 1));
413 }
414 }
415
416 UMA_HISTOGRAM_COUNTS_100(
417 "ExtensionToolbarModel.BrowserActionsPermanentlyHidden", hidden);
418 UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsCount",
419 toolbar_items_.size());
420
421 if (!toolbar_items_.empty()) {
422 // Visible count can be -1, meaning: 'show all'. Since UMA converts negative
423 // values to 0, this would be counted as 'show none' unless we convert it to
424 // max.
425 UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsVisible",
426 visible_icon_count_ == -1 ?
427 base::HistogramBase::kSampleType_MAX :
428 visible_icon_count_);
429 }
430 }
431
UpdatePrefs()432 void ExtensionToolbarModel::UpdatePrefs() {
433 if (!extension_prefs_)
434 return;
435
436 // Don't observe change caused by self.
437 pref_change_registrar_.Remove(pref_names::kToolbar);
438 extension_prefs_->SetToolbarOrder(last_known_positions_);
439 pref_change_registrar_.Add(pref_names::kToolbar, pref_change_callback_);
440 }
441
IncognitoIndexToOriginal(int incognito_index)442 int ExtensionToolbarModel::IncognitoIndexToOriginal(int incognito_index) {
443 int original_index = 0, i = 0;
444 for (ExtensionList::iterator iter = toolbar_items_.begin();
445 iter != toolbar_items_.end();
446 ++iter, ++original_index) {
447 if (util::IsIncognitoEnabled((*iter)->id(), profile_)) {
448 if (incognito_index == i)
449 break;
450 ++i;
451 }
452 }
453 return original_index;
454 }
455
OriginalIndexToIncognito(int original_index)456 int ExtensionToolbarModel::OriginalIndexToIncognito(int original_index) {
457 int incognito_index = 0, i = 0;
458 for (ExtensionList::iterator iter = toolbar_items_.begin();
459 iter != toolbar_items_.end();
460 ++iter, ++i) {
461 if (original_index == i)
462 break;
463 if (util::IsIncognitoEnabled((*iter)->id(), profile_))
464 ++incognito_index;
465 }
466 return incognito_index;
467 }
468
OnExtensionToolbarPrefChange()469 void ExtensionToolbarModel::OnExtensionToolbarPrefChange() {
470 // If extensions are not ready, defer to later Populate() call.
471 if (!extensions_initialized_)
472 return;
473
474 // Recalculate |last_known_positions_| to be |pref_positions| followed by
475 // ones that are only in |last_known_positions_|.
476 ExtensionIdList pref_positions = extension_prefs_->GetToolbarOrder();
477 size_t pref_position_size = pref_positions.size();
478 for (size_t i = 0; i < last_known_positions_.size(); ++i) {
479 if (std::find(pref_positions.begin(), pref_positions.end(),
480 last_known_positions_[i]) == pref_positions.end()) {
481 pref_positions.push_back(last_known_positions_[i]);
482 }
483 }
484 last_known_positions_.swap(pref_positions);
485
486 // Re-populate.
487 Populate(last_known_positions_,
488 ExtensionRegistry::Get(profile_)->enabled_extensions());
489
490 if (last_known_positions_.size() > pref_position_size) {
491 // Need to update pref because we have extra icons. But can't call
492 // UpdatePrefs() directly within observation closure.
493 base::MessageLoop::current()->PostTask(
494 FROM_HERE,
495 base::Bind(&ExtensionToolbarModel::UpdatePrefs,
496 weak_ptr_factory_.GetWeakPtr()));
497 }
498 }
499
ShowBrowserActionPopup(const Extension * extension)500 bool ExtensionToolbarModel::ShowBrowserActionPopup(const Extension* extension) {
501 ObserverListBase<Observer>::Iterator it(observers_);
502 Observer* obs = NULL;
503 while ((obs = it.GetNext()) != NULL) {
504 // Stop after first popup since it should only show in the active window.
505 if (obs->BrowserActionShowPopup(extension))
506 return true;
507 }
508 return false;
509 }
510
EnsureVisibility(const ExtensionIdList & extension_ids)511 void ExtensionToolbarModel::EnsureVisibility(
512 const ExtensionIdList& extension_ids) {
513 if (visible_icon_count_ == -1)
514 return; // Already showing all.
515
516 // Otherwise, make sure we have enough room to show all the extensions
517 // requested.
518 if (visible_icon_count_ < static_cast<int>(extension_ids.size())) {
519 SetVisibleIconCount(extension_ids.size());
520
521 // Inform observers.
522 FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
523 }
524
525 if (visible_icon_count_ == -1)
526 return; // May have been set to max by SetVisibleIconCount.
527
528 // Guillotine's Delight: Move an orange noble to the front of the line.
529 for (ExtensionIdList::const_iterator it = extension_ids.begin();
530 it != extension_ids.end(); ++it) {
531 for (ExtensionList::const_iterator extension = toolbar_items_.begin();
532 extension != toolbar_items_.end(); ++extension) {
533 if ((*extension)->id() == (*it)) {
534 if (extension - toolbar_items_.begin() >= visible_icon_count_)
535 MoveBrowserAction(*extension, 0);
536 break;
537 }
538 }
539 }
540 }
541
HighlightExtensions(const ExtensionIdList & extension_ids)542 bool ExtensionToolbarModel::HighlightExtensions(
543 const ExtensionIdList& extension_ids) {
544 highlighted_items_.clear();
545
546 for (ExtensionIdList::const_iterator id = extension_ids.begin();
547 id != extension_ids.end();
548 ++id) {
549 for (ExtensionList::const_iterator extension = toolbar_items_.begin();
550 extension != toolbar_items_.end();
551 ++extension) {
552 if (*id == (*extension)->id())
553 highlighted_items_.push_back(*extension);
554 }
555 }
556
557 // If we have any items in |highlighted_items_|, then we entered highlighting
558 // mode.
559 if (highlighted_items_.size()) {
560 old_visible_icon_count_ = visible_icon_count_;
561 is_highlighting_ = true;
562 if (visible_icon_count_ != -1 &&
563 visible_icon_count_ < static_cast<int>(extension_ids.size())) {
564 SetVisibleIconCount(extension_ids.size());
565 FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
566 }
567
568 FOR_EACH_OBSERVER(Observer, observers_, HighlightModeChanged(true));
569 return true;
570 }
571
572 // Otherwise, we didn't enter highlighting mode (and, in fact, exited it if
573 // we were otherwise in it).
574 if (is_highlighting_)
575 StopHighlighting();
576 return false;
577 }
578
StopHighlighting()579 void ExtensionToolbarModel::StopHighlighting() {
580 if (is_highlighting_) {
581 highlighted_items_.clear();
582 is_highlighting_ = false;
583 if (old_visible_icon_count_ != visible_icon_count_) {
584 SetVisibleIconCount(old_visible_icon_count_);
585 FOR_EACH_OBSERVER(Observer, observers_, VisibleCountChanged());
586 }
587 FOR_EACH_OBSERVER(Observer, observers_, HighlightModeChanged(false));
588 }
589 }
590
591 } // namespace extensions
592