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.
5 #include "chrome/browser/extensions/extension_sorting.h"
10 #include "chrome/browser/chrome_notification_types.h"
11 #include "chrome/browser/extensions/extension_scoped_prefs.h"
12 #include "chrome/browser/extensions/extension_sync_service.h"
13 #include "chrome/common/extensions/extension.h"
14 #include "content/public/browser/notification_service.h"
16 #if defined(OS_CHROMEOS)
17 #include "chrome/browser/chromeos/extensions/default_app_order.h"
20 using extensions::ExtensionPrefs;
24 // The number of apps per page. This isn't a hard limit, but new apps installed
25 // from the webstore will overflow onto a new page if this limit is reached.
26 const size_t kNaturalAppPageSize = 18;
28 // A preference determining the order of which the apps appear on the NTP.
29 const char kPrefAppLaunchIndexDeprecated[] = "app_launcher_index";
30 const char kPrefAppLaunchOrdinal[] = "app_launcher_ordinal";
32 // A preference determining the page on which an app appears in the NTP.
33 const char kPrefPageIndexDeprecated[] = "page_index";
34 const char kPrefPageOrdinal[] = "page_ordinal";
38 ////////////////////////////////////////////////////////////////////////////////
39 // ExtensionSorting::AppOrdinals
41 ExtensionSorting::AppOrdinals::AppOrdinals() {}
43 ExtensionSorting::AppOrdinals::~AppOrdinals() {}
45 ////////////////////////////////////////////////////////////////////////////////
48 ExtensionSorting::ExtensionSorting(ExtensionScopedPrefs* extension_scoped_prefs)
49 : extension_scoped_prefs_(extension_scoped_prefs),
50 extension_sync_service_(NULL),
51 default_ordinals_created_(false) {
54 ExtensionSorting::~ExtensionSorting() {
57 void ExtensionSorting::SetExtensionSyncService(
58 ExtensionSyncService* extension_sync_service) {
59 extension_sync_service_ = extension_sync_service;
62 void ExtensionSorting::Initialize(
63 const extensions::ExtensionIdList& extension_ids) {
64 InitializePageOrdinalMap(extension_ids);
66 MigrateAppIndex(extension_ids);
69 void ExtensionSorting::CreateOrdinalsIfNecessary(size_t minimum_size) {
70 // Create StringOrdinal values as required to ensure |ntp_ordinal_map_| has at
71 // least |minimum_size| entries.
72 if (ntp_ordinal_map_.empty() && minimum_size > 0)
73 ntp_ordinal_map_[syncer::StringOrdinal::CreateInitialOrdinal()];
75 while (ntp_ordinal_map_.size() < minimum_size) {
76 syncer::StringOrdinal filler =
77 ntp_ordinal_map_.rbegin()->first.CreateAfter();
78 AppLaunchOrdinalMap empty_ordinal_map;
79 ntp_ordinal_map_.insert(std::make_pair(filler, empty_ordinal_map));
83 void ExtensionSorting::MigrateAppIndex(
84 const extensions::ExtensionIdList& extension_ids) {
85 if (extension_ids.empty())
88 // Convert all the page index values to page ordinals. If there are any
89 // app launch values that need to be migrated, inserted them into a sorted
90 // set to be dealt with later.
91 typedef std::map<syncer::StringOrdinal, std::map<int, const std::string*>,
92 syncer::StringOrdinal::LessThanFn> AppPositionToIdMapping;
93 AppPositionToIdMapping app_launches_to_convert;
94 for (extensions::ExtensionIdList::const_iterator ext_id =
95 extension_ids.begin(); ext_id != extension_ids.end(); ++ext_id) {
96 int old_page_index = 0;
97 syncer::StringOrdinal page = GetPageOrdinal(*ext_id);
98 if (extension_scoped_prefs_->ReadPrefAsInteger(
100 kPrefPageIndexDeprecated,
102 // Some extensions have invalid page index, so we don't
103 // attempt to convert them.
104 if (old_page_index < 0) {
105 DLOG(WARNING) << "Extension " << *ext_id
106 << " has an invalid page index " << old_page_index
107 << ". Aborting attempt to convert its index.";
111 CreateOrdinalsIfNecessary(static_cast<size_t>(old_page_index) + 1);
113 page = PageIntegerAsStringOrdinal(old_page_index);
114 SetPageOrdinal(*ext_id, page);
115 extension_scoped_prefs_->UpdateExtensionPref(
116 *ext_id, kPrefPageIndexDeprecated, NULL);
119 int old_app_launch_index = 0;
120 if (extension_scoped_prefs_->ReadPrefAsInteger(
122 kPrefAppLaunchIndexDeprecated,
123 &old_app_launch_index)) {
124 // We can't update the app launch index value yet, because we use
125 // GetNextAppLaunchOrdinal to get the new ordinal value and it requires
126 // all the ordinals with lower values to have already been migrated.
127 // A valid page ordinal is also required because otherwise there is
128 // no page to add the app to.
130 app_launches_to_convert[page][old_app_launch_index] = &*ext_id;
132 extension_scoped_prefs_->UpdateExtensionPref(
133 *ext_id, kPrefAppLaunchIndexDeprecated, NULL);
137 // Remove any empty pages that may have been added. This shouldn't occur,
138 // but double check here to prevent future problems with conversions between
139 // integers and StringOrdinals.
140 for (PageOrdinalMap::iterator it = ntp_ordinal_map_.begin();
141 it != ntp_ordinal_map_.end();) {
142 if (it->second.empty()) {
143 PageOrdinalMap::iterator prev_it = it;
145 ntp_ordinal_map_.erase(prev_it);
151 if (app_launches_to_convert.empty())
154 // Create the new app launch ordinals and remove the old preferences. Since
155 // the set is sorted, each time we migrate an apps index, we know that all of
156 // the remaining apps will appear further down the NTP than it or on a
158 for (AppPositionToIdMapping::const_iterator page_it =
159 app_launches_to_convert.begin();
160 page_it != app_launches_to_convert.end(); ++page_it) {
161 syncer::StringOrdinal page = page_it->first;
162 for (std::map<int, const std::string*>::const_iterator launch_it =
163 page_it->second.begin(); launch_it != page_it->second.end();
165 SetAppLaunchOrdinal(*(launch_it->second),
166 CreateNextAppLaunchOrdinal(page));
171 void ExtensionSorting::FixNTPOrdinalCollisions() {
172 for (PageOrdinalMap::iterator page_it = ntp_ordinal_map_.begin();
173 page_it != ntp_ordinal_map_.end(); ++page_it) {
174 AppLaunchOrdinalMap& page = page_it->second;
176 AppLaunchOrdinalMap::iterator app_launch_it = page.begin();
177 while (app_launch_it != page.end()) {
178 int app_count = page.count(app_launch_it->first);
179 if (app_count == 1) {
184 syncer::StringOrdinal repeated_ordinal = app_launch_it->first;
186 // Sort the conflicting keys by their extension id, this is how
187 // the order is decided.
188 std::vector<std::string> conflicting_ids;
189 for (int i = 0; i < app_count; ++i, ++app_launch_it)
190 conflicting_ids.push_back(app_launch_it->second);
191 std::sort(conflicting_ids.begin(), conflicting_ids.end());
193 syncer::StringOrdinal upper_bound_ordinal = app_launch_it == page.end() ?
194 syncer::StringOrdinal() :
195 app_launch_it->first;
196 syncer::StringOrdinal lower_bound_ordinal = repeated_ordinal;
198 // Start at position 1 because the first extension can keep the conflicted
200 for (int i = 1; i < app_count; ++i) {
201 syncer::StringOrdinal unique_app_launch;
202 if (upper_bound_ordinal.IsValid()) {
204 lower_bound_ordinal.CreateBetween(upper_bound_ordinal);
206 unique_app_launch = lower_bound_ordinal.CreateAfter();
209 SetAppLaunchOrdinal(conflicting_ids[i], unique_app_launch);
210 lower_bound_ordinal = unique_app_launch;
215 content::NotificationService::current()->Notify(
216 chrome::NOTIFICATION_EXTENSION_LAUNCHER_REORDERED,
217 content::Source<ExtensionSorting>(this),
218 content::NotificationService::NoDetails());
221 void ExtensionSorting::EnsureValidOrdinals(
222 const std::string& extension_id,
223 const syncer::StringOrdinal& suggested_page) {
224 syncer::StringOrdinal page_ordinal = GetPageOrdinal(extension_id);
225 if (!page_ordinal.IsValid()) {
226 if (suggested_page.IsValid()) {
227 page_ordinal = suggested_page;
228 } else if (!GetDefaultOrdinals(extension_id, &page_ordinal, NULL) ||
229 !page_ordinal.IsValid()) {
230 page_ordinal = GetNaturalAppPageOrdinal();
233 SetPageOrdinal(extension_id, page_ordinal);
236 syncer::StringOrdinal app_launch_ordinal = GetAppLaunchOrdinal(extension_id);
237 if (!app_launch_ordinal.IsValid()) {
238 // If using default app launcher ordinal, make sure there is no collision.
239 if (GetDefaultOrdinals(extension_id, NULL, &app_launch_ordinal) &&
240 app_launch_ordinal.IsValid())
241 app_launch_ordinal = ResolveCollision(page_ordinal, app_launch_ordinal);
243 app_launch_ordinal = CreateNextAppLaunchOrdinal(page_ordinal);
245 SetAppLaunchOrdinal(extension_id, app_launch_ordinal);
249 void ExtensionSorting::OnExtensionMoved(
250 const std::string& moved_extension_id,
251 const std::string& predecessor_extension_id,
252 const std::string& successor_extension_id) {
253 // We only need to change the StringOrdinal if there are neighbours.
254 if (!predecessor_extension_id.empty() || !successor_extension_id.empty()) {
255 if (predecessor_extension_id.empty()) {
259 GetAppLaunchOrdinal(successor_extension_id).CreateBefore());
260 } else if (successor_extension_id.empty()) {
261 // Only a predecessor.
264 GetAppLaunchOrdinal(predecessor_extension_id).CreateAfter());
266 // Both a successor and predecessor
267 const syncer::StringOrdinal& predecessor_ordinal =
268 GetAppLaunchOrdinal(predecessor_extension_id);
269 const syncer::StringOrdinal& successor_ordinal =
270 GetAppLaunchOrdinal(successor_extension_id);
271 SetAppLaunchOrdinal(moved_extension_id,
272 predecessor_ordinal.CreateBetween(successor_ordinal));
276 content::NotificationService::current()->Notify(
277 chrome::NOTIFICATION_EXTENSION_LAUNCHER_REORDERED,
278 content::Source<ExtensionSorting>(this),
279 content::Details<const std::string>(&moved_extension_id));
283 syncer::StringOrdinal ExtensionSorting::GetAppLaunchOrdinal(
284 const std::string& extension_id) const {
285 std::string raw_value;
286 // If the preference read fails then raw_value will still be unset and we
287 // will return an invalid StringOrdinal to signal that no app launch ordinal
289 extension_scoped_prefs_->ReadPrefAsString(
290 extension_id, kPrefAppLaunchOrdinal, &raw_value);
291 return syncer::StringOrdinal(raw_value);
294 void ExtensionSorting::SetAppLaunchOrdinal(
295 const std::string& extension_id,
296 const syncer::StringOrdinal& new_app_launch_ordinal) {
297 // No work is required if the old and new values are the same.
298 if (new_app_launch_ordinal.EqualsOrBothInvalid(
299 GetAppLaunchOrdinal(extension_id))) {
303 syncer::StringOrdinal page_ordinal = GetPageOrdinal(extension_id);
304 RemoveOrdinalMapping(
305 extension_id, page_ordinal, GetAppLaunchOrdinal(extension_id));
306 AddOrdinalMapping(extension_id, page_ordinal, new_app_launch_ordinal);
308 Value* new_value = new_app_launch_ordinal.IsValid() ?
309 new base::StringValue(new_app_launch_ordinal.ToInternalValue()) :
312 extension_scoped_prefs_->UpdateExtensionPref(
314 kPrefAppLaunchOrdinal,
316 SyncIfNeeded(extension_id);
319 syncer::StringOrdinal ExtensionSorting::CreateFirstAppLaunchOrdinal(
320 const syncer::StringOrdinal& page_ordinal) const {
321 const syncer::StringOrdinal& min_ordinal =
322 GetMinOrMaxAppLaunchOrdinalsOnPage(page_ordinal,
323 ExtensionSorting::MIN_ORDINAL);
325 if (min_ordinal.IsValid())
326 return min_ordinal.CreateBefore();
328 return syncer::StringOrdinal::CreateInitialOrdinal();
331 syncer::StringOrdinal ExtensionSorting::CreateNextAppLaunchOrdinal(
332 const syncer::StringOrdinal& page_ordinal) const {
333 const syncer::StringOrdinal& max_ordinal =
334 GetMinOrMaxAppLaunchOrdinalsOnPage(page_ordinal,
335 ExtensionSorting::MAX_ORDINAL);
337 if (max_ordinal.IsValid())
338 return max_ordinal.CreateAfter();
340 return syncer::StringOrdinal::CreateInitialOrdinal();
343 syncer::StringOrdinal ExtensionSorting::CreateFirstAppPageOrdinal() const {
344 if (ntp_ordinal_map_.empty())
345 return syncer::StringOrdinal::CreateInitialOrdinal();
347 return ntp_ordinal_map_.begin()->first;
350 syncer::StringOrdinal ExtensionSorting::GetNaturalAppPageOrdinal() const {
351 if (ntp_ordinal_map_.empty())
352 return syncer::StringOrdinal::CreateInitialOrdinal();
354 for (PageOrdinalMap::const_iterator it = ntp_ordinal_map_.begin();
355 it != ntp_ordinal_map_.end(); ++it) {
356 if (CountItemsVisibleOnNtp(it->second) < kNaturalAppPageSize)
360 // Add a new page as all existing pages are full.
361 syncer::StringOrdinal last_element = ntp_ordinal_map_.rbegin()->first;
362 return last_element.CreateAfter();
365 syncer::StringOrdinal ExtensionSorting::GetPageOrdinal(
366 const std::string& extension_id) const {
367 std::string raw_data;
368 // If the preference read fails then raw_data will still be unset and we will
369 // return an invalid StringOrdinal to signal that no page ordinal was found.
370 extension_scoped_prefs_->ReadPrefAsString(
371 extension_id, kPrefPageOrdinal, &raw_data);
372 return syncer::StringOrdinal(raw_data);
375 void ExtensionSorting::SetPageOrdinal(
376 const std::string& extension_id,
377 const syncer::StringOrdinal& new_page_ordinal) {
378 // No work is required if the old and new values are the same.
379 if (new_page_ordinal.EqualsOrBothInvalid(GetPageOrdinal(extension_id)))
382 syncer::StringOrdinal app_launch_ordinal = GetAppLaunchOrdinal(extension_id);
383 RemoveOrdinalMapping(
384 extension_id, GetPageOrdinal(extension_id), app_launch_ordinal);
385 AddOrdinalMapping(extension_id, new_page_ordinal, app_launch_ordinal);
387 Value* new_value = new_page_ordinal.IsValid() ?
388 new base::StringValue(new_page_ordinal.ToInternalValue()) :
391 extension_scoped_prefs_->UpdateExtensionPref(
395 SyncIfNeeded(extension_id);
398 void ExtensionSorting::ClearOrdinals(const std::string& extension_id) {
399 RemoveOrdinalMapping(extension_id,
400 GetPageOrdinal(extension_id),
401 GetAppLaunchOrdinal(extension_id));
403 extension_scoped_prefs_->UpdateExtensionPref(
404 extension_id, kPrefPageOrdinal, NULL);
405 extension_scoped_prefs_->UpdateExtensionPref(
406 extension_id, kPrefAppLaunchOrdinal, NULL);
409 int ExtensionSorting::PageStringOrdinalAsInteger(
410 const syncer::StringOrdinal& page_ordinal) const {
411 if (!page_ordinal.IsValid())
414 PageOrdinalMap::const_iterator it = ntp_ordinal_map_.find(page_ordinal);
415 return it != ntp_ordinal_map_.end() ?
416 std::distance(ntp_ordinal_map_.begin(), it) : -1;
419 syncer::StringOrdinal ExtensionSorting::PageIntegerAsStringOrdinal(
421 if (page_index < ntp_ordinal_map_.size()) {
422 PageOrdinalMap::const_iterator it = ntp_ordinal_map_.begin();
423 std::advance(it, page_index);
427 CreateOrdinalsIfNecessary(page_index + 1);
428 return ntp_ordinal_map_.rbegin()->first;
431 void ExtensionSorting::MarkExtensionAsHidden(const std::string& extension_id) {
432 ntp_hidden_extensions_.insert(extension_id);
435 syncer::StringOrdinal ExtensionSorting::GetMinOrMaxAppLaunchOrdinalsOnPage(
436 const syncer::StringOrdinal& target_page_ordinal,
437 AppLaunchOrdinalReturn return_type) const {
438 CHECK(target_page_ordinal.IsValid());
440 syncer::StringOrdinal return_value;
442 PageOrdinalMap::const_iterator page =
443 ntp_ordinal_map_.find(target_page_ordinal);
444 if (page != ntp_ordinal_map_.end()) {
445 const AppLaunchOrdinalMap& app_list = page->second;
447 if (app_list.empty())
448 return syncer::StringOrdinal();
450 if (return_type == ExtensionSorting::MAX_ORDINAL)
451 return_value = app_list.rbegin()->first;
452 else if (return_type == ExtensionSorting::MIN_ORDINAL)
453 return_value = app_list.begin()->first;
459 void ExtensionSorting::InitializePageOrdinalMap(
460 const extensions::ExtensionIdList& extension_ids) {
461 for (extensions::ExtensionIdList::const_iterator ext_it =
462 extension_ids.begin(); ext_it != extension_ids.end(); ++ext_it) {
463 AddOrdinalMapping(*ext_it,
464 GetPageOrdinal(*ext_it),
465 GetAppLaunchOrdinal(*ext_it));
467 // Ensure that the web store app still isn't found in this list, since
468 // it is added after this loop.
469 DCHECK(*ext_it != extension_misc::kWebStoreAppId);
470 DCHECK(*ext_it != extension_misc::kChromeAppId);
473 // Include the Web Store App since it is displayed on the NTP.
474 syncer::StringOrdinal web_store_app_page =
475 GetPageOrdinal(extension_misc::kWebStoreAppId);
476 if (web_store_app_page.IsValid()) {
477 AddOrdinalMapping(extension_misc::kWebStoreAppId,
479 GetAppLaunchOrdinal(extension_misc::kWebStoreAppId));
481 // Include the Chrome App since it is displayed in the app launcher.
482 syncer::StringOrdinal chrome_app_page =
483 GetPageOrdinal(extension_misc::kChromeAppId);
484 if (chrome_app_page.IsValid()) {
485 AddOrdinalMapping(extension_misc::kChromeAppId,
487 GetAppLaunchOrdinal(extension_misc::kChromeAppId));
491 void ExtensionSorting::AddOrdinalMapping(
492 const std::string& extension_id,
493 const syncer::StringOrdinal& page_ordinal,
494 const syncer::StringOrdinal& app_launch_ordinal) {
495 if (!page_ordinal.IsValid() || !app_launch_ordinal.IsValid())
498 ntp_ordinal_map_[page_ordinal].insert(
499 std::make_pair(app_launch_ordinal, extension_id));
502 void ExtensionSorting::RemoveOrdinalMapping(
503 const std::string& extension_id,
504 const syncer::StringOrdinal& page_ordinal,
505 const syncer::StringOrdinal& app_launch_ordinal) {
506 if (!page_ordinal.IsValid() || !app_launch_ordinal.IsValid())
509 // Check that the page exists using find to prevent creating a new page
510 // if |page_ordinal| isn't a used page.
511 PageOrdinalMap::iterator page_map = ntp_ordinal_map_.find(page_ordinal);
512 if (page_map == ntp_ordinal_map_.end())
515 for (AppLaunchOrdinalMap::iterator it =
516 page_map->second.find(app_launch_ordinal);
517 it != page_map->second.end(); ++it) {
518 if (it->second == extension_id) {
519 page_map->second.erase(it);
525 void ExtensionSorting::SyncIfNeeded(const std::string& extension_id) {
526 if (extension_sync_service_)
527 extension_sync_service_->SyncOrderingChange(extension_id);
530 void ExtensionSorting::CreateDefaultOrdinals() {
531 if (default_ordinals_created_)
533 default_ordinals_created_ = true;
535 // The following defines the default order of apps.
536 #if defined(OS_CHROMEOS)
537 std::vector<std::string> app_ids;
538 chromeos::default_app_order::Get(&app_ids);
540 const char* kDefaultAppOrder[] = {
541 extension_misc::kChromeAppId,
542 extension_misc::kWebStoreAppId,
544 const std::vector<const char*> app_ids(
545 kDefaultAppOrder, kDefaultAppOrder + arraysize(kDefaultAppOrder));
548 syncer::StringOrdinal page_ordinal = CreateFirstAppPageOrdinal();
549 syncer::StringOrdinal app_launch_ordinal =
550 CreateFirstAppLaunchOrdinal(page_ordinal);
551 for (size_t i = 0; i < app_ids.size(); ++i) {
552 const std::string extension_id = app_ids[i];
553 default_ordinals_[extension_id].page_ordinal = page_ordinal;
554 default_ordinals_[extension_id].app_launch_ordinal = app_launch_ordinal;
555 app_launch_ordinal = app_launch_ordinal.CreateAfter();
559 bool ExtensionSorting::GetDefaultOrdinals(
560 const std::string& extension_id,
561 syncer::StringOrdinal* page_ordinal,
562 syncer::StringOrdinal* app_launch_ordinal) {
563 CreateDefaultOrdinals();
564 AppOrdinalsMap::const_iterator it = default_ordinals_.find(extension_id);
565 if (it == default_ordinals_.end())
569 *page_ordinal = it->second.page_ordinal;
570 if (app_launch_ordinal)
571 *app_launch_ordinal = it->second.app_launch_ordinal;
575 syncer::StringOrdinal ExtensionSorting::ResolveCollision(
576 const syncer::StringOrdinal& page_ordinal,
577 const syncer::StringOrdinal& app_launch_ordinal) const {
578 DCHECK(page_ordinal.IsValid() && app_launch_ordinal.IsValid());
580 PageOrdinalMap::const_iterator page_it = ntp_ordinal_map_.find(page_ordinal);
581 if (page_it == ntp_ordinal_map_.end())
582 return app_launch_ordinal;
584 const AppLaunchOrdinalMap& page = page_it->second;
585 AppLaunchOrdinalMap::const_iterator app_it = page.find(app_launch_ordinal);
586 if (app_it == page.end())
587 return app_launch_ordinal;
589 // Finds the next app launcher ordinal. This is done by the following loop
590 // because this function could be called before FixNTPOrdinalCollisions and
591 // thus |page| might contains multiple entries with the same app launch
592 // ordinal. See http://crbug.com/155603
593 while (app_it != page.end() && app_launch_ordinal.Equals(app_it->first))
596 // If there is no next after the collision, returns the next ordinal.
597 if (app_it == page.end())
598 return app_launch_ordinal.CreateAfter();
600 // Otherwise, returns the ordinal between the collision and the next ordinal.
601 return app_launch_ordinal.CreateBetween(app_it->first);
604 size_t ExtensionSorting::CountItemsVisibleOnNtp(
605 const AppLaunchOrdinalMap& m) const {
607 for (AppLaunchOrdinalMap::const_iterator it = m.begin(); it != m.end();
609 const std::string& id = it->second;
610 if (ntp_hidden_extensions_.count(id) == 0)