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