Chromium Code Reviews| 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/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 | |
| OLD | NEW |