OLD | NEW |
---|---|
(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 | |
OLD | NEW |