extension_toolbar_model.cc revision 6e8cce623b6e4fe0c9e4af605d675dd9d0338c38
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_manager.h" 15#include "chrome/browser/extensions/extension_tab_util.h" 16#include "chrome/browser/extensions/extension_toolbar_model_factory.h" 17#include "chrome/browser/extensions/extension_util.h" 18#include "chrome/browser/extensions/tab_helper.h" 19#include "chrome/browser/profiles/profile.h" 20#include "chrome/browser/ui/browser.h" 21#include "chrome/browser/ui/tabs/tab_strip_model.h" 22#include "chrome/common/pref_names.h" 23#include "content/public/browser/notification_details.h" 24#include "content/public/browser/notification_source.h" 25#include "content/public/browser/web_contents.h" 26#include "extensions/browser/extension_prefs.h" 27#include "extensions/browser/extension_registry.h" 28#include "extensions/browser/extension_system.h" 29#include "extensions/browser/pref_names.h" 30#include "extensions/common/extension.h" 31#include "extensions/common/extension_set.h" 32#include "extensions/common/feature_switch.h" 33#include "extensions/common/one_shot_event.h" 34 35namespace extensions { 36 37bool ExtensionToolbarModel::Observer::BrowserActionShowPopup( 38 const Extension* extension) { 39 return false; 40} 41 42ExtensionToolbarModel::ExtensionToolbarModel(Profile* profile, 43 ExtensionPrefs* extension_prefs) 44 : profile_(profile), 45 extension_prefs_(extension_prefs), 46 prefs_(profile_->GetPrefs()), 47 extensions_initialized_(false), 48 is_highlighting_(false), 49 extension_registry_observer_(this), 50 weak_ptr_factory_(this) { 51 ExtensionSystem::Get(profile_)->ready().Post( 52 FROM_HERE, 53 base::Bind(&ExtensionToolbarModel::OnReady, 54 weak_ptr_factory_.GetWeakPtr())); 55 visible_icon_count_ = prefs_->GetInteger(pref_names::kToolbarSize); 56 pref_change_registrar_.Init(prefs_); 57 pref_change_callback_ = 58 base::Bind(&ExtensionToolbarModel::OnExtensionToolbarPrefChange, 59 base::Unretained(this)); 60 pref_change_registrar_.Add(pref_names::kToolbar, pref_change_callback_); 61} 62 63ExtensionToolbarModel::~ExtensionToolbarModel() { 64} 65 66// static 67ExtensionToolbarModel* ExtensionToolbarModel::Get(Profile* profile) { 68 return ExtensionToolbarModelFactory::GetForProfile(profile); 69} 70 71void ExtensionToolbarModel::AddObserver(Observer* observer) { 72 observers_.AddObserver(observer); 73} 74 75void ExtensionToolbarModel::RemoveObserver(Observer* observer) { 76 observers_.RemoveObserver(observer); 77} 78 79void ExtensionToolbarModel::MoveBrowserAction(const Extension* extension, 80 int index) { 81 ExtensionList::iterator pos = std::find(toolbar_items_.begin(), 82 toolbar_items_.end(), extension); 83 if (pos == toolbar_items_.end()) { 84 NOTREACHED(); 85 return; 86 } 87 toolbar_items_.erase(pos); 88 89 ExtensionIdList::iterator pos_id; 90 pos_id = std::find(last_known_positions_.begin(), 91 last_known_positions_.end(), extension->id()); 92 if (pos_id != last_known_positions_.end()) 93 last_known_positions_.erase(pos_id); 94 95 int i = 0; 96 bool inserted = false; 97 for (ExtensionList::iterator iter = toolbar_items_.begin(); 98 iter != toolbar_items_.end(); 99 ++iter, ++i) { 100 if (i == index) { 101 pos_id = std::find(last_known_positions_.begin(), 102 last_known_positions_.end(), (*iter)->id()); 103 last_known_positions_.insert(pos_id, extension->id()); 104 105 toolbar_items_.insert(iter, make_scoped_refptr(extension)); 106 inserted = true; 107 break; 108 } 109 } 110 111 if (!inserted) { 112 DCHECK_EQ(index, static_cast<int>(toolbar_items_.size())); 113 index = toolbar_items_.size(); 114 115 toolbar_items_.push_back(make_scoped_refptr(extension)); 116 last_known_positions_.push_back(extension->id()); 117 } 118 119 FOR_EACH_OBSERVER(Observer, observers_, BrowserActionMoved(extension, index)); 120 121 UpdatePrefs(); 122} 123 124ExtensionAction::ShowAction ExtensionToolbarModel::ExecuteBrowserAction( 125 const Extension* extension, 126 Browser* browser, 127 GURL* popup_url_out, 128 bool should_grant) { 129 content::WebContents* web_contents = NULL; 130 int tab_id = 0; 131 if (!ExtensionTabUtil::GetDefaultTab(browser, &web_contents, &tab_id)) 132 return ExtensionAction::ACTION_NONE; 133 134 ExtensionAction* browser_action = 135 ExtensionActionManager::Get(profile_)->GetBrowserAction(*extension); 136 137 // For browser actions, visibility == enabledness. 138 if (!browser_action->GetIsVisible(tab_id)) 139 return ExtensionAction::ACTION_NONE; 140 141 if (should_grant) { 142 TabHelper::FromWebContents(web_contents) 143 ->active_tab_permission_granter() 144 ->GrantIfRequested(extension); 145 } 146 147 if (browser_action->HasPopup(tab_id)) { 148 if (popup_url_out) 149 *popup_url_out = browser_action->GetPopupUrl(tab_id); 150 return ExtensionAction::ACTION_SHOW_POPUP; 151 } 152 153 ExtensionActionAPI::BrowserActionExecuted( 154 browser->profile(), *browser_action, web_contents); 155 return ExtensionAction::ACTION_NONE; 156} 157 158void ExtensionToolbarModel::SetVisibleIconCount(int count) { 159 visible_icon_count_ = 160 count == static_cast<int>(toolbar_items_.size()) ? -1 : count; 161 // Only set the prefs if we're not in highlight mode. Highlight mode is 162 // designed to be a transitory state, and should not persist across browser 163 // restarts (though it may be re-entered). 164 if (!is_highlighting_) 165 prefs_->SetInteger(pref_names::kToolbarSize, visible_icon_count_); 166} 167 168void ExtensionToolbarModel::OnExtensionLoaded( 169 content::BrowserContext* browser_context, 170 const Extension* extension) { 171 // We don't want to add the same extension twice. It may have already been 172 // added by EXTENSION_BROWSER_ACTION_VISIBILITY_CHANGED below, if the user 173 // hides the browser action and then disables and enables the extension. 174 for (size_t i = 0; i < toolbar_items_.size(); i++) { 175 if (toolbar_items_[i].get() == extension) 176 return; 177 } 178 if (ExtensionActionAPI::GetBrowserActionVisibility(extension_prefs_, 179 extension->id())) { 180 AddExtension(extension); 181 } 182} 183 184void ExtensionToolbarModel::OnExtensionUnloaded( 185 content::BrowserContext* browser_context, 186 const Extension* extension, 187 UnloadedExtensionInfo::Reason reason) { 188 RemoveExtension(extension); 189} 190 191void ExtensionToolbarModel::OnExtensionUninstalled( 192 content::BrowserContext* browser_context, 193 const Extension* extension, 194 extensions::UninstallReason reason) { 195 // Remove the extension id from the ordered list, if it exists (the extension 196 // might not be represented in the list because it might not have an icon). 197 ExtensionIdList::iterator pos = 198 std::find(last_known_positions_.begin(), 199 last_known_positions_.end(), extension->id()); 200 201 if (pos != last_known_positions_.end()) { 202 last_known_positions_.erase(pos); 203 UpdatePrefs(); 204 } 205} 206 207void ExtensionToolbarModel::Observe( 208 int type, 209 const content::NotificationSource& source, 210 const content::NotificationDetails& details) { 211 DCHECK_EQ( 212 extensions::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 extensions::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