extension_toolbar_model.cc revision 116680a4aac90f2aa7413d9095a592090648e557
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
36namespace extensions {
37
38bool ExtensionToolbarModel::Observer::BrowserActionShowPopup(
39    const Extension* extension) {
40  return false;
41}
42
43ExtensionToolbarModel::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
64ExtensionToolbarModel::~ExtensionToolbarModel() {
65}
66
67// static
68ExtensionToolbarModel* ExtensionToolbarModel::Get(Profile* profile) {
69  return ExtensionToolbarModelFactory::GetForProfile(profile);
70}
71
72void ExtensionToolbarModel::AddObserver(Observer* observer) {
73  observers_.AddObserver(observer);
74}
75
76void ExtensionToolbarModel::RemoveObserver(Observer* observer) {
77  observers_.RemoveObserver(observer);
78}
79
80void 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
125ExtensionToolbarModel::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
160void 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
170void 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
186void ExtensionToolbarModel::OnExtensionUnloaded(
187    content::BrowserContext* browser_context,
188    const Extension* extension,
189    UnloadedExtensionInfo::Reason reason) {
190  RemoveExtension(extension);
191}
192
193void 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
208void 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
225void 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
238size_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  // Position not found.
260  return toolbar_items_.size();
261}
262
263void ExtensionToolbarModel::AddExtension(const Extension* extension) {
264  // We only care about extensions with browser actions.
265  if (!ExtensionActionManager::Get(profile_)->GetBrowserAction(*extension))
266    return;
267
268  size_t new_index = toolbar_items_.size();
269
270  // See if we have a last known good position for this extension.
271  ExtensionIdList::iterator last_pos = std::find(last_known_positions_.begin(),
272                                                 last_known_positions_.end(),
273                                                 extension->id());
274  if (last_pos != last_known_positions_.end()) {
275    new_index = FindNewPositionFromLastKnownGood(extension);
276    if (new_index != toolbar_items_.size()) {
277      toolbar_items_.insert(toolbar_items_.begin() + new_index,
278                            make_scoped_refptr(extension));
279    } else {
280      toolbar_items_.push_back(make_scoped_refptr(extension));
281    }
282  } else {
283    // This is a never before seen extension, that was added to the end. Make
284    // sure to reflect that. (|new_index| was set above.)
285    toolbar_items_.push_back(make_scoped_refptr(extension));
286    last_known_positions_.push_back(extension->id());
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
299void 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.
334void 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
343void 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
432void 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
442int 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
456int 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
469void 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
500bool 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
511void 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
542bool 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
579void 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