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