Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(146)

Side by Side Diff: components/ntp_snippets/sessions/foreign_sessions_suggestions_provider.cc

Issue 2279123002: [Sync] Initial implementation of foreign sessions suggestions provider. (Closed)
Patch Set: Adding sessions deps to BUILD.gn Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // Copyright 2016 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 "components/ntp_snippets/sessions/foreign_sessions_suggestions_provider .h"
6
7 #include <algorithm>
8 #include <map>
9 #include <tuple>
10 #include <utility>
11
12 #include "base/strings/string_piece.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/time/time.h"
15 #include "components/ntp_snippets/category_factory.h"
16 #include "components/ntp_snippets/category_info.h"
17 #include "components/ntp_snippets/content_suggestion.h"
18 #include "components/ntp_snippets/features.h"
19 #include "components/ntp_snippets/pref_names.h"
20 #include "components/ntp_snippets/pref_util.h"
21 #include "components/prefs/pref_registry_simple.h"
22 #include "components/prefs/pref_service.h"
23 #include "components/sessions/core/session_types.h"
24 #include "components/sync_sessions/open_tabs_ui_delegate.h"
25 #include "components/sync_sessions/synced_session.h"
26 #include "grit/components_strings.h"
27 #include "ui/base/l10n/l10n_util.h"
28 #include "ui/gfx/image/image.h"
29 #include "url/gurl.h"
30
31 using base::TimeDelta;
32 using sessions::SerializedNavigationEntry;
33 using sessions::SessionTab;
34 using sessions::SessionWindow;
35 using sync_driver::SyncedSession;
36
37 namespace ntp_snippets {
38 namespace {
39
40 const int kMaxForeignTabsTotal = 10;
41 const int kMaxForeignTabsPerDevice = 3;
42 const int kMaxForeignTabAgeInMinutes = 180;
43
44 const char* kMaxForeignTabsTotalParamName = "max_foreign_tabs_total";
45 const char* kMaxForeignTabsPerDeviceParamName = "max_foreign_tabs_per_device";
46 const char* kMaxForeignTabAgeInMinutesParamName =
47 "max_foreign_tabs_age_in_minutes";
48
49 int GetMaxForeignTabsTotal() {
50 return GetParamAsInt(ntp_snippets::kForeignSessionsSuggestionsFeature,
51 kMaxForeignTabsTotalParamName, kMaxForeignTabsTotal);
52 }
53
54 int GetMaxForeignTabsPerDevice() {
55 return GetParamAsInt(ntp_snippets::kForeignSessionsSuggestionsFeature,
56 kMaxForeignTabsPerDeviceParamName,
57 kMaxForeignTabsPerDevice);
58 }
59
60 TimeDelta GetMaxForeignTabAge() {
61 return TimeDelta::FromMinutes(GetParamAsInt(
62 ntp_snippets::kForeignSessionsSuggestionsFeature,
63 kMaxForeignTabAgeInMinutesParamName, kMaxForeignTabAgeInMinutes));
64 }
65
66 } // namespace
67
68 ForeignSessionsSuggestionsProvider::ForeignSessionsSuggestionsProvider(
69 ContentSuggestionsProvider::Observer* observer,
70 CategoryFactory* category_factory,
71 std::unique_ptr<OpenTabsUIDelegateProvider> delegate_provider,
72 PrefService* pref_service)
73 : ContentSuggestionsProvider(observer, category_factory),
74 category_status_(CategoryStatus::INITIALIZING),
75 provided_category_(
76 category_factory->FromKnownCategory(KnownCategories::FOREIGN_TABS)),
77 delegate_provider_(std::move(delegate_provider)),
78 pref_service_(pref_service) {
79 delegate_provider_->SubscribeForForeignTabChange(
80 base::Bind(&ForeignSessionsSuggestionsProvider::OnForeignTabChange,
81 base::Unretained(this)));
82
83 // If sync is already initialzed, try suggesting now, though this is unlikely.
84 OnForeignTabChange(delegate_provider_->GetOpenTabsUIDelegate());
85 }
86
87 ForeignSessionsSuggestionsProvider::~ForeignSessionsSuggestionsProvider() {}
88
89 // static
90 void ForeignSessionsSuggestionsProvider::RegisterProfilePrefs(
91 PrefRegistrySimple* registry) {
92 registry->RegisterListPref(prefs::kDismissedForeignSessionsSuggestions);
93 }
94
95 CategoryStatus ForeignSessionsSuggestionsProvider::GetCategoryStatus(
96 Category category) {
97 DCHECK_EQ(category, provided_category_);
98 return category_status_;
99 }
100
101 CategoryInfo ForeignSessionsSuggestionsProvider::GetCategoryInfo(
102 Category category) {
103 DCHECK_EQ(category, provided_category_);
104 return CategoryInfo(l10n_util::GetStringUTF16(
105 IDS_NTP_FOREIGN_SESSIONS_SUGGESTIONS_SECTION_HEADER),
106 ContentSuggestionsCardLayout::MINIMAL_CARD,
107 /* has_more_button */ true,
108 /* show_if_empty */ false);
109 }
110
111 void ForeignSessionsSuggestionsProvider::DismissSuggestion(
112 const std::string& suggestion_id) {
113 // TODO(skym): Right now this continuously grows, without clearing out old and
114 // irrelevant entries. Could either use a timestamp and expire after a
115 // threshold, or compare with current foreign tabs and remove anything that
116 // isn't actively blockign a foreign_sessions tab.
117 std::set<std::string> dismissed_ids = prefs::ReadDismissedIDsFromPrefs(
118 *pref_service_, prefs::kDismissedForeignSessionsSuggestions);
119 dismissed_ids.insert(suggestion_id);
120 prefs::StoreDismissedIDsToPrefs(pref_service_,
121 prefs::kDismissedForeignSessionsSuggestions,
122 dismissed_ids);
123 }
124
125 void ForeignSessionsSuggestionsProvider::FetchSuggestionImage(
126 const std::string& suggestion_id,
127 const ImageFetchedCallback& callback) {
128 base::ThreadTaskRunnerHandle::Get()->PostTask(
129 FROM_HERE, base::Bind(callback, gfx::Image()));
130 }
131
132 void ForeignSessionsSuggestionsProvider::ClearHistory(
133 base::Time begin,
134 base::Time end,
135 const base::Callback<bool(const GURL& url)>& filter) {
136 std::set<std::string> dismissed_ids = prefs::ReadDismissedIDsFromPrefs(
137 *pref_service_, prefs::kDismissedForeignSessionsSuggestions);
138 for (auto iter = dismissed_ids.begin(); iter != dismissed_ids.end();) {
139 if (filter.Run(GURL(base::StringPiece(*iter)))) {
140 iter = dismissed_ids.erase(iter);
141 } else {
142 ++iter;
143 }
144 }
145 prefs::StoreDismissedIDsToPrefs(pref_service_,
146 prefs::kDismissedForeignSessionsSuggestions,
147 dismissed_ids);
148 }
149
150 void ForeignSessionsSuggestionsProvider::ClearCachedSuggestions(
151 Category category) {
152 DCHECK_EQ(category, provided_category_);
153 // Ignored.
154 }
155
156 void ForeignSessionsSuggestionsProvider::GetDismissedSuggestionsForDebugging(
157 Category category,
158 const DismissedSuggestionsCallback& callback) {
159 DCHECK_EQ(category, provided_category_);
160 callback.Run(std::vector<ContentSuggestion>());
161 }
162
163 void ForeignSessionsSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
164 Category category) {
165 DCHECK_EQ(category, provided_category_);
166 pref_service_->ClearPref(prefs::kDismissedForeignSessionsSuggestions);
167 }
168
169 void ForeignSessionsSuggestionsProvider::OnForeignTabChange(
170 sync_driver::OpenTabsUIDelegate* open_tabs_ui_delegate) {
Marc Treib 2016/09/16 12:26:48 Is it necessary to pass in the delegate? Can't we
skym 2016/09/16 18:18:48 Done.
171 if (open_tabs_ui_delegate) {
tschumann 2016/09/16 13:33:20 nit: i'd invert this condition and exit early inst
skym 2016/09/16 18:18:48 Done.
172 if (category_status_ != CategoryStatus::AVAILABLE) {
173 // The further below logic will overwrite any error state. This is
174 // currently okay because no where in the current implementation does the
175 // status get set to an error state. Should this change, reconsider the
176 // overwriting logic.
177 DCHECK(category_status_ == CategoryStatus::INITIALIZING ||
178 category_status_ == CategoryStatus::NOT_PROVIDED);
179
180 // It is difficult to tell if sync simply has not initialized yet or there
181 // will never be data because the user is signed out or has disabled the
182 // sessions data type. Because this provider is hidden when there are no
183 // results, always just update to AVAILABLE once we might have results.
184 category_status_ = CategoryStatus::AVAILABLE;
185 observer()->OnCategoryStatusChanged(this, provided_category_,
186 category_status_);
187 }
188
189 std::vector<const SyncedSession*> foreign_sessions;
190 open_tabs_ui_delegate->GetAllForeignSessions(&foreign_sessions);
191 // observer()->OnNewSuggestions must be called even when we have no
192 // suggestions to remove previous suggestions that are now filtered out.
193 observer()->OnNewSuggestions(this, provided_category_,
194 BuildSuggestions(foreign_sessions));
195 } else if (category_status_ == CategoryStatus::AVAILABLE) {
196 // This is to handle the case where the user disabled sync [sessions] or
197 // logs out after we've already provided actual suggestions.
198 category_status_ = CategoryStatus::NOT_PROVIDED;
199 observer()->OnCategoryStatusChanged(this, provided_category_,
200 category_status_);
201 }
202 }
203
204 std::vector<ContentSuggestion>
205 ForeignSessionsSuggestionsProvider::BuildSuggestions(
206 const std::vector<const SyncedSession*>& foreign_sessions) {
207 // TODO(skym): If a tab was previously dismissed, but was since updated,
208 // should it be resurrected and removed from the dismissed list? This would
209 // likely require a change to the dismissed ids.
210 // TODO(skym): No sense in keeping around dismissals for urls that no longer
211 // exist on any current foreign devices. Should prune and save the pref back.
212 const TimeDelta max_foreign_tab_age = GetMaxForeignTabAge();
213 const int max_foreign_tabs_total = GetMaxForeignTabsTotal();
214 const int max_foreign_tabs_per_device = GetMaxForeignTabsPerDevice();
215 using SessionTuple = std::tuple<const SyncedSession*, const SessionTab*,
216 const SerializedNavigationEntry*>;
217 std::vector<SessionTuple> suggestion_candidates;
218 std::set<std::string> dismissed_ids = prefs::ReadDismissedIDsFromPrefs(
219 *pref_service_, prefs::kDismissedForeignSessionsSuggestions);
220 for (const SyncedSession* session : foreign_sessions) {
221 for (const std::pair<const SessionID::id_type, SessionWindow*>& key_value :
222 session->windows) {
223 for (const SessionTab* tab : key_value.second->tabs) {
224 if (tab->navigations.empty())
225 continue;
226
227 const SerializedNavigationEntry& navigation = tab->navigations.back();
228 const std::string unique_id =
229 MakeUniqueID(provided_category_, navigation.virtual_url().spec());
tschumann 2016/09/16 13:33:20 this line confused me at first a bit, as we don't
skym 2016/09/16 18:18:48 Done. I was hesitant to initially break up this fu
tschumann 2016/09/17 15:52:56 Thanks!
230 // TODO(skym): Filter out internal pages. Tabs that contain only
231 // non-syncable content should never reach the local client, but
232 // sometimes the most recent navigation may be internal while one
233 // of the previous ones was more valid.
234 if (dismissed_ids.find(unique_id) == dismissed_ids.end() &&
235 (base::Time::Now() - tab->timestamp) < max_foreign_tab_age) {
236 suggestion_candidates.emplace_back(session, tab, &navigation);
237 }
238 }
239 }
240 }
241
242 // Sort by recency. Note that SerializedNavigationEntry::timestamp() is
tschumann 2016/09/16 13:33:20 nit: rephrase to better reflect why we sort? like
skym 2016/09/16 18:18:48 Done.
243 // never set to a value, so use SessionTab::timestamp() instead.
244 // TODO(skym): It might be better if we sorted by recency of session, and only
245 // then by recency of the tab. Right now a single device's tabs can be
246 // interleaved with another device's tabs.
247 std::sort(suggestion_candidates.begin(), suggestion_candidates.end(),
248 [](const SessionTuple& a, const SessionTuple& b) -> bool {
249 return std::get<1>(a)->timestamp > std::get<1>(b)->timestamp;
250 });
251 std::vector<ContentSuggestion> suggestions;
252 std::set<std::string> duplicate_urls;
tschumann 2016/09/16 13:33:20 nano nit: this variable is technically not collect
skym 2016/09/16 18:18:48 Done.
253 std::map<std::string, int> suggestions_per_session;
254 for (const SessionTuple& tuple : suggestion_candidates) {
255 const SyncedSession& session = *std::get<0>(tuple);
256 const SessionTab& tab = *std::get<1>(tuple);
257 const SerializedNavigationEntry& navigation = *std::get<2>(tuple);
258
259 auto duplicates_iter = duplicate_urls.find(navigation.virtual_url().spec());
260 auto count_iter = suggestions_per_session.find(session.session_tag);
261 int count =
262 count_iter == suggestions_per_session.end() ? 0 : count_iter->second;
263
264 // Pick up to max (total and per device) tabs, and ensure no duplicates
265 // are selected. This filtering must be done in a second pass because
266 // this can cause newer tabs occluding less recent tabs, requiring more
267 // than |max_foreign_tabs_per_device| to be considered per device.
268 if (static_cast<int>(suggestions.size()) >= max_foreign_tabs_total ||
269 duplicates_iter != duplicate_urls.end() ||
270 count >= max_foreign_tabs_per_device) {
271 continue;
272 }
273 duplicate_urls.insert(navigation.virtual_url().spec());
274 suggestions_per_session[session.session_tag] = count + 1;
275 suggestions.push_back(BuildSuggestion(session, tab, navigation));
276 }
277
278 return suggestions;
279 }
280
281 ContentSuggestion ForeignSessionsSuggestionsProvider::BuildSuggestion(
282 const SyncedSession& session,
283 const SessionTab& tab,
284 const SerializedNavigationEntry& navigation) {
285 ContentSuggestion suggestion(
286 MakeUniqueID(provided_category_, navigation.virtual_url().spec()),
287 navigation.virtual_url());
288 suggestion.set_title(navigation.title());
289 suggestion.set_publish_date(tab.timestamp);
290 suggestion.set_publisher_name(base::UTF8ToUTF16(
291 navigation.virtual_url().host() + " - " + session.session_name));
Marc Treib 2016/09/16 12:26:48 This should probably be a string resource with two
skym 2016/09/16 18:18:48 I'll throw in a big comment here. The favicon is o
Marc Treib 2016/09/19 09:24:08 Acknowledged.
292 return suggestion;
293 }
294
295 } // namespace ntp_snippets
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698