Home | History | Annotate | Download | only in extensions
      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 
     38 bool ExtensionToolbarModel::Observer::BrowserActionShowPopup(
     39     const Extension* extension) {
     40   return false;
     41 }
     42 
     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 
     64 ExtensionToolbarModel::~ExtensionToolbarModel() {
     65 }
     66 
     67 // static
     68 ExtensionToolbarModel* ExtensionToolbarModel::Get(Profile* profile) {
     69   return ExtensionToolbarModelFactory::GetForProfile(profile);
     70 }
     71 
     72 void ExtensionToolbarModel::AddObserver(Observer* observer) {
     73   observers_.AddObserver(observer);
     74 }
     75 
     76 void ExtensionToolbarModel::RemoveObserver(Observer* observer) {
     77   observers_.RemoveObserver(observer);
     78 }
     79 
     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 
    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 
    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 
    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 
    186 void ExtensionToolbarModel::OnExtensionUnloaded(
    187     content::BrowserContext* browser_context,
    188     const Extension* extension,
    189     UnloadedExtensionInfo::Reason reason) {
    190   RemoveExtension(extension);
    191 }
    192 
    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 
    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 
    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 
    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 
    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 
    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.
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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