Index: components/ntp_snippets/category_rankers/click_based_category_ranker.cc |
diff --git a/components/ntp_snippets/category_rankers/click_based_category_ranker.cc b/components/ntp_snippets/category_rankers/click_based_category_ranker.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..45da64bd5bdf2a0c55db471f604eec4f1cef2e4f |
--- /dev/null |
+++ b/components/ntp_snippets/category_rankers/click_based_category_ranker.cc |
@@ -0,0 +1,236 @@ |
+// Copyright 2016 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 "components/ntp_snippets/category_rankers/click_based_category_ranker.h" |
+ |
+#include <algorithm> |
+ |
+#include "base/memory/ptr_util.h" |
+#include "base/values.h" |
+#include "components/ntp_snippets/category_rankers/constant_category_ranker.h" |
+#include "components/ntp_snippets/pref_names.h" |
+#include "components/prefs/pref_registry_simple.h" |
+#include "components/prefs/pref_service.h" |
+ |
+namespace ntp_snippets { |
+ |
+namespace { |
+ |
+// In order to increase stability and predictability of the order, an extra |
+// level of "confidence" is required before moving a category upwards. In other |
+// words, the category is moved not when it reaches the previous one, but rather |
+// when it leads by some amount. We refer to this required extra "confidence" as |
+// a passing margin. Each position has its own passing margin. The category is |
+// moved upwards (i.e. passes another category) when it has at least passing |
+// margin of the previous category position more clicks. |
+const int kPassingMargin = 5; |
+ |
+// The first categories get more attention and, therefore, here more stability |
+// is needed. The passing margin of such categories is increased and they are |
+// referred to as top categories (with extra margin). Only category position |
+// defines whether a category is top, but not its content. |
+const int kNumTopCategoriesWithExtraMargin = 3; |
+ |
+// The increase of passing margin for each top category compared to the next |
+// category (e.g. the first top category has passing margin larger by this value |
+// than the second top category, the last top category has it larger by this |
+// value than the first non-top category). |
+const int kExtraPassingMargin = 2; |
+ |
+const char kCategoryIdKey[] = "category"; |
+const char kClicksKey[] = "clicks"; |
+ |
+} // namespace |
+ |
+ClickBasedCategoryRanker::ClickBasedCategoryRanker(PrefService* pref_service) |
+ : pref_service_(pref_service) { |
+ if (!ReadOrderFromPrefs(&ordered_categories_)) { |
+ // TODO(crbug.com/676273): Handle adding new hardcoded KnownCategories to |
+ // existing order from prefs. Currently such new category is completely |
+ // ignored and may be never shown. |
+ RestoreDefaultOrder(); |
+ } |
+} |
+ |
+ClickBasedCategoryRanker::~ClickBasedCategoryRanker() = default; |
+ |
+bool ClickBasedCategoryRanker::Compare(Category left, Category right) const { |
+ if (!ContainsCategory(left)) { |
+ LOG(DFATAL) << "The category with ID " << left.id() |
+ << " has not been added using AppendCategoryIfNecessary."; |
+ } |
+ if (!ContainsCategory(right)) { |
+ LOG(DFATAL) << "The category with ID " << right.id() |
+ << " has not been added using AppendCategoryIfNecessary."; |
+ } |
+ if (left == right) { |
+ return false; |
+ } |
+ for (const RankedCategory& ranked_category : ordered_categories_) { |
+ if (ranked_category.category == left) { |
+ return true; |
+ } |
+ if (ranked_category.category == right) { |
+ return false; |
+ } |
+ } |
+ // This fallback is provided only to satisfy "Compare" contract if by mistake |
+ // categories are not added using AppendCategoryIfNecessary. One should not |
+ // rely on this, instead the order must be defined explicitly using |
+ // AppendCategoryIfNecessary. |
+ return left.id() < right.id(); |
+} |
+ |
+void ClickBasedCategoryRanker::ClearHistory(base::Time begin, base::Time end) { |
+ // TODO(crbug.com/675953): Preserve remote categories with 0 counts instead of |
+ // removing them. |
+ RestoreDefaultOrder(); |
+} |
+ |
+void ClickBasedCategoryRanker::AppendCategoryIfNecessary(Category category) { |
+ if (!ContainsCategory(category)) { |
+ ordered_categories_.push_back(RankedCategory(category, /*clicks=*/0)); |
+ } |
+} |
+ |
+// TODO(vitaliii): Listen to dismissed sections and penalise them. |
+void ClickBasedCategoryRanker::OnSuggestionOpened(Category category) { |
+ if (!ContainsCategory(category)) { |
+ LOG(DFATAL) << "The category with ID " << category.id() |
+ << " has not been added using AppendCategoryIfNecessary."; |
+ return; |
+ } |
+ |
+ std::vector<RankedCategory>::iterator current = FindCategory(category); |
+ current->clicks++; |
+ |
+ // TODO(crbug.com/676279): Prevent overflow. |
+ if (current != ordered_categories_.begin()) { |
+ std::vector<RankedCategory>::iterator previous = current - 1; |
+ const int passing_margin = GetPositionPassingMargin(previous); |
+ if (current->clicks >= previous->clicks + passing_margin) { |
+ // It is intended to move only by one position at a time in order to avoid |
+ // dramatic changes, which could confuse the user. |
+ std::swap(*current, *previous); |
+ } |
+ } |
+ |
+ StoreOrderToPrefs(ordered_categories_); |
+} |
+ |
+// static |
+void ClickBasedCategoryRanker::RegisterProfilePrefs( |
+ PrefRegistrySimple* registry) { |
+ registry->RegisterListPref(prefs::kClickBasedCategoryRankerOrderWithClicks); |
+} |
+ |
+// static |
+int ClickBasedCategoryRanker::GetPassingMargin() { |
+ return kPassingMargin; |
+} |
+ |
+// static |
+int ClickBasedCategoryRanker::GetNumTopCategoriesWithExtraMargin() { |
+ return kNumTopCategoriesWithExtraMargin; |
+} |
+ |
+ClickBasedCategoryRanker::RankedCategory::RankedCategory(Category category, |
+ int clicks) |
+ : category(category), clicks(clicks) {} |
+ |
+// Returns passing margin for a given position taking into account whether it is |
+// a top category. |
+int ClickBasedCategoryRanker::GetPositionPassingMargin( |
+ std::vector<RankedCategory>::const_iterator category_position) const { |
+ int index = category_position - ordered_categories_.cbegin(); |
+ int passing_margin_increase = 0; |
+ if (index < kNumTopCategoriesWithExtraMargin) { |
+ passing_margin_increase = |
+ kExtraPassingMargin * (kNumTopCategoriesWithExtraMargin - index); |
+ } |
+ return kPassingMargin + passing_margin_increase; |
+} |
+ |
+void ClickBasedCategoryRanker::RestoreDefaultOrder() { |
+ ordered_categories_.clear(); |
+ |
+ std::vector<KnownCategories> ordered_known_categories = |
+ ConstantCategoryRanker::GetKnownCategoriesDefaultOrder(); |
+ |
+ for (KnownCategories known_category : ordered_known_categories) { |
+ AppendKnownCategory(known_category); |
+ } |
+ |
+ StoreOrderToPrefs(ordered_categories_); |
+} |
+ |
+void ClickBasedCategoryRanker::AppendKnownCategory( |
+ KnownCategories known_category) { |
+ Category category = Category::FromKnownCategory(known_category); |
+ DCHECK(!ContainsCategory(category)); |
+ ordered_categories_.push_back(RankedCategory(category, /*clicks=*/0)); |
+} |
+ |
+bool ClickBasedCategoryRanker::ReadOrderFromPrefs( |
+ std::vector<RankedCategory>* result_categories) { |
+ result_categories->clear(); |
+ const base::ListValue* list = |
+ pref_service_->GetList(prefs::kClickBasedCategoryRankerOrderWithClicks); |
+ if (!list || list->GetSize() == 0) { |
+ return false; |
+ } |
+ |
+ for (const std::unique_ptr<base::Value>& value : *list) { |
+ base::DictionaryValue* dictionary; |
+ if (!value->GetAsDictionary(&dictionary)) { |
+ LOG(DFATAL) << "Failed to parse category data from prefs param " |
+ << prefs::kClickBasedCategoryRankerOrderWithClicks |
+ << " into dictionary."; |
+ return false; |
+ } |
+ int category_id, clicks; |
+ if (!dictionary->GetInteger(kCategoryIdKey, &category_id)) { |
+ LOG(DFATAL) << "Dictionary does not have '" << kCategoryIdKey << "' key."; |
+ return false; |
+ } |
+ if (!dictionary->GetInteger(kClicksKey, &clicks)) { |
+ LOG(DFATAL) << "Dictionary does not have '" << kClicksKey << "' key."; |
+ return false; |
+ } |
+ Category category = Category::FromIDValue(category_id); |
+ result_categories->push_back(RankedCategory(category, clicks)); |
+ } |
+ return true; |
+} |
+ |
+void ClickBasedCategoryRanker::StoreOrderToPrefs( |
+ const std::vector<RankedCategory>& ordered_categories) { |
+ base::ListValue list; |
+ for (const RankedCategory& category : ordered_categories) { |
+ auto dictionary = base::MakeUnique<base::DictionaryValue>(); |
+ dictionary->SetInteger(kCategoryIdKey, category.category.id()); |
+ dictionary->SetInteger(kClicksKey, category.clicks); |
+ list.Append(std::move(dictionary)); |
+ } |
+ pref_service_->Set(prefs::kClickBasedCategoryRankerOrderWithClicks, list); |
+} |
+ |
+std::vector<ClickBasedCategoryRanker::RankedCategory>::iterator |
+ClickBasedCategoryRanker::FindCategory(Category category) { |
+ return std::find_if(ordered_categories_.begin(), ordered_categories_.end(), |
+ [category](const RankedCategory& ranked_category) { |
+ return category == ranked_category.category; |
+ }); |
+} |
+ |
+bool ClickBasedCategoryRanker::ContainsCategory(Category category) const { |
+ for (const auto& ranked_category : ordered_categories_) { |
+ if (category == ranked_category.category) { |
+ return true; |
+ } |
+ } |
+ return false; |
+} |
+ |
+} // namespace ntp_snippets |