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

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: Explicitly compare raw pointer with nullptr. 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/synced_session.h"
25 #include "grit/components_strings.h"
26 #include "ui/base/l10n/l10n_util.h"
27 #include "ui/gfx/image/image.h"
28 #include "url/gurl.h"
29
30 using base::TimeDelta;
31 using sessions::SerializedNavigationEntry;
32 using sessions::SessionTab;
33 using sessions::SessionWindow;
34 using sync_sessions::SyncedSession;
35
36 namespace ntp_snippets {
37 namespace {
38
39 const int kMaxForeignTabsTotal = 10;
40 const int kMaxForeignTabsPerDevice = 3;
41 const int kMaxForeignTabAgeInMinutes = 180;
42
43 const char* kMaxForeignTabsTotalParamName = "max_foreign_tabs_total";
44 const char* kMaxForeignTabsPerDeviceParamName = "max_foreign_tabs_per_device";
45 const char* kMaxForeignTabAgeInMinutesParamName =
46 "max_foreign_tabs_age_in_minutes";
47
48 int GetMaxForeignTabsTotal() {
49 return GetParamAsInt(ntp_snippets::kForeignSessionsSuggestionsFeature,
50 kMaxForeignTabsTotalParamName, kMaxForeignTabsTotal);
51 }
52
53 int GetMaxForeignTabsPerDevice() {
54 return GetParamAsInt(ntp_snippets::kForeignSessionsSuggestionsFeature,
55 kMaxForeignTabsPerDeviceParamName,
56 kMaxForeignTabsPerDevice);
57 }
58
59 TimeDelta GetMaxForeignTabAge() {
60 return TimeDelta::FromMinutes(GetParamAsInt(
61 ntp_snippets::kForeignSessionsSuggestionsFeature,
62 kMaxForeignTabAgeInMinutesParamName, kMaxForeignTabAgeInMinutes));
63 }
64
65 } // namespace
66
67 ForeignSessionsSuggestionsProvider::ForeignSessionsSuggestionsProvider(
68 ContentSuggestionsProvider::Observer* observer,
69 CategoryFactory* category_factory,
70 std::unique_ptr<ForeignSessionsProvider> foreign_sessions_provider,
71 PrefService* pref_service)
72 : ContentSuggestionsProvider(observer, category_factory),
73 category_status_(CategoryStatus::INITIALIZING),
74 provided_category_(
75 category_factory->FromKnownCategory(KnownCategories::FOREIGN_TABS)),
76 foreign_sessions_provider_(std::move(foreign_sessions_provider)),
77 pref_service_(pref_service) {
78 foreign_sessions_provider_->SubscribeForForeignTabChange(
79 base::Bind(&ForeignSessionsSuggestionsProvider::OnForeignTabChange,
80 base::Unretained(this)));
81
82 // If sync is already initialzed, try suggesting now, though this is unlikely.
83 OnForeignTabChange();
84 }
85
86 ForeignSessionsSuggestionsProvider::~ForeignSessionsSuggestionsProvider() {}
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,
tschumann 2016/09/17 15:52:57 nit: if you follow the pattern /*has_more_button=*
Marc Treib 2016/09/19 09:24:08 Do we have that in Chrome? If so, that should reso
tschumann 2016/09/19 12:19:23 Apparently, you can run it manually -- not sure ho
skym 2016/09/19 18:54:48 Cool, had no idea this was a thing, done. Also upd
107 /* show_if_empty */ false);
108 }
109
110 void ForeignSessionsSuggestionsProvider::DismissSuggestion(
111 const std::string& suggestion_id) {
112 // TODO(skym): Right now this continuously grows, without clearing out old and
113 // irrelevant entries. Could either use a timestamp and expire after a
114 // threshold, or compare with current foreign tabs and remove anything that
115 // isn't actively blockign a foreign_sessions tab.
116 std::set<std::string> dismissed_ids = prefs::ReadDismissedIDsFromPrefs(
117 *pref_service_, prefs::kDismissedForeignSessionsSuggestions);
118 dismissed_ids.insert(suggestion_id);
119 prefs::StoreDismissedIDsToPrefs(pref_service_,
120 prefs::kDismissedForeignSessionsSuggestions,
121 dismissed_ids);
122 }
123
124 void ForeignSessionsSuggestionsProvider::FetchSuggestionImage(
125 const std::string& suggestion_id,
126 const ImageFetchedCallback& callback) {
127 base::ThreadTaskRunnerHandle::Get()->PostTask(
128 FROM_HERE, base::Bind(callback, gfx::Image()));
129 }
130
131 void ForeignSessionsSuggestionsProvider::ClearHistory(
132 base::Time begin,
133 base::Time end,
134 const base::Callback<bool(const GURL& url)>& filter) {
135 std::set<std::string> dismissed_ids = prefs::ReadDismissedIDsFromPrefs(
136 *pref_service_, prefs::kDismissedForeignSessionsSuggestions);
137 for (auto iter = dismissed_ids.begin(); iter != dismissed_ids.end();) {
138 if (filter.Run(GURL(base::StringPiece(*iter)))) {
139 iter = dismissed_ids.erase(iter);
140 } else {
141 ++iter;
142 }
143 }
144 prefs::StoreDismissedIDsToPrefs(pref_service_,
145 prefs::kDismissedForeignSessionsSuggestions,
146 dismissed_ids);
147 }
148
149 void ForeignSessionsSuggestionsProvider::ClearCachedSuggestions(
150 Category category) {
151 DCHECK_EQ(category, provided_category_);
152 // Ignored.
153 }
154
155 void ForeignSessionsSuggestionsProvider::GetDismissedSuggestionsForDebugging(
156 Category category,
157 const DismissedSuggestionsCallback& callback) {
158 DCHECK_EQ(category, provided_category_);
159 callback.Run(std::vector<ContentSuggestion>());
160 }
161
162 void ForeignSessionsSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
163 Category category) {
164 DCHECK_EQ(category, provided_category_);
165 pref_service_->ClearPref(prefs::kDismissedForeignSessionsSuggestions);
166 }
167
168 void ForeignSessionsSuggestionsProvider::OnForeignTabChange() {
169 if (!foreign_sessions_provider_->HasSessionsData()) {
170 if (category_status_ == CategoryStatus::AVAILABLE) {
171 // This is to handle the case where the user disabled sync [sessions] or
172 // logs out after we've already provided actual suggestions.
173 category_status_ = CategoryStatus::NOT_PROVIDED;
174 observer()->OnCategoryStatusChanged(this, provided_category_,
175 category_status_);
176 }
177 return;
178 }
179
180 if (category_status_ != CategoryStatus::AVAILABLE) {
181 // The further below logic will overwrite any error state. This is
182 // currently okay because no where in the current implementation does the
183 // status get set to an error state. Should this change, reconsider the
184 // overwriting logic.
185 DCHECK(category_status_ == CategoryStatus::INITIALIZING ||
186 category_status_ == CategoryStatus::NOT_PROVIDED);
187
188 // It is difficult to tell if sync simply has not initialized yet or there
189 // will never be data because the user is signed out or has disabled the
190 // sessions data type. Because this provider is hidden when there are no
191 // results, always just update to AVAILABLE once we might have results.
192 category_status_ = CategoryStatus::AVAILABLE;
193 observer()->OnCategoryStatusChanged(this, provided_category_,
194 category_status_);
195 }
196
197 // observer()->OnNewSuggestions must be called even when we have no
198 // suggestions to remove previous suggestions that are now filtered out.
199 observer()->OnNewSuggestions(
200 this, provided_category_,
201 BuildSuggestions(foreign_sessions_provider_->GetAllForeignSessions()));
202 }
203
204 std::vector<ContentSuggestion>
205 ForeignSessionsSuggestionsProvider::BuildSuggestions(
206 const std::vector<const SyncedSession*>& foreign_sessions) {
207 const int max_foreign_tabs_total = GetMaxForeignTabsTotal();
208 const int max_foreign_tabs_per_device = GetMaxForeignTabsPerDevice();
209
210 std::vector<SessionData> suggestion_candidates =
211 GetSuggestionCandidates(foreign_sessions);
212 // This sorts by recency so that we keep the most recent entries and they
213 // appear as
Marc Treib 2016/09/19 09:24:08 nit: remove the extra line break
skym 2016/09/19 18:54:48 Done.
214 // suggestions in reverse chronological order.
215 std::sort(suggestion_candidates.begin(), suggestion_candidates.end());
216
217 std::vector<ContentSuggestion> suggestions;
218 std::set<std::string> included_urls;
219 std::map<std::string, int> suggestions_per_session;
220 for (const SessionData& candidate : suggestion_candidates) {
221 /*const SyncedSession& session = *std::get<0>(tuple);
tschumann 2016/09/17 15:52:57 please remove ;-)
skym 2016/09/19 18:54:48 Whooops, done!
222 const SessionTab& tab = *std::get<1>(tuple);
223 const SerializedNavigationEntry& navigation = *std::get<2>(tuple);*/
224 const std::string& session_tag = candidate.session->session_tag;
225 auto duplicates_iter =
226 included_urls.find(candidate.navigation->virtual_url().spec());
227 auto count_iter = suggestions_per_session.find(session_tag);
228 int count =
229 count_iter == suggestions_per_session.end() ? 0 : count_iter->second;
230
231 // Pick up to max (total and per device) tabs, and ensure no duplicates
232 // are selected. This filtering must be done in a second pass because
233 // this can cause newer tabs occluding less recent tabs, requiring more
234 // than |max_foreign_tabs_per_device| to be considered per device.
235 if (static_cast<int>(suggestions.size()) >= max_foreign_tabs_total ||
236 duplicates_iter != included_urls.end() ||
237 count >= max_foreign_tabs_per_device) {
238 continue;
239 }
240 included_urls.insert(candidate.navigation->virtual_url().spec());
241 suggestions_per_session[session_tag] = count + 1;
242 suggestions.push_back(BuildSuggestion(candidate));
243 }
244
245 return suggestions;
246 }
247
248 std::vector<ForeignSessionsSuggestionsProvider::SessionData>
249 ForeignSessionsSuggestionsProvider::GetSuggestionCandidates(
250 const std::vector<const SyncedSession*>& foreign_sessions) {
251 // TODO(skym): If a tab was previously dismissed, but was since updated,
252 // should it be resurrected and removed from the dismissed list? This would
253 // likely require a change to the dismissed ids.
254 // TODO(skym): No sense in keeping around dismissals for urls that no longer
255 // exist on any current foreign devices. Should prune and save the pref back.
256 std::set<std::string> dismissed_ids = prefs::ReadDismissedIDsFromPrefs(
257 *pref_service_, prefs::kDismissedForeignSessionsSuggestions);
tschumann 2016/09/17 15:52:56 I'm not feeling strongly, just want to mention the
Marc Treib 2016/09/19 09:24:08 This seems like a good idea. If you keep the membe
skym 2016/09/19 18:54:48 I like keeping this a member function. Going forwa
skym 2016/09/19 18:54:48 Acknowledged.
258 const TimeDelta max_foreign_tab_age = GetMaxForeignTabAge();
259 std::vector<SessionData> suggestion_candidates;
260
261 for (const SyncedSession* session : foreign_sessions) {
262 for (const std::pair<const SessionID::id_type, SessionWindow*>& key_value :
263 session->windows) {
264 for (const SessionTab* tab : key_value.second->tabs) {
265 if (tab->navigations.empty())
266 continue;
267
268 const SerializedNavigationEntry& navigation = tab->navigations.back();
269 const std::string unique_id =
270 MakeUniqueID(provided_category_, navigation.virtual_url().spec());
271 // TODO(skym): Filter out internal pages. Tabs that contain only
272 // non-syncable content should never reach the local client, but
273 // sometimes the most recent navigation may be internal while one
274 // of the previous ones was more valid.
275 if (dismissed_ids.find(unique_id) == dismissed_ids.end() &&
276 (base::Time::Now() - tab->timestamp) < max_foreign_tab_age) {
277 suggestion_candidates.push_back(
278 SessionData{session, tab, &navigation});
279 }
280 }
281 }
282 }
283 return suggestion_candidates;
284 }
285
286 ContentSuggestion ForeignSessionsSuggestionsProvider::BuildSuggestion(
287 const SessionData& data) {
288 ContentSuggestion suggestion(
289 MakeUniqueID(provided_category_, data.navigation->virtual_url().spec()),
290 data.navigation->virtual_url());
291 suggestion.set_title(data.navigation->title());
292 suggestion.set_publish_date(data.tab->timestamp);
293 // TODO(skym): It's unclear if this single approach is sufficient for
Marc Treib 2016/09/19 09:24:09 s/single/simple/
skym 2016/09/19 18:54:47 Done.
294 // right-to-left languages.
295 // This field is sandwiched between the url's favicon, which is on the left,
296 // and the |publish_date|, which is to the right. The domain always appear
Marc Treib 2016/09/19 09:24:09 nit: should this be "The domain always appear*s*",
skym 2016/09/19 18:54:47 Going with should.
297 // next to the favicon.
298 suggestion.set_publisher_name(
299 base::UTF8ToUTF16(data.navigation->virtual_url().host() + " - " +
300 data.session->session_name));
301 return suggestion;
302 }
303
304 } // namespace ntp_snippets
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698