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 // Collection of pointers to various sessions objects that contain a superset of |
| 68 // the information needed to create a single suggestion. |
| 69 struct ForeignSessionsSuggestionsProvider::SessionData { |
| 70 const sync_sessions::SyncedSession* session; |
| 71 const sessions::SessionTab* tab; |
| 72 const sessions::SerializedNavigationEntry* navigation; |
| 73 bool operator<(const SessionData& other) const { |
| 74 // Note that SerializedNavigationEntry::timestamp() is never set to a |
| 75 // value, so always use SessionTab::timestamp() instead. |
| 76 // TODO(skym): It might be better if we sorted by recency of session, and |
| 77 // only then by recency of the tab. Right now this causes a single |
| 78 // device's tabs to be interleaved with another devices' tabs. |
| 79 return tab->timestamp > other.tab->timestamp; |
| 80 } |
| 81 }; |
| 82 |
| 83 ForeignSessionsSuggestionsProvider::ForeignSessionsSuggestionsProvider( |
| 84 ContentSuggestionsProvider::Observer* observer, |
| 85 CategoryFactory* category_factory, |
| 86 std::unique_ptr<ForeignSessionsProvider> foreign_sessions_provider, |
| 87 PrefService* pref_service) |
| 88 : ContentSuggestionsProvider(observer, category_factory), |
| 89 category_status_(CategoryStatus::INITIALIZING), |
| 90 provided_category_( |
| 91 category_factory->FromKnownCategory(KnownCategories::FOREIGN_TABS)), |
| 92 foreign_sessions_provider_(std::move(foreign_sessions_provider)), |
| 93 pref_service_(pref_service) { |
| 94 foreign_sessions_provider_->SubscribeForForeignTabChange( |
| 95 base::Bind(&ForeignSessionsSuggestionsProvider::OnForeignTabChange, |
| 96 base::Unretained(this))); |
| 97 |
| 98 // If sync is already initialzed, try suggesting now, though this is unlikely. |
| 99 OnForeignTabChange(); |
| 100 } |
| 101 |
| 102 ForeignSessionsSuggestionsProvider::~ForeignSessionsSuggestionsProvider() {} |
| 103 |
| 104 // static |
| 105 void ForeignSessionsSuggestionsProvider::RegisterProfilePrefs( |
| 106 PrefRegistrySimple* registry) { |
| 107 registry->RegisterListPref(prefs::kDismissedForeignSessionsSuggestions); |
| 108 } |
| 109 |
| 110 CategoryStatus ForeignSessionsSuggestionsProvider::GetCategoryStatus( |
| 111 Category category) { |
| 112 DCHECK_EQ(category, provided_category_); |
| 113 return category_status_; |
| 114 } |
| 115 |
| 116 CategoryInfo ForeignSessionsSuggestionsProvider::GetCategoryInfo( |
| 117 Category category) { |
| 118 DCHECK_EQ(category, provided_category_); |
| 119 return CategoryInfo(l10n_util::GetStringUTF16( |
| 120 IDS_NTP_FOREIGN_SESSIONS_SUGGESTIONS_SECTION_HEADER), |
| 121 ContentSuggestionsCardLayout::MINIMAL_CARD, |
| 122 /*has_more_button=*/true, |
| 123 /*show_if_empty=*/false); |
| 124 } |
| 125 |
| 126 void ForeignSessionsSuggestionsProvider::DismissSuggestion( |
| 127 const std::string& suggestion_id) { |
| 128 // TODO(skym): Right now this continuously grows, without clearing out old and |
| 129 // irrelevant entries. Could either use a timestamp and expire after a |
| 130 // threshold, or compare with current foreign tabs and remove anything that |
| 131 // isn't actively blockign a foreign_sessions tab. |
| 132 std::set<std::string> dismissed_ids = prefs::ReadDismissedIDsFromPrefs( |
| 133 *pref_service_, prefs::kDismissedForeignSessionsSuggestions); |
| 134 dismissed_ids.insert(suggestion_id); |
| 135 prefs::StoreDismissedIDsToPrefs(pref_service_, |
| 136 prefs::kDismissedForeignSessionsSuggestions, |
| 137 dismissed_ids); |
| 138 } |
| 139 |
| 140 void ForeignSessionsSuggestionsProvider::FetchSuggestionImage( |
| 141 const std::string& suggestion_id, |
| 142 const ImageFetchedCallback& callback) { |
| 143 base::ThreadTaskRunnerHandle::Get()->PostTask( |
| 144 FROM_HERE, base::Bind(callback, gfx::Image())); |
| 145 } |
| 146 |
| 147 void ForeignSessionsSuggestionsProvider::ClearHistory( |
| 148 base::Time begin, |
| 149 base::Time end, |
| 150 const base::Callback<bool(const GURL& url)>& filter) { |
| 151 std::set<std::string> dismissed_ids = prefs::ReadDismissedIDsFromPrefs( |
| 152 *pref_service_, prefs::kDismissedForeignSessionsSuggestions); |
| 153 for (auto iter = dismissed_ids.begin(); iter != dismissed_ids.end();) { |
| 154 if (filter.Run(GURL(base::StringPiece(*iter)))) { |
| 155 iter = dismissed_ids.erase(iter); |
| 156 } else { |
| 157 ++iter; |
| 158 } |
| 159 } |
| 160 prefs::StoreDismissedIDsToPrefs(pref_service_, |
| 161 prefs::kDismissedForeignSessionsSuggestions, |
| 162 dismissed_ids); |
| 163 } |
| 164 |
| 165 void ForeignSessionsSuggestionsProvider::ClearCachedSuggestions( |
| 166 Category category) { |
| 167 DCHECK_EQ(category, provided_category_); |
| 168 // Ignored. |
| 169 } |
| 170 |
| 171 void ForeignSessionsSuggestionsProvider::GetDismissedSuggestionsForDebugging( |
| 172 Category category, |
| 173 const DismissedSuggestionsCallback& callback) { |
| 174 DCHECK_EQ(category, provided_category_); |
| 175 callback.Run(std::vector<ContentSuggestion>()); |
| 176 } |
| 177 |
| 178 void ForeignSessionsSuggestionsProvider::ClearDismissedSuggestionsForDebugging( |
| 179 Category category) { |
| 180 DCHECK_EQ(category, provided_category_); |
| 181 pref_service_->ClearPref(prefs::kDismissedForeignSessionsSuggestions); |
| 182 } |
| 183 |
| 184 void ForeignSessionsSuggestionsProvider::OnForeignTabChange() { |
| 185 if (!foreign_sessions_provider_->HasSessionsData()) { |
| 186 if (category_status_ == CategoryStatus::AVAILABLE) { |
| 187 // This is to handle the case where the user disabled sync [sessions] or |
| 188 // logs out after we've already provided actual suggestions. |
| 189 category_status_ = CategoryStatus::NOT_PROVIDED; |
| 190 observer()->OnCategoryStatusChanged(this, provided_category_, |
| 191 category_status_); |
| 192 } |
| 193 return; |
| 194 } |
| 195 |
| 196 if (category_status_ != CategoryStatus::AVAILABLE) { |
| 197 // The further below logic will overwrite any error state. This is |
| 198 // currently okay because no where in the current implementation does the |
| 199 // status get set to an error state. Should this change, reconsider the |
| 200 // overwriting logic. |
| 201 DCHECK(category_status_ == CategoryStatus::INITIALIZING || |
| 202 category_status_ == CategoryStatus::NOT_PROVIDED); |
| 203 |
| 204 // It is difficult to tell if sync simply has not initialized yet or there |
| 205 // will never be data because the user is signed out or has disabled the |
| 206 // sessions data type. Because this provider is hidden when there are no |
| 207 // results, always just update to AVAILABLE once we might have results. |
| 208 category_status_ = CategoryStatus::AVAILABLE; |
| 209 observer()->OnCategoryStatusChanged(this, provided_category_, |
| 210 category_status_); |
| 211 } |
| 212 |
| 213 // observer()->OnNewSuggestions must be called even when we have no |
| 214 // suggestions to remove previous suggestions that are now filtered out. |
| 215 observer()->OnNewSuggestions(this, provided_category_, BuildSuggestions()); |
| 216 } |
| 217 |
| 218 std::vector<ContentSuggestion> |
| 219 ForeignSessionsSuggestionsProvider::BuildSuggestions() { |
| 220 const int max_foreign_tabs_total = GetMaxForeignTabsTotal(); |
| 221 const int max_foreign_tabs_per_device = GetMaxForeignTabsPerDevice(); |
| 222 |
| 223 std::vector<SessionData> suggestion_candidates = GetSuggestionCandidates(); |
| 224 // This sorts by recency so that we keep the most recent entries and they |
| 225 // appear as suggestions in reverse chronological order. |
| 226 std::sort(suggestion_candidates.begin(), suggestion_candidates.end()); |
| 227 |
| 228 std::vector<ContentSuggestion> suggestions; |
| 229 std::set<std::string> included_urls; |
| 230 std::map<std::string, int> suggestions_per_session; |
| 231 for (const SessionData& candidate : suggestion_candidates) { |
| 232 const std::string& session_tag = candidate.session->session_tag; |
| 233 auto duplicates_iter = |
| 234 included_urls.find(candidate.navigation->virtual_url().spec()); |
| 235 auto count_iter = suggestions_per_session.find(session_tag); |
| 236 int count = |
| 237 count_iter == suggestions_per_session.end() ? 0 : count_iter->second; |
| 238 |
| 239 // Pick up to max (total and per device) tabs, and ensure no duplicates |
| 240 // are selected. This filtering must be done in a second pass because |
| 241 // this can cause newer tabs occluding less recent tabs, requiring more |
| 242 // than |max_foreign_tabs_per_device| to be considered per device. |
| 243 if (static_cast<int>(suggestions.size()) >= max_foreign_tabs_total || |
| 244 duplicates_iter != included_urls.end() || |
| 245 count >= max_foreign_tabs_per_device) { |
| 246 continue; |
| 247 } |
| 248 included_urls.insert(candidate.navigation->virtual_url().spec()); |
| 249 suggestions_per_session[session_tag] = count + 1; |
| 250 suggestions.push_back(BuildSuggestion(candidate)); |
| 251 } |
| 252 |
| 253 return suggestions; |
| 254 } |
| 255 |
| 256 std::vector<ForeignSessionsSuggestionsProvider::SessionData> |
| 257 ForeignSessionsSuggestionsProvider::GetSuggestionCandidates() { |
| 258 // TODO(skym): If a tab was previously dismissed, but was since updated, |
| 259 // should it be resurrected and removed from the dismissed list? This would |
| 260 // likely require a change to the dismissed ids. |
| 261 // TODO(skym): No sense in keeping around dismissals for urls that no longer |
| 262 // exist on any current foreign devices. Should prune and save the pref back. |
| 263 const std::vector<const SyncedSession*>& foreign_sessions = |
| 264 foreign_sessions_provider_->GetAllForeignSessions(); |
| 265 std::set<std::string> dismissed_ids = prefs::ReadDismissedIDsFromPrefs( |
| 266 *pref_service_, prefs::kDismissedForeignSessionsSuggestions); |
| 267 const TimeDelta max_foreign_tab_age = GetMaxForeignTabAge(); |
| 268 std::vector<SessionData> suggestion_candidates; |
| 269 |
| 270 for (const SyncedSession* session : foreign_sessions) { |
| 271 for (const std::pair<const SessionID::id_type, SessionWindow*>& key_value : |
| 272 session->windows) { |
| 273 for (const SessionTab* tab : key_value.second->tabs) { |
| 274 if (tab->navigations.empty()) |
| 275 continue; |
| 276 |
| 277 const SerializedNavigationEntry& navigation = tab->navigations.back(); |
| 278 const std::string unique_id = |
| 279 MakeUniqueID(provided_category_, navigation.virtual_url().spec()); |
| 280 // TODO(skym): Filter out internal pages. Tabs that contain only |
| 281 // non-syncable content should never reach the local client, but |
| 282 // sometimes the most recent navigation may be internal while one |
| 283 // of the previous ones was more valid. |
| 284 if (dismissed_ids.find(unique_id) == dismissed_ids.end() && |
| 285 (base::Time::Now() - tab->timestamp) < max_foreign_tab_age) { |
| 286 suggestion_candidates.push_back( |
| 287 SessionData{session, tab, &navigation}); |
| 288 } |
| 289 } |
| 290 } |
| 291 } |
| 292 return suggestion_candidates; |
| 293 } |
| 294 |
| 295 ContentSuggestion ForeignSessionsSuggestionsProvider::BuildSuggestion( |
| 296 const SessionData& data) { |
| 297 ContentSuggestion suggestion( |
| 298 MakeUniqueID(provided_category_, data.navigation->virtual_url().spec()), |
| 299 data.navigation->virtual_url()); |
| 300 suggestion.set_title(data.navigation->title()); |
| 301 suggestion.set_publish_date(data.tab->timestamp); |
| 302 // TODO(skym): It's unclear if this simple approach is sufficient for |
| 303 // right-to-left languages. |
| 304 // This field is sandwiched between the url's favicon, which is on the left, |
| 305 // and the |publish_date|, which is to the right. The domain should always |
| 306 // appear next to the favicon. |
| 307 suggestion.set_publisher_name( |
| 308 base::UTF8ToUTF16(data.navigation->virtual_url().host() + " - " + |
| 309 data.session->session_name)); |
| 310 return suggestion; |
| 311 } |
| 312 |
| 313 } // namespace ntp_snippets |
OLD | NEW |