// Copyright (c) 2012 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_toolbar_model.h" #include <algorithm> #include <string> #include "base/location.h" #include "base/metrics/histogram.h" #include "base/metrics/histogram_base.h" #include "base/prefs/pref_service.h" #include "base/single_thread_task_runner.h" #include "base/thread_task_runner_handle.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/extensions/api/extension_action/extension_action_api.h" #include "chrome/browser/extensions/extension_action_manager.h" #include "chrome/browser/extensions/extension_tab_util.h" #include "chrome/browser/extensions/extension_toolbar_model_factory.h" #include "chrome/browser/extensions/extension_util.h" #include "chrome/browser/extensions/tab_helper.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/tabs/tab_strip_model.h" #include "content/public/browser/notification_details.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/web_contents.h" #include "extensions/browser/extension_prefs.h" #include "extensions/browser/extension_registry.h" #include "extensions/browser/extension_system.h" #include "extensions/browser/pref_names.h" #include "extensions/common/extension.h" #include "extensions/common/extension_set.h" #include "extensions/common/feature_switch.h" #include "extensions/common/manifest_constants.h" #include "extensions/common/one_shot_event.h" namespace extensions { ExtensionToolbarModel::ExtensionToolbarModel(Profile* profile, ExtensionPrefs* extension_prefs) : profile_(profile), extension_prefs_(extension_prefs), prefs_(profile_->GetPrefs()), extension_action_api_(ExtensionActionAPI::Get(profile_)), extensions_initialized_(false), include_all_extensions_( FeatureSwitch::extension_action_redesign()->IsEnabled()), is_highlighting_(false), extension_action_observer_(this), extension_registry_observer_(this), weak_ptr_factory_(this) { ExtensionSystem::Get(profile_)->ready().Post( FROM_HERE, base::Bind(&ExtensionToolbarModel::OnReady, weak_ptr_factory_.GetWeakPtr())); visible_icon_count_ = prefs_->GetInteger(pref_names::kToolbarSize); // We only care about watching the prefs if not in incognito mode. if (!profile_->IsOffTheRecord()) { pref_change_registrar_.Init(prefs_); pref_change_callback_ = base::Bind(&ExtensionToolbarModel::OnExtensionToolbarPrefChange, base::Unretained(this)); pref_change_registrar_.Add(pref_names::kToolbar, pref_change_callback_); } } ExtensionToolbarModel::~ExtensionToolbarModel() { } // static ExtensionToolbarModel* ExtensionToolbarModel::Get(Profile* profile) { return ExtensionToolbarModelFactory::GetForProfile(profile); } void ExtensionToolbarModel::AddObserver(Observer* observer) { observers_.AddObserver(observer); } void ExtensionToolbarModel::RemoveObserver(Observer* observer) { observers_.RemoveObserver(observer); } void ExtensionToolbarModel::MoveExtensionIcon(const std::string& id, size_t index) { ExtensionList::iterator pos = toolbar_items_.begin(); while (pos != toolbar_items_.end() && (*pos)->id() != id) ++pos; if (pos == toolbar_items_.end()) { NOTREACHED(); return; } scoped_refptr<const Extension> extension = *pos; toolbar_items_.erase(pos); ExtensionIdList::iterator pos_id = std::find(last_known_positions_.begin(), last_known_positions_.end(), id); if (pos_id != last_known_positions_.end()) last_known_positions_.erase(pos_id); if (index < toolbar_items_.size()) { // If the index is not at the end, find the item currently at |index|, and // insert |extension| before it in both |toolbar_items_| and // |last_known_positions_|. ExtensionList::iterator iter = toolbar_items_.begin() + index; last_known_positions_.insert(std::find(last_known_positions_.begin(), last_known_positions_.end(), (*iter)->id()), id); toolbar_items_.insert(iter, extension); } else { // Otherwise, put |extension| at the end. DCHECK_EQ(toolbar_items_.size(), index); index = toolbar_items_.size(); toolbar_items_.push_back(extension); last_known_positions_.push_back(id); } FOR_EACH_OBSERVER(Observer, observers_, OnToolbarExtensionMoved(extension.get(), index)); MaybeUpdateVisibilityPref(extension.get(), index); UpdatePrefs(); } void ExtensionToolbarModel::SetVisibleIconCount(size_t count) { visible_icon_count_ = (count >= toolbar_items_.size()) ? -1 : count; // Only set the prefs if we're not in highlight mode and the profile is not // incognito. Highlight mode is designed to be a transitory state, and should // not persist across browser restarts (though it may be re-entered), and we // don't store anything in incognito. if (!is_highlighting_ && !profile_->IsOffTheRecord()) { // Additionally, if we are using the new toolbar, any icons which are in the // overflow menu are considered "hidden". But it so happens that the times // we are likely to call SetVisibleIconCount() are also those when we are // in flux. So wait for things to cool down before setting the prefs. base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::Bind(&ExtensionToolbarModel::MaybeUpdateVisibilityPrefs, weak_ptr_factory_.GetWeakPtr())); prefs_->SetInteger(pref_names::kToolbarSize, visible_icon_count_); } FOR_EACH_OBSERVER(Observer, observers_, OnToolbarVisibleCountChanged()); } void ExtensionToolbarModel::OnExtensionActionUpdated( ExtensionAction* extension_action, content::WebContents* web_contents, content::BrowserContext* browser_context) { const Extension* extension = ExtensionRegistry::Get(profile_)->enabled_extensions().GetByID( extension_action->extension_id()); // Notify observers if the extension exists and is in the model. if (std::find(toolbar_items_.begin(), toolbar_items_.end(), extension) != toolbar_items_.end()) { FOR_EACH_OBSERVER(Observer, observers_, OnToolbarExtensionUpdated(extension)); } } void ExtensionToolbarModel::OnExtensionActionVisibilityChanged( const std::string& extension_id, bool is_now_visible) { const Extension* extension = ExtensionRegistry::Get(profile_)->GetExtensionById( extension_id, ExtensionRegistry::EVERYTHING); // Hiding works differently with the new and old toolbars. if (include_all_extensions_) { // It's possible that we haven't added this extension yet, if its // visibility was adjusted in the course of its initialization. if (std::find(toolbar_items_.begin(), toolbar_items_.end(), extension) == toolbar_items_.end()) return; int new_size = 0; int new_index = 0; if (is_now_visible) { // If this action used to be hidden, we can't possibly be showing all. DCHECK_LT(visible_icon_count(), toolbar_items_.size()); // Grow the bar by one and move the extension to the end of the visibles. new_size = visible_icon_count() + 1; new_index = new_size - 1; } else { // If we're hiding one, we must be showing at least one. DCHECK_GE(visible_icon_count(), 0u); // Shrink the bar by one and move the extension to the beginning of the // overflow menu. new_size = visible_icon_count() - 1; new_index = new_size; } SetVisibleIconCount(new_size); MoveExtensionIcon(extension->id(), new_index); } else { // Don't include all extensions. if (is_now_visible) AddExtension(extension); else RemoveExtension(extension); } } void ExtensionToolbarModel::OnExtensionLoaded( content::BrowserContext* browser_context, const Extension* extension) { // We don't want to add the same extension twice. It may have already been // added by EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED below, if the user // hides the browser action and then disables and enables the extension. for (size_t i = 0; i < toolbar_items_.size(); i++) { if (toolbar_items_[i].get() == extension) return; } AddExtension(extension); } void ExtensionToolbarModel::OnExtensionUnloaded( content::BrowserContext* browser_context, const Extension* extension, UnloadedExtensionInfo::Reason reason) { RemoveExtension(extension); } void ExtensionToolbarModel::OnExtensionUninstalled( content::BrowserContext* browser_context, const Extension* extension, extensions::UninstallReason reason) { // Remove the extension id from the ordered list, if it exists (the extension // might not be represented in the list because it might not have an icon). ExtensionIdList::iterator pos = std::find(last_known_positions_.begin(), last_known_positions_.end(), extension->id()); if (pos != last_known_positions_.end()) { last_known_positions_.erase(pos); UpdatePrefs(); } } void ExtensionToolbarModel::OnReady() { ExtensionRegistry* registry = ExtensionRegistry::Get(profile_); InitializeExtensionList(); // Wait until the extension system is ready before observing any further // changes so that the toolbar buttons can be shown in their stable ordering // taken from prefs. extension_registry_observer_.Add(registry); extension_action_observer_.Add(extension_action_api_); } size_t ExtensionToolbarModel::FindNewPositionFromLastKnownGood( const Extension* extension) { // See if we have last known good position for this extension. size_t new_index = 0; // Loop through the ID list of known positions, to count the number of visible // extension icons preceding |extension|. for (ExtensionIdList::const_iterator iter_id = last_known_positions_.begin(); iter_id < last_known_positions_.end(); ++iter_id) { if ((*iter_id) == extension->id()) return new_index; // We've found the right position. // Found an id, need to see if it is visible. for (ExtensionList::const_iterator iter_ext = toolbar_items_.begin(); iter_ext < toolbar_items_.end(); ++iter_ext) { if ((*iter_ext)->id() == (*iter_id)) { // This extension is visible, update the index value. ++new_index; break; } } } // Position not found. return toolbar_items_.size(); } bool ExtensionToolbarModel::ShouldAddExtension(const Extension* extension) { // In incognito mode, don't add any extensions that aren't incognito-enabled. if (profile_->IsOffTheRecord() && !util::IsIncognitoEnabled(extension->id(), profile_)) return false; ExtensionActionManager* action_manager = ExtensionActionManager::Get(profile_); if (include_all_extensions_) { // In this case, we don't care about the browser action visibility, because // we want to show each extension regardless. // TODO(devlin): Extension actions which are not visible should be moved to // the overflow menu by default. return action_manager->GetExtensionAction(*extension) != NULL; } return action_manager->GetBrowserAction(*extension) && extension_action_api_->GetBrowserActionVisibility(extension->id()); } void ExtensionToolbarModel::AddExtension(const Extension* extension) { // We only use AddExtension() once the system is initialized. DCHECK(extensions_initialized_); if (!ShouldAddExtension(extension)) return; // See if we have a last known good position for this extension. bool is_new_extension = std::find(last_known_positions_.begin(), last_known_positions_.end(), extension->id()) == last_known_positions_.end(); // New extensions go at the right (end) of the visible extensions. Other // extensions go at their previous position. size_t new_index = 0; if (is_new_extension) { new_index = Manifest::IsComponentLocation(extension->location()) ? 0 : visible_icon_count(); // For the last-known position, we use the index of the extension that is // just before this extension, plus one. (Note that this isn't the same // as new_index + 1, because last_known_positions_ can include disabled // extensions.) int new_last_known_index = new_index == 0 ? 0 : std::find(last_known_positions_.begin(), last_known_positions_.end(), toolbar_items_[new_index - 1]->id()) - last_known_positions_.begin() + 1; // In theory, the extension before this one should always // be in last known positions, but if something funny happened with prefs, // make sure we handle it. // TODO(devlin): Track down these cases so we can CHECK this. new_last_known_index = std::min<int>(new_last_known_index, last_known_positions_.size()); last_known_positions_.insert( last_known_positions_.begin() + new_last_known_index, extension->id()); UpdatePrefs(); } else { new_index = FindNewPositionFromLastKnownGood(extension); } toolbar_items_.insert(toolbar_items_.begin() + new_index, extension); // If we're currently highlighting, then even though we add a browser action // to the full list (|toolbar_items_|, there won't be another *visible* // browser action, which was what the observers care about. if (!is_highlighting_) { FOR_EACH_OBSERVER(Observer, observers_, OnToolbarExtensionAdded(extension, new_index)); int visible_count_delta = 0; if (is_new_extension && !all_icons_visible()) { // If this is a new extension (and not all extensions are visible), we // expand the toolbar out so that the new one can be seen. visible_count_delta = 1; } else if (profile_->IsOffTheRecord()) { // If this is an incognito profile, we also have to check to make sure the // overflow matches the main bar's status. ExtensionToolbarModel* main_model = ExtensionToolbarModel::Get(profile_->GetOriginalProfile()); // Find what the index will be in the main bar. Because Observer calls are // nondeterministic, we can't just assume the main bar will have the // extension and look it up. size_t main_index = main_model->FindNewPositionFromLastKnownGood(extension); bool visible = main_index < main_model->visible_icon_count(); // We may need to adjust the visible count if the incognito bar isn't // showing all icons and this one is visible, or if it is showing all // icons and this is hidden. if (visible && !all_icons_visible()) visible_count_delta = 1; else if (!visible && all_icons_visible()) visible_count_delta = -1; } if (visible_count_delta) SetVisibleIconCount(visible_icon_count() + visible_count_delta); } MaybeUpdateVisibilityPref(extension, new_index); } void ExtensionToolbarModel::RemoveExtension(const Extension* extension) { ExtensionList::iterator pos = std::find(toolbar_items_.begin(), toolbar_items_.end(), extension); if (pos == toolbar_items_.end()) return; // If our visible count is set to the current size, we need to decrement it. if (visible_icon_count_ == static_cast<int>(toolbar_items_.size())) SetVisibleIconCount(toolbar_items_.size() - 1); toolbar_items_.erase(pos); // If we're in highlight mode, we also have to remove the extension from // the highlighted list. if (is_highlighting_) { pos = std::find(highlighted_items_.begin(), highlighted_items_.end(), extension); if (pos != highlighted_items_.end()) { highlighted_items_.erase(pos); FOR_EACH_OBSERVER(Observer, observers_, OnToolbarExtensionRemoved(extension)); // If the highlighted list is now empty, we stop highlighting. if (highlighted_items_.empty()) StopHighlighting(); } } else { FOR_EACH_OBSERVER(Observer, observers_, OnToolbarExtensionRemoved(extension)); } UpdatePrefs(); } // Combine the currently enabled extensions that have browser actions (which // we get from the ExtensionRegistry) with the ordering we get from the // pref service. For robustness we use a somewhat inefficient process: // 1. Create a vector of extensions sorted by their pref values. This vector may // have holes. // 2. Create a vector of extensions that did not have a pref value. // 3. Remove holes from the sorted vector and append the unsorted vector. void ExtensionToolbarModel::InitializeExtensionList() { DCHECK(toolbar_items_.empty()); // We shouldn't have any items yet. last_known_positions_ = extension_prefs_->GetToolbarOrder(); if (profile_->IsOffTheRecord()) IncognitoPopulate(); else Populate(&last_known_positions_); extensions_initialized_ = true; MaybeUpdateVisibilityPrefs(); FOR_EACH_OBSERVER(Observer, observers_, OnToolbarModelInitialized()); } void ExtensionToolbarModel::Populate(ExtensionIdList* positions) { DCHECK(!profile_->IsOffTheRecord()); const ExtensionSet& extensions = ExtensionRegistry::Get(profile_)->enabled_extensions(); // Items that have explicit positions. ExtensionList sorted(positions->size(), NULL); // The items that don't have explicit positions. ExtensionList unsorted; // Create the lists. int hidden = 0; for (const scoped_refptr<const Extension>& extension : extensions) { if (!ShouldAddExtension(extension.get())) { if (!extension_action_api_->GetBrowserActionVisibility(extension->id())) ++hidden; continue; } ExtensionIdList::const_iterator pos = std::find(positions->begin(), positions->end(), extension->id()); if (pos != positions->end()) { sorted[pos - positions->begin()] = extension; } else { // Unknown extension - push it to the back of unsorted, and add it to the // list of ids at the end. unsorted.push_back(extension); positions->push_back(extension->id()); } } // Merge the lists. sorted.insert(sorted.end(), unsorted.begin(), unsorted.end()); toolbar_items_.reserve(sorted.size()); for (const scoped_refptr<const Extension>& extension : sorted) { // It's possible for the extension order to contain items that aren't // actually loaded on this machine. For example, when extension sync is on, // we sync the extension order as-is but double-check with the user before // syncing NPAPI-containing extensions, so if one of those is not actually // synced, we'll get a NULL in the list. This sort of case can also happen // if some error prevents an extension from loading. if (extension.get()) { // We don't notify observers of the added extension yet. Rather, observers // should wait for the "OnToolbarModelInitialized" notification, and then // bulk-update. (This saves a lot of bouncing-back-and-forth here, and // allows observers to ensure that the extension system is always // initialized before using the extensions). toolbar_items_.push_back(extension); } } UMA_HISTOGRAM_COUNTS_100( "ExtensionToolbarModel.BrowserActionsPermanentlyHidden", hidden); UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsCount", toolbar_items_.size()); if (!toolbar_items_.empty()) { // Visible count can be -1, meaning: 'show all'. Since UMA converts negative // values to 0, this would be counted as 'show none' unless we convert it to // max. UMA_HISTOGRAM_COUNTS_100("ExtensionToolbarModel.BrowserActionsVisible", visible_icon_count_ == -1 ? base::HistogramBase::kSampleType_MAX : visible_icon_count_); } } void ExtensionToolbarModel::IncognitoPopulate() { DCHECK(profile_->IsOffTheRecord()); const ExtensionToolbarModel* original_model = ExtensionToolbarModel::Get(profile_->GetOriginalProfile()); // Find the absolute value of the original model's count. int original_visible = original_model->visible_icon_count(); // In incognito mode, we show only those extensions that are // incognito-enabled. Further, any actions that were overflowed in regular // mode are still overflowed. Order is the same as in regular mode. visible_icon_count_ = 0; for (ExtensionList::const_iterator iter = original_model->toolbar_items_.begin(); iter != original_model->toolbar_items_.end(); ++iter) { if (ShouldAddExtension(iter->get())) { toolbar_items_.push_back(*iter); if (iter - original_model->toolbar_items_.begin() < original_visible) ++visible_icon_count_; } } } void ExtensionToolbarModel::UpdatePrefs() { if (!extension_prefs_ || profile_->IsOffTheRecord()) return; // Don't observe change caused by self. pref_change_registrar_.Remove(pref_names::kToolbar); extension_prefs_->SetToolbarOrder(last_known_positions_); pref_change_registrar_.Add(pref_names::kToolbar, pref_change_callback_); } void ExtensionToolbarModel::MaybeUpdateVisibilityPref( const Extension* extension, size_t index) { // We only update the visibility pref for hidden/not hidden based on the // overflow menu with the new toolbar design. if (include_all_extensions_ && !profile_->IsOffTheRecord()) { bool visible = index < visible_icon_count(); if (visible != extension_action_api_->GetBrowserActionVisibility( extension->id())) { // Don't observe changes caused by ourselves. bool was_registered = false; if (extension_action_observer_.IsObserving(extension_action_api_)) { was_registered = true; extension_action_observer_.RemoveAll(); } extension_action_api_->SetBrowserActionVisibility(extension->id(), visible); if (was_registered) extension_action_observer_.Add(extension_action_api_); } } } void ExtensionToolbarModel::MaybeUpdateVisibilityPrefs() { for (size_t i = 0u; i < toolbar_items_.size(); ++i) MaybeUpdateVisibilityPref(toolbar_items_[i].get(), i); } void ExtensionToolbarModel::OnExtensionToolbarPrefChange() { // If extensions are not ready, defer to later Populate() call. if (!extensions_initialized_) return; // Recalculate |last_known_positions_| to be |pref_positions| followed by // ones that are only in |last_known_positions_|. ExtensionIdList pref_positions = extension_prefs_->GetToolbarOrder(); size_t pref_position_size = pref_positions.size(); for (size_t i = 0; i < last_known_positions_.size(); ++i) { if (std::find(pref_positions.begin(), pref_positions.end(), last_known_positions_[i]) == pref_positions.end()) { pref_positions.push_back(last_known_positions_[i]); } } last_known_positions_.swap(pref_positions); int desired_index = 0; // Loop over the updated list of last known positions, moving any extensions // that are in the wrong place. for (const std::string& id : last_known_positions_) { int current_index = GetIndexForId(id); if (current_index == -1) continue; if (current_index != desired_index) { scoped_refptr<const Extension> extension = toolbar_items_[current_index]; toolbar_items_.erase(toolbar_items_.begin() + current_index); toolbar_items_.insert(toolbar_items_.begin() + desired_index, extension); // Notify the observers to keep them up-to-date. FOR_EACH_OBSERVER( Observer, observers_, OnToolbarExtensionMoved(extension.get(), desired_index)); } ++desired_index; } if (last_known_positions_.size() > pref_position_size) { // Need to update pref because we have extra icons. But can't call // UpdatePrefs() directly within observation closure. base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::Bind(&ExtensionToolbarModel::UpdatePrefs, weak_ptr_factory_.GetWeakPtr())); } } int ExtensionToolbarModel::GetIndexForId(const std::string& id) const { for (size_t i = 0; i < toolbar_items().size(); ++i) { if (toolbar_items()[i]->id() == id) return i; } return -1; } bool ExtensionToolbarModel::ShowExtensionActionPopup( const Extension* extension, Browser* browser, bool grant_active_tab) { base::ObserverListBase<Observer>::Iterator it(&observers_); Observer* obs = NULL; // Look for the Observer associated with the browser. // This would be cleaner if we had an abstract class for the Toolbar UI // (like we do for LocationBar), but sadly, we don't. while ((obs = it.GetNext()) != NULL) { if (obs->GetBrowser() == browser) return obs->ShowExtensionActionPopup(extension, grant_active_tab); } return false; } void ExtensionToolbarModel::EnsureVisibility( const ExtensionIdList& extension_ids) { if (all_icons_visible()) return; // Already showing all. // Otherwise, make sure we have enough room to show all the extensions // requested. if (visible_icon_count() < extension_ids.size()) SetVisibleIconCount(extension_ids.size()); if (all_icons_visible()) return; // May have been set to max by SetVisibleIconCount. // Guillotine's Delight: Move an orange noble to the front of the line. for (ExtensionIdList::const_iterator it = extension_ids.begin(); it != extension_ids.end(); ++it) { for (ExtensionList::const_iterator extension = toolbar_items_.begin(); extension != toolbar_items_.end(); ++extension) { if ((*extension)->id() == (*it)) { if (extension - toolbar_items_.begin() >= static_cast<int>(visible_icon_count())) MoveExtensionIcon((*extension)->id(), 0); break; } } } } bool ExtensionToolbarModel::HighlightExtensions( const ExtensionIdList& extension_ids) { highlighted_items_.clear(); for (ExtensionIdList::const_iterator id = extension_ids.begin(); id != extension_ids.end(); ++id) { for (ExtensionList::const_iterator extension = toolbar_items_.begin(); extension != toolbar_items_.end(); ++extension) { if (*id == (*extension)->id()) highlighted_items_.push_back(*extension); } } // If we have any items in |highlighted_items_|, then we entered highlighting // mode. if (highlighted_items_.size()) { old_visible_icon_count_ = visible_icon_count_; if (visible_icon_count() < extension_ids.size()) SetVisibleIconCount(extension_ids.size()); // It's important that is_highlighting_ is changed immediately before the // observers are notified since it changes the result of toolbar_items(). is_highlighting_ = true; FOR_EACH_OBSERVER(Observer, observers_, OnToolbarHighlightModeChanged(true)); return true; } // Otherwise, we didn't enter highlighting mode (and, in fact, exited it if // we were otherwise in it). if (is_highlighting_) StopHighlighting(); return false; } void ExtensionToolbarModel::StopHighlighting() { if (is_highlighting_) { if (old_visible_icon_count_ != visible_icon_count_) SetVisibleIconCount(old_visible_icon_count_); // It's important that is_highlighting_ is changed immediately before the // observers are notified since it changes the result of toolbar_items(). is_highlighting_ = false; FOR_EACH_OBSERVER(Observer, observers_, OnToolbarHighlightModeChanged(false)); // For the same reason, we don't clear highlighted_items_ until after the // mode changed. highlighted_items_.clear(); } } bool ExtensionToolbarModel::RedesignIsShowingNewIcons() const { for (const scoped_refptr<const Extension>& extension : toolbar_items_) { // Without the redesign, we only show extensions with browser actions. // Any extension without a browser action is an indication that we're // showing something new. if (!extension->manifest()->HasKey(manifest_keys::kBrowserAction)) return true; } return false; } } // namespace extensions