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

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: Remove foreign sessions suggestions when user disabled session data syncing. 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 <memory>
10 #include <tuple>
11 #include <utility>
12
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/prefs/pref_registry_simple.h"
21 #include "components/prefs/pref_service.h"
22 #include "components/sessions/core/session_types.h"
23 #include "components/sync_sessions/synced_session.h"
24 #include "grit/components_strings.h"
25 #include "ui/base/l10n/l10n_util.h"
26 #include "ui/gfx/image/image.h"
27 #include "url/gurl.h"
28
29 using base::TimeDelta;
30 using sessions::SerializedNavigationEntry;
31 using sessions::SessionTab;
32 using sync_driver::SyncedSession;
33
34 namespace ntp_snippets {
35 namespace {
36
37 const int kMaxForeignTabsTotal = 10;
38 const int kMaxForeignTabsPerDevice = 3;
39 const int kMaxForeignTabAgeInMinutes = 60;
40
41 const char* kMaxForeignTabsTotalParamName = "max_foreign_tabs_total";
42 const char* kMaxForeignTabsPerDeviceParamName = "max_tabs_per_device";
Marc Treib 2016/08/29 09:18:51 max_foreign_tabs_per_device, for consistency?
skym 2016/09/15 23:18:17 Done.
43 const char* kMaxForeignTabAgeInMinutesParamName =
44 "max_foreign_tabs_age_in_minutes";
45
46 int GetMaxForeignTabsTotal() {
47 return GetParamAsInt(ntp_snippets::kForeignSessionsSuggestionsFeature,
48 kMaxForeignTabsTotalParamName, kMaxForeignTabsTotal);
49 }
50
51 int GetMaxForeignTabsPerDevice() {
52 return GetParamAsInt(ntp_snippets::kForeignSessionsSuggestionsFeature,
53 kMaxForeignTabsPerDeviceParamName,
54 kMaxForeignTabsPerDevice);
55 }
56
57 TimeDelta GetMaxForeignTabAge() {
58 return TimeDelta::FromMinutes(GetParamAsInt(
59 ntp_snippets::kForeignSessionsSuggestionsFeature,
60 kMaxForeignTabAgeInMinutesParamName, kMaxForeignTabAgeInMinutes));
61 }
62
63 } // namespace
64
65 ForeignSessionsSuggestionsProvider::ForeignSessionsSuggestionsProvider(
66 ContentSuggestionsProvider::Observer* observer,
67 CategoryFactory* category_factory,
68 sync_driver::SyncService* sync_service,
69 PrefService* pref_service)
70 : ContentSuggestionsProvider(observer, category_factory),
71 category_status_(CategoryStatus::INITIALIZING),
72 provided_category_(
73 category_factory->FromKnownCategory(KnownCategories::FOREIGN_TABS)),
74 sync_service_(sync_service),
tschumann 2016/08/29 09:39:15 nit: I wonder if ForeignSessionsSuggestionsProvide
skym 2016/09/15 23:18:17 Done. Tried to make it DI as well, got a little aw
75 pref_service_(pref_service),
76 dismissed_ids_(prefs::ReadDismissedIDsFromPrefs(
77 prefs::kDismissedForeignSessionsSuggestions,
78 pref_service)) {
79 sync_service_->AddObserver(this);
80 // If sync is already initialzed, try suggesting now, though this is unlikely.
81 TrySuggest();
82 }
83
84 ForeignSessionsSuggestionsProvider::~ForeignSessionsSuggestionsProvider() {
85 sync_service_->RemoveObserver(this);
86 }
87
88 // static
89 void ForeignSessionsSuggestionsProvider::RegisterProfilePrefs(
90 PrefRegistrySimple* registry) {
91 registry->RegisterListPref(prefs::kDismissedForeignSessionsSuggestions);
92 }
93
94 CategoryStatus ForeignSessionsSuggestionsProvider::GetCategoryStatus(
95 Category category) {
96 DCHECK_EQ(category, provided_category_);
97 return category_status_;
98 }
99
100 CategoryInfo ForeignSessionsSuggestionsProvider::GetCategoryInfo(
101 Category category) {
102 DCHECK_EQ(category, provided_category_);
103 return CategoryInfo(l10n_util::GetStringUTF16(
104 IDS_NTP_FOREIGN_SESSIONS_SUGGESTIONS_SECTION_HEADER),
105 ContentSuggestionsCardLayout::MINIMAL_CARD,
106 /* has_more_button */ true,
107 /* show_if_empty */ false);
108 }
109
110 void ForeignSessionsSuggestionsProvider::DismissSuggestion(
111 const std::string& suggestion_id) {
112 dismissed_ids_.insert(suggestion_id);
Marc Treib 2016/08/29 09:18:51 Right now, this can only ever grow. We need some w
battre 2016/08/29 09:48:26 +1 - I am a bit concerned that the preferences fil
Marc Treib 2016/08/29 09:56:38 Right now, this is behind a disabled-by-default fl
skym 2016/09/15 23:18:17 Done.
113 prefs::StoreDismissedIDsToPrefs(prefs::kDismissedForeignSessionsSuggestions,
114 dismissed_ids_, pref_service_);
115 }
116
117 void ForeignSessionsSuggestionsProvider::FetchSuggestionImage(
118 const std::string& suggestion_id,
119 const ImageFetchedCallback& callback) {
120 base::ThreadTaskRunnerHandle::Get()->PostTask(
121 FROM_HERE, base::Bind(callback, gfx::Image()));
122 }
123
124 void ForeignSessionsSuggestionsProvider::ClearCachedSuggestions(
125 Category category) {
126 DCHECK_EQ(category, provided_category_);
127 // Ignored.
128 }
129
130 void ForeignSessionsSuggestionsProvider::GetDismissedSuggestionsForDebugging(
131 Category category,
132 const DismissedSuggestionsCallback& callback) {
133 DCHECK_EQ(category, provided_category_);
134 callback.Run(std::vector<ContentSuggestion>());
135 }
136
137 void ForeignSessionsSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
138 Category category) {
139 DCHECK_EQ(category, provided_category_);
140 pref_service_->ClearPref(prefs::kDismissedForeignSessionsSuggestions);
141 }
142
143 void ForeignSessionsSuggestionsProvider::OnStateChanged() {
144 // Ignored.
145 }
146
147 void ForeignSessionsSuggestionsProvider::OnSyncConfigurationCompleted() {
148 TrySuggest();
149 }
150
151 void ForeignSessionsSuggestionsProvider::OnForeignSessionUpdated() {
152 TrySuggest();
153 }
154
155 void ForeignSessionsSuggestionsProvider::TrySuggest() {
156 sync_driver::OpenTabsUIDelegate* open_tabs_ui_delegate =
157 sync_service_->GetOpenTabsUIDelegate();
158 if (open_tabs_ui_delegate) {
159 if (category_status_ != CategoryStatus::AVAILABLE) {
160 // It is difficult to tell if sync simply has not initialized yet or there
Marc Treib 2016/08/29 09:18:51 Here, category_status_ can only be INITIALIZING or
skym 2016/09/15 23:18:17 Done.
161 // will never be data because the user is signed out or has disabled the
162 // sessions data type. Because this provider is hidden when there are no
163 // results, always just update to AVAILABLE once we might have results.
164 category_status_ = CategoryStatus::AVAILABLE;
165 observer()->OnCategoryStatusChanged(this, provided_category_,
166 category_status_);
167 }
168
169 std::vector<const SyncedSession*> foreign_sessions;
170 if (open_tabs_ui_delegate->GetAllForeignSessions(&foreign_sessions)) {
171 // observer()->OnNewSuggestions must be called even when we have no
172 // suggestions to remove previous suggestions that are now filtered out.
Marc Treib 2016/08/29 09:18:51 ...so the "if" this is in shouldn't be there?
skym 2016/09/15 23:18:18 Whooops, you are correct! Great catch.
173 observer()->OnNewSuggestions(this, provided_category_,
174 BuildSuggestions(foreign_sessions));
175 }
176 } else if (category_status_ == CategoryStatus::AVAILABLE) {
177 // This is to handle the case where the user disabled sync [sessions] or
178 // logs out after we've already provided actual suggestions.
179 category_status_ = CategoryStatus::NOT_PROVIDED;
180 observer()->OnCategoryStatusChanged(this, provided_category_,
181 category_status_);
182 }
183 }
184
185 std::vector<ContentSuggestion>
186 ForeignSessionsSuggestionsProvider::BuildSuggestions(
187 const std::vector<const SyncedSession*>& foreign_sessions) {
188 // TODO(skym): If a tab was previously dismissed, but was since updated,
189 // should it be resurrected and removed from the dismissed list? This would
190 // likely require a change to the dismissed ids.
191 const TimeDelta max_foreign_tab_age = GetMaxForeignTabAge();
192 const int max_foreign_tabs_total = GetMaxForeignTabsTotal();
193 const int max_foreign_tabs_per_device = GetMaxForeignTabsPerDevice();
194 using SessionTuple = std::tuple<const SyncedSession*, const SessionTab*,
195 const SerializedNavigationEntry*>;
196 std::vector<SessionTuple> suggestion_candidates;
197 for (const SyncedSession* session : foreign_sessions) {
198 for (const std::pair<const SessionID::id_type, sessions::SessionWindow*>&
199 key_value : session->windows) {
200 for (const SessionTab* tab : key_value.second->tabs) {
201 if (tab->navigations.size() > 0) {
Marc Treib 2016/08/29 09:18:51 optional nit: "if (tab->navigations.size() == 0) c
battre 2016/08/29 09:48:26 I think we prefer empty() over size() == 0
skym 2016/09/15 23:18:18 Done.
skym 2016/09/15 23:18:18 Done.
202 const SerializedNavigationEntry& navigation = tab->navigations.back();
203 const std::string unique_id =
204 MakeUniqueID(provided_category_, navigation.virtual_url().spec());
205 // TODO(skym): Filter out internal pages. Tabs that contain only
206 // non-syncable content should never reach the local client, but
207 // sometimes the most recent navigation may be internal while one
208 // of the previous ones was more valid.
209 if (dismissed_ids_.find(unique_id) == dismissed_ids_.end() &&
210 (base::Time::Now() - tab->timestamp) < max_foreign_tab_age) {
211 suggestion_candidates.emplace_back(session, tab, &navigation);
212 }
213 }
214 }
215 }
216 }
217
218 // Sort by recency. Note that SerializedNavigationEntry::timestamp() is
219 // never set to a value, so use SessionTab::timestamp() instead.
220 std::sort(suggestion_candidates.begin(), suggestion_candidates.end(),
221 [](const SessionTuple& a, const SessionTuple& b) -> bool {
222 return std::get<1>(a)->timestamp > std::get<1>(b)->timestamp;
223 });
224 std::vector<ContentSuggestion> suggestions;
225 std::set<std::string> duplicate_urls;
226 std::map<std::string, int> suggestions_per_session;
227 for (const SessionTuple& tuple : suggestion_candidates) {
228 const SyncedSession& session = *std::get<0>(tuple);
229 const SessionTab& tab = *std::get<1>(tuple);
230 const SerializedNavigationEntry& navigation = *std::get<2>(tuple);
231
232 auto duplicates_iter = duplicate_urls.find(navigation.virtual_url().spec());
233 auto count_iter = suggestions_per_session.find(session.session_tag);
234 int count =
235 count_iter == suggestions_per_session.end() ? 0 : count_iter->second;
236
237 // Pick up to max (total and per device) tabs, and ensure no duplcates
Marc Treib 2016/08/29 09:18:51 s/duplcates/duplicates/
skym 2016/09/15 23:18:18 Done.
238 // are selected. This filtering must be done in a second pass because
239 // this can cause newer tabs occluding less recent tabs, requiring more
240 // than |max_foreign_tabs_per_device| to be considered per device.
241 if (static_cast<int>(suggestions.size()) >= max_foreign_tabs_total ||
242 duplicates_iter != duplicate_urls.end() ||
243 count >= max_foreign_tabs_per_device) {
244 continue;
245 }
246 duplicate_urls.insert(navigation.virtual_url().spec());
247 suggestions_per_session[session.session_tag] = count + 1;
248 suggestions.emplace_back(BuildSuggestion(tab, navigation));
Marc Treib 2016/08/29 09:18:51 nit: push_back will do the same thing here
skym 2016/09/15 23:18:18 Done.
249 }
250 return suggestions;
251 }
252
253 ContentSuggestion ForeignSessionsSuggestionsProvider::BuildSuggestion(
254 const SessionTab& tab,
255 const SerializedNavigationEntry& navigation) {
256 // TODO(skym): Ideally we would expose the host device's name, but there
257 // is not currently a convineint way to show this in the UI. If device
Marc Treib 2016/08/29 09:18:51 s/convineint/convenient/
skym 2016/09/15 23:18:18 Done.
258 // name is shown, it may make sense for tabs to be ordered by device
259 // recency and then tab/navigation recency.
Marc Treib 2016/08/29 09:18:51 Some ideas (not thought through): - Have a section
skym 2016/09/15 23:18:17 The line with the publisher already looks like: [
260 ContentSuggestion suggestion(
261 MakeUniqueID(provided_category_, navigation.virtual_url().spec()),
262 navigation.virtual_url());
263 suggestion.set_title(navigation.title());
264 suggestion.set_publish_date(tab.timestamp);
265 suggestion.set_publisher_name(
266 base::UTF8ToUTF16(navigation.virtual_url().host()));
267 return suggestion;
268 }
269
270 } // namespace ntp_snippets
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698