| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "components/feed/core/v2/feed_stream.h" |
| |
| #include <algorithm> |
| #include <set> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/containers/contains.h" |
| #include "base/containers/flat_set.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/location.h" |
| #include "base/logging.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/time/time.h" |
| #include "build/buildflag.h" |
| #include "components/feed/core/common/pref_names.h" |
| #include "components/feed/core/proto/v2/store.pb.h" |
| #include "components/feed/core/proto/v2/ui.pb.h" |
| #include "components/feed/core/proto/v2/wire/content_id.pb.h" |
| #include "components/feed/core/proto/v2/wire/reliability_logging_enums.pb.h" |
| #include "components/feed/core/proto/v2/wire/there_and_back_again_data.pb.h" |
| #include "components/feed/core/shared_prefs/pref_names.h" |
| #include "components/feed/core/v2/config.h" |
| #include "components/feed/core/v2/enums.h" |
| #include "components/feed/core/v2/feed_network.h" |
| #include "components/feed/core/v2/feed_store.h" |
| #include "components/feed/core/v2/feed_stream_surface.h" |
| #include "components/feed/core/v2/feedstore_util.h" |
| #include "components/feed/core/v2/image_fetcher.h" |
| #include "components/feed/core/v2/ios_shared_prefs.h" |
| #include "components/feed/core/v2/metrics_reporter.h" |
| #include "components/feed/core/v2/prefs.h" |
| #include "components/feed/core/v2/protocol_translator.h" |
| #include "components/feed/core/v2/public/common_enums.h" |
| #include "components/feed/core/v2/public/feed_api.h" |
| #include "components/feed/core/v2/public/feed_service.h" |
| #include "components/feed/core/v2/public/logging_parameters.h" |
| #include "components/feed/core/v2/public/refresh_task_scheduler.h" |
| #include "components/feed/core/v2/public/reliability_logging_bridge.h" |
| #include "components/feed/core/v2/public/stream_type.h" |
| #include "components/feed/core/v2/public/types.h" |
| #include "components/feed/core/v2/public/unread_content_observer.h" |
| #include "components/feed/core/v2/scheduling.h" |
| #include "components/feed/core/v2/stream/unread_content_notifier.h" |
| #include "components/feed/core/v2/stream_model.h" |
| #include "components/feed/core/v2/surface_updater.h" |
| #include "components/feed/core/v2/tasks/clear_all_task.h" |
| #include "components/feed/core/v2/tasks/clear_stream_task.h" |
| #include "components/feed/core/v2/tasks/load_stream_task.h" |
| #include "components/feed/core/v2/tasks/prefetch_images_task.h" |
| #include "components/feed/core/v2/tasks/upload_actions_task.h" |
| #include "components/feed/core/v2/tasks/wait_for_store_initialize_task.h" |
| #include "components/feed/core/v2/types.h" |
| #include "components/feed/core/v2/web_feed_subscription_coordinator.h" |
| #include "components/feed/feed_feature_list.h" |
| #include "components/offline_pages/task/closure_task.h" |
| #include "components/prefs/pref_service.h" |
| #include "components/signin/public/base/signin_pref_names.h" |
| #include "google_apis/gaia/gaia_id.h" |
| |
| namespace feed { |
| namespace { |
| constexpr size_t kMaxRecentFeedNavigations = 10; |
| constexpr base::TimeDelta kFeedCloseRefreshDelay = base::Minutes(30); |
| |
| void UpdateDebugStreamData( |
| const UploadActionsTask::Result& upload_actions_result, |
| DebugStreamData& debug_data) { |
| if (upload_actions_result.last_network_response_info) { |
| debug_data.upload_info = upload_actions_result.last_network_response_info; |
| } |
| } |
| |
| void PopulateDebugStreamData(const LoadStreamTask::Result& load_result, |
| PrefService& profile_prefs) { |
| DebugStreamData debug_data = ::feed::prefs::GetDebugStreamData(profile_prefs); |
| std::stringstream ss; |
| ss << "Code: " << load_result.final_status; |
| debug_data.load_stream_status = ss.str(); |
| if (load_result.network_response_info) { |
| debug_data.fetch_info = load_result.network_response_info; |
| } |
| if (load_result.upload_actions_result) { |
| UpdateDebugStreamData(*load_result.upload_actions_result, debug_data); |
| } |
| ::feed::prefs::SetDebugStreamData(debug_data, profile_prefs); |
| } |
| |
| void PopulateDebugStreamData( |
| const UploadActionsTask::Result& upload_actions_result, |
| PrefService& profile_prefs) { |
| DebugStreamData debug_data = ::feed::prefs::GetDebugStreamData(profile_prefs); |
| UpdateDebugStreamData(upload_actions_result, debug_data); |
| ::feed::prefs::SetDebugStreamData(debug_data, profile_prefs); |
| } |
| |
| // Will check all sources of ordering setting and always return a valid result. |
| ContentOrder GetValidWebFeedContentOrder(const PrefService& pref_service) { |
| // First priority is the prefs stored order choice. |
| ContentOrder pref_order = prefs::GetWebFeedContentOrder(pref_service); |
| if (pref_order != ContentOrder::kUnspecified) |
| return pref_order; |
| // Defaults to grouped, encompassing finch_order == "grouped". |
| return ContentOrder::kGrouped; |
| } |
| |
| LoadType RequestScheduleTypeToLoadType(RequestSchedule::Type type) { |
| switch (type) { |
| case RequestSchedule::Type::kFeedCloseRefresh: |
| return LoadType::kFeedCloseBackgroundRefresh; |
| case RequestSchedule::Type::kScheduledRefresh: |
| default: |
| return LoadType::kBackgroundRefresh; |
| } |
| } |
| |
| } // namespace |
| |
| FeedStream::Stream::Stream(const StreamType& stream_type) |
| : type(stream_type), surfaces(stream_type) {} |
| FeedStream::Stream::~Stream() = default; |
| |
| FeedStream::FeedStream(RefreshTaskScheduler* refresh_task_scheduler, |
| MetricsReporter* metrics_reporter, |
| Delegate* delegate, |
| PrefService* profile_prefs, |
| FeedNetwork* feed_network, |
| ImageFetcher* image_fetcher, |
| FeedStore* feed_store, |
| PersistentKeyValueStoreImpl* persistent_key_value_store, |
| TemplateURLService* template_url_service, |
| const ChromeInfo& chrome_info) |
| : refresh_task_scheduler_(refresh_task_scheduler), |
| metrics_reporter_(metrics_reporter), |
| delegate_(delegate), |
| profile_prefs_(profile_prefs), |
| feed_network_(feed_network), |
| image_fetcher_(image_fetcher), |
| store_(feed_store), |
| persistent_key_value_store_(persistent_key_value_store), |
| template_url_service_(template_url_service), |
| chrome_info_(chrome_info), |
| task_queue_(this), |
| request_throttler_(profile_prefs), |
| privacy_notice_card_tracker_(profile_prefs), |
| info_card_tracker_(profile_prefs), |
| user_actions_collector_(profile_prefs) { |
| DCHECK(persistent_key_value_store_); |
| DCHECK(feed_network_); |
| DCHECK(profile_prefs_); |
| DCHECK(metrics_reporter); |
| DCHECK(image_fetcher_); |
| |
| static WireResponseTranslator default_translator; |
| wire_response_translator_ = &default_translator; |
| metrics_reporter_->Initialize(this); |
| |
| base::RepeatingClosure preference_change_callback = |
| base::BindRepeating(&FeedStream::EnabledPreferencesChanged, GetWeakPtr()); |
| snippets_enabled_by_policy_.Init(prefs::kEnableSnippets, profile_prefs, |
| preference_change_callback); |
| articles_list_visible_.Init(prefs::kArticlesListVisible, profile_prefs, |
| preference_change_callback); |
| snippets_enabled_by_dse_.Init(prefs::kEnableSnippetsByDse, profile_prefs, |
| preference_change_callback); |
| has_stored_data_.Init(feed::prefs::kHasStoredData, profile_prefs); |
| signin_allowed_.Init( |
| ::prefs::kSigninAllowed, profile_prefs, |
| base::BindRepeating(&FeedStream::ClearAll, GetWeakPtr())); |
| web_feed_subscription_coordinator_ = |
| std::make_unique<WebFeedSubscriptionCoordinator>(delegate, this); |
| |
| // Inserting this task first ensures that |store_| is initialized before |
| // it is used. |
| task_queue_.AddTask(FROM_HERE, |
| std::make_unique<WaitForStoreInitializeTask>( |
| store_, this, |
| base::BindOnce(&FeedStream::InitializeComplete, |
| base::Unretained(this)))); |
| EnabledPreferencesChanged(); |
| } |
| |
| FeedStream::~FeedStream() = default; |
| |
| WebFeedSubscriptionCoordinator& FeedStream::subscriptions() { |
| return *web_feed_subscription_coordinator_; |
| } |
| |
| FeedStream::Stream* FeedStream::FindStream(const StreamType& stream_type) { |
| auto iter = streams_.find(stream_type); |
| return (iter != streams_.end()) ? &iter->second : nullptr; |
| } |
| |
| const FeedStream::Stream* FeedStream::FindStream( |
| const StreamType& stream_type) const { |
| return const_cast<FeedStream*>(this)->FindStream(stream_type); |
| } |
| |
| FeedStream::Stream* FeedStream::FindStream(SurfaceId surface_id) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (surface) { |
| return FindStream(surface->GetStreamType()); |
| } |
| return nullptr; |
| } |
| |
| FeedStreamSurface* FeedStream::FindSurface(SurfaceId surface_id) { |
| // There should only ever be a handful of surfaces at once, so linear search |
| // is fine. |
| for (FeedStreamSurface& surface : all_surfaces_) { |
| if (surface.GetSurfaceId() == surface_id) { |
| return &surface; |
| } |
| } |
| return nullptr; |
| } |
| |
| FeedStream::Stream& FeedStream::GetStream(const StreamType& stream_type) { |
| CHECK(stream_type.IsValid()); |
| auto iter = streams_.find(stream_type); |
| if (iter != streams_.end()) |
| return iter->second; |
| FeedStream::Stream& new_stream = |
| streams_.emplace(stream_type, stream_type).first->second; |
| new_stream.surface_updater = std::make_unique<SurfaceUpdater>( |
| metrics_reporter_, &global_datastore_slice_, &new_stream.surfaces); |
| new_stream.surfaces.AddObserver(new_stream.surface_updater.get()); |
| return new_stream; |
| } |
| |
| StreamModel* FeedStream::GetModel(const StreamType& stream_type) { |
| Stream* stream = FindStream(stream_type); |
| return stream ? stream->model.get() : nullptr; |
| } |
| |
| StreamModel* FeedStream::GetModel(SurfaceId surface_id) { |
| Stream* stream = FindStream(surface_id); |
| return stream ? stream->model.get() : nullptr; |
| } |
| |
| feedwire::DiscoverLaunchResult FeedStream::TriggerStreamLoad( |
| const StreamType& stream_type, |
| SingleWebFeedEntryPoint entry_point) { |
| Stream& stream = GetStream(stream_type); |
| if (stream.model || stream.model_loading_in_progress) |
| return feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED; |
| |
| // If we should not load the stream, abort and send a zero-state update. |
| LaunchResult do_not_attempt_reason = |
| ShouldAttemptLoad(stream_type, LoadType::kInitialLoad); |
| if (do_not_attempt_reason.load_stream_status != LoadStreamStatus::kNoStatus) { |
| LoadStreamTask::Result result(stream_type, |
| do_not_attempt_reason.load_stream_status); |
| result.launch_result = do_not_attempt_reason.launch_result; |
| StreamLoadComplete(std::move(result)); |
| return do_not_attempt_reason.launch_result; |
| } |
| |
| stream.model_loading_in_progress = true; |
| |
| stream.surface_updater->LoadStreamStarted(/*manual_refreshing=*/false); |
| LoadStreamTask::Options options; |
| options.stream_type = stream_type; |
| options.single_feed_entry_point = entry_point; |
| if (!loaded_after_start_ && |
| base::FeatureList::IsEnabled(kRefreshFeedOnRestart)) { |
| options.refresh_even_when_not_stale = true; |
| } |
| task_queue_.AddTask(FROM_HERE, |
| std::make_unique<LoadStreamTask>( |
| options, this, |
| base::BindOnce(&FeedStream::StreamLoadComplete, |
| base::Unretained(this)))); |
| return feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED; |
| } |
| |
| void FeedStream::InitializeComplete(WaitForStoreInitializeTask::Result result) { |
| metadata_ = *std::move(result.startup_data.metadata); |
| delegate_->SetFeedLaunchCuiMetadata(metadata_.feed_launch_cui_metadata()); |
| for (const feedstore::StreamData& stream_data : |
| result.startup_data.stream_data) { |
| StreamType stream_type = |
| feedstore::StreamTypeFromKey(stream_data.stream_key()); |
| if (stream_type.IsValid()) { |
| GetStream(stream_type).content_ids = |
| feedstore::GetContentIds(stream_data); |
| } |
| if (stream_type.IsForYou()) { |
| GetStream(stream_type).viewed_content_hashes = |
| feedstore::GetViewedContentHashes(metadata_, stream_type); |
| } |
| } |
| |
| metadata_populated_ = true; |
| metrics_reporter_->OnMetadataInitialized( |
| IsFeedEnabledByEnterprisePolicy(), IsArticlesListVisible(), IsSignedIn(), |
| IsFeedEnabled(), metadata_); |
| |
| web_feed_subscription_coordinator_->Populate(result.web_feed_startup_data); |
| |
| for (const feedstore::StreamData& stream_data : |
| result.startup_data.stream_data) { |
| StreamType stream_type = |
| feedstore::StreamTypeFromKey(stream_data.stream_key()); |
| if (stream_type.IsValid()) |
| MaybeNotifyHasUnreadContent(stream_type); |
| } |
| |
| if (!IsEnabledAndVisible() && has_stored_data_.GetValue()) { |
| ClearAll(); |
| } |
| } |
| |
| void FeedStream::StreamLoadComplete(LoadStreamTask::Result result) { |
| DCHECK(result.load_type == LoadType::kInitialLoad || |
| result.load_type == LoadType::kManualRefresh); |
| |
| loaded_after_start_ = true; |
| |
| Stream& stream = GetStream(result.stream_type); |
| if (result.load_type == LoadType::kManualRefresh) |
| UnloadModel(result.stream_type); |
| |
| if (result.update_request) { |
| auto model = std::make_unique<StreamModel>( |
| &stream_model_context_, |
| feed::MakeLoggingParameters(prefs::GetClientInstanceId(*profile_prefs_), |
| |
| *result.update_request)); |
| model->Update(std::move(result.update_request)); |
| |
| if (!model->HasVisibleContent() && |
| result.launch_result == |
| feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED) { |
| result.launch_result = |
| feedwire::DiscoverLaunchResult::NO_CARDS_RESPONSE_ERROR_ZERO_CARDS; |
| } |
| |
| LoadModel(result.stream_type, std::move(model)); |
| } |
| |
| if (result.request_schedule) |
| SetRequestSchedule(stream.type, *result.request_schedule); |
| |
| ContentStats content_stats; |
| if (stream.model) |
| content_stats = stream.model->GetContentStats(); |
| |
| // stream_metadata is an optional because in some scenarios StreamLoadComplete |
| // is called bypassing the task queue. In this case WaitForStoreInitialize |
| // task is not completed first and the metadata is not populated. Thus, we |
| // dont need to track content_lifetime. |
| std::optional<feedstore::Metadata::StreamMetadata> stream_metadata; |
| if (metadata_populated_) { |
| feedstore::Metadata metadata = GetMetadata(); |
| stream_metadata = |
| feedstore::MetadataForStream(metadata, result.stream_type); |
| } |
| |
| MetricsReporter::LoadStreamResultSummary result_summary; |
| result_summary.load_from_store_status = result.load_from_store_status; |
| result_summary.final_status = result.final_status; |
| result_summary.is_initial_load = result.load_type == LoadType::kInitialLoad; |
| result_summary.loaded_new_content_from_network = |
| result.loaded_new_content_from_network; |
| result_summary.stored_content_age = result.stored_content_age; |
| result_summary.content_order = GetContentOrder(result.stream_type); |
| result_summary.stream_metadata = stream_metadata; |
| |
| metrics_reporter_->OnLoadStream(stream.type, result_summary, content_stats, |
| std::move(result.latencies)); |
| |
| stream.model_loading_in_progress = false; |
| stream.surface_updater->LoadStreamComplete( |
| stream.model != nullptr, result.final_status, result.launch_result); |
| |
| LoadTaskComplete(result); |
| |
| // When done loading the for-you feed, try to refresh the web-feed if there's |
| // no unread content. |
| if (IsWebFeedEnabled() && IsSignedIn() && |
| result.load_type != LoadType::kManualRefresh && |
| result.stream_type.IsForYou() && chained_web_feed_refresh_enabled_) { |
| // Checking for users without follows. |
| // TODO(b/229143375) - We should rate limit fetches if the server side is |
| // turned off for this locale, and continually fails. |
| StreamType following_type = StreamType(StreamKind::kFollowing); |
| if (!HasUnreadContent(following_type)) { |
| LoadStreamTask::Options options; |
| options.load_type = LoadType::kBackgroundRefresh; |
| options.stream_type = following_type; |
| options.abort_if_unread_content = true; |
| task_queue_.AddTask( |
| FROM_HERE, std::make_unique<LoadStreamTask>( |
| options, this, |
| base::BindOnce(&FeedStream::BackgroundRefreshComplete, |
| base::Unretained(this)))); |
| } |
| } |
| |
| if (result.load_type == LoadType::kManualRefresh) { |
| std::vector<base::OnceCallback<void(bool)>> moved_callbacks = |
| std::move(stream.refresh_complete_callbacks); |
| for (auto& callback : moved_callbacks) { |
| std::move(callback).Run(result.loaded_new_content_from_network); |
| } |
| } |
| } |
| |
| void FeedStream::OnEnterBackground() { |
| metrics_reporter_->OnEnterBackground(); |
| if (GetFeedConfig().upload_actions_on_enter_background) { |
| task_queue_.AddTask( |
| FROM_HERE, |
| std::make_unique<UploadActionsTask>( |
| // Pass empty list to read pending actions from the store. |
| std::vector<feedstore::StoredAction>(), |
| /*from_load_more=*/false, |
| // Pass unknown stream type to skip logging upload actions events. |
| StreamType(), this, |
| base::BindOnce(&FeedStream::UploadActionsComplete, |
| base::Unretained(this)))); |
| } |
| } |
| |
| std::string FeedStream::GetSessionId() const { |
| return metadata_.session_id().token(); |
| } |
| |
| const feedstore::Metadata& FeedStream::GetMetadata() const { |
| DCHECK(metadata_populated_) |
| << "Metadata is not yet populated. This function should only be called " |
| "after the WaitForStoreInitialize task is complete."; |
| return metadata_; |
| } |
| |
| void FeedStream::SetMetadata(feedstore::Metadata metadata) { |
| metadata_ = std::move(metadata); |
| store_->WriteMetadata(metadata_, base::DoNothing()); |
| delegate_->SetFeedLaunchCuiMetadata(metadata_.feed_launch_cui_metadata()); |
| } |
| |
| void FeedStream::SetStreamStale(const StreamType& stream_type, bool is_stale) { |
| feedstore::Metadata metadata = GetMetadata(); |
| feedstore::Metadata::StreamMetadata& stream_metadata = |
| feedstore::MetadataForStream(metadata, stream_type); |
| if (stream_metadata.is_known_stale() != is_stale) { |
| stream_metadata.set_is_known_stale(is_stale); |
| if (is_stale) { |
| SetStreamViewContentHashes(metadata_, stream_type, {}); |
| } |
| SetMetadata(metadata); |
| } |
| } |
| |
| bool FeedStream::SetMetadata(std::optional<feedstore::Metadata> metadata) { |
| if (metadata) { |
| SetMetadata(std::move(*metadata)); |
| return true; |
| } |
| return false; |
| } |
| |
| void FeedStream::PrefetchImage(const GURL& url) { |
| delegate_->PrefetchImage(url); |
| } |
| |
| void FeedStream::UpdateExperiments(Experiments experiments) { |
| delegate_->RegisterExperiments(experiments); |
| prefs::SetExperiments(experiments, *profile_prefs_); |
| } |
| |
| SurfaceId FeedStream::CreateSurface(const StreamType& type, |
| SingleWebFeedEntryPoint entry_point) { |
| if (base::TimeTicks::Now() - surface_destroy_time_ > kSurfaceDestroyDelay) { |
| CleanupDestroyedSurfaces(); |
| } |
| |
| return all_surfaces_.emplace_back(type, entry_point).GetSurfaceId(); |
| } |
| |
| void FeedStream::DestroySurface(SurfaceId surface) { |
| destroyed_surfaces_.push_back(surface); |
| surface_destroy_time_ = base::TimeTicks::Now(); |
| } |
| |
| void FeedStream::CleanupDestroyedSurfaces() { |
| auto to_remove = std::ranges::remove_if( |
| all_surfaces_, [&](const FeedStreamSurface& surface) { |
| return std::ranges::find(destroyed_surfaces_, surface.GetSurfaceId()) != |
| destroyed_surfaces_.end(); |
| }); |
| all_surfaces_.erase(to_remove.begin(), to_remove.end()); |
| destroyed_surfaces_.clear(); |
| } |
| |
| void FeedStream::AttachSurface(SurfaceId surface_id, |
| SurfaceRenderer* renderer) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| CHECK(surface); |
| metrics_reporter_->SurfaceOpened(surface->GetStreamType(), |
| surface->GetSurfaceId(), |
| surface->GetSingleWebFeedEntryPoint()); |
| Stream& stream = GetStream(surface->GetStreamType()); |
| // Skip normal processing when overriding stream data from the internals page. |
| if (forced_stream_update_for_debugging_.updated_slices_size() > 0) { |
| stream.surfaces.SurfaceAdded( |
| surface_id, renderer, |
| /*loading_not_allowed_reason=*/ |
| feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED); |
| renderer->StreamUpdate(forced_stream_update_for_debugging_); |
| return; |
| } |
| |
| stream.surfaces.SurfaceAdded( |
| surface_id, renderer, |
| TriggerStreamLoad(surface->GetStreamType(), |
| surface->GetSingleWebFeedEntryPoint())); |
| |
| // Cancel any scheduled model unload task. |
| ++stream.unload_on_detach_sequence_number; |
| } |
| |
| void FeedStream::DetachSurface(SurfaceId surface_id) { |
| Stream* stream = FindStream(surface_id); |
| if (!stream) { |
| return; |
| } |
| // Ignore subsequent DetachSurface calls. |
| if (!stream->surfaces.SurfacePresent(surface_id)) { |
| return; |
| } |
| |
| metrics_reporter_->SurfaceClosed(surface_id); |
| stream->surfaces.SurfaceRemoved(surface_id); |
| ScheduleModelUnloadIfNoSurfacesAttached(stream->type); |
| } |
| |
| void FeedStream::AddUnreadContentObserver(const StreamType& stream_type, |
| UnreadContentObserver* observer) { |
| GetStream(stream_type) |
| .unread_content_notifiers.emplace_back(observer->GetWeakPtr()); |
| MaybeNotifyHasUnreadContent(stream_type); |
| } |
| |
| void FeedStream::RemoveUnreadContentObserver(const StreamType& stream_type, |
| UnreadContentObserver* observer) { |
| Stream& stream = GetStream(stream_type); |
| auto predicate = [&](const UnreadContentNotifier& notifier) { |
| UnreadContentObserver* ptr = notifier.observer().get(); |
| return ptr == nullptr || observer == ptr; |
| }; |
| std::erase_if(stream.unread_content_notifiers, predicate); |
| } |
| |
| void FeedStream::ScheduleModelUnloadIfNoSurfacesAttached( |
| const StreamType& stream_type) { |
| Stream& stream = GetStream(stream_type); |
| if (!stream.surfaces.empty()) |
| return; |
| |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&FeedStream::AddUnloadModelIfNoSurfacesAttachedTask, |
| GetWeakPtr(), stream.type, |
| stream.unload_on_detach_sequence_number), |
| GetFeedConfig().model_unload_timeout); |
| } |
| |
| void FeedStream::AddUnloadModelIfNoSurfacesAttachedTask( |
| const StreamType& stream_type, |
| int sequence_number) { |
| Stream& stream = GetStream(stream_type); |
| // Don't continue if unload_on_detach_sequence_number_ has changed. |
| if (stream.unload_on_detach_sequence_number != sequence_number) |
| return; |
| task_queue_.AddTask( |
| FROM_HERE, std::make_unique<offline_pages::ClosureTask>(base::BindOnce( |
| &FeedStream::UnloadModelIfNoSurfacesAttachedTask, |
| base::Unretained(this), stream_type))); |
| // If this is a SingleWebFeed stream, remove it and delete stream data on a |
| // delay. |
| if (stream_type.IsSingleWebFeed()) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce(&FeedStream::ClearStream, GetWeakPtr(), stream_type, |
| sequence_number), |
| GetFeedConfig().single_web_feed_stream_clear_timeout); |
| } |
| } |
| |
| void FeedStream::UnloadModelIfNoSurfacesAttachedTask( |
| const StreamType& stream_type) { |
| Stream& stream = GetStream(stream_type); |
| if (!stream.surfaces.empty()) |
| return; |
| UnloadModel(stream_type); |
| } |
| |
| bool FeedStream::IsArticlesListVisible() { |
| return articles_list_visible_.GetValue(); |
| } |
| |
| bool FeedStream::IsFeedEnabledByEnterprisePolicy() { |
| return snippets_enabled_by_policy_.GetValue(); |
| } |
| |
| bool FeedStream::IsFeedEnabled() { |
| return FeedService::IsEnabled(*profile_prefs()); |
| } |
| |
| bool FeedStream::IsEnabledAndVisible() { |
| return IsArticlesListVisible() && IsFeedEnabled() && IsFeedEnabledByDse(); |
| } |
| |
| bool FeedStream::IsFeedEnabledByDse() { |
| #if BUILDFLAG(IS_ANDROID) |
| if (chrome_info_.is_new_tab_search_engine_url_android_enabled) { |
| return snippets_enabled_by_dse_.GetValue(); |
| } |
| #endif // BUILDFLAG(IS_ANDROID) |
| return true; |
| } |
| |
| bool FeedStream::IsWebFeedEnabled() { |
| return feed::IsWebFeedEnabledForLocale(delegate_->GetCountry()) && |
| !base::FeatureList::IsEnabled(kWebFeedKillSwitch); |
| } |
| |
| void FeedStream::EnabledPreferencesChanged() { |
| // Assume there might be stored data if the Feed is ever enabled. |
| if (IsEnabledAndVisible()) |
| has_stored_data_.SetValue(true); |
| } |
| |
| void FeedStream::LoadMore(SurfaceId surface_id, |
| base::OnceCallback<void(bool)> callback) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| CHECK(surface); |
| Stream& stream = GetStream(surface->GetStreamType()); |
| if (!stream.model) { |
| DLOG(ERROR) << "Ignoring LoadMore() before the model is loaded"; |
| return std::move(callback).Run(false); |
| } |
| |
| // We want to abort early to avoid showing a loading spinner if it's not |
| // necessary. |
| if (ShouldMakeFeedQueryRequest(surface->GetStreamType(), LoadType::kLoadMore, |
| /*consume_quota=*/false) |
| .load_stream_status != LoadStreamStatus::kNoStatus) { |
| return std::move(callback).Run(false); |
| } |
| |
| stream.surface_updater->launch_reliability_logger().LogLoadMoreStarted(); |
| |
| metrics_reporter_->OnLoadMoreBegin(surface->GetStreamType(), surface_id); |
| stream.surface_updater->SetLoadingMore(true); |
| |
| // Have at most one in-flight LoadMore() request per stream. Send the result |
| // to all requestors. |
| stream.load_more_complete_callbacks.push_back(std::move(callback)); |
| if (stream.load_more_complete_callbacks.size() == 1) { |
| task_queue_.AddTask(FROM_HERE, |
| std::make_unique<LoadMoreTask>( |
| surface->GetStreamType(), this, |
| base::BindOnce(&FeedStream::LoadMoreComplete, |
| base::Unretained(this)))); |
| } |
| } |
| |
| void FeedStream::LoadMoreComplete(LoadMoreTask::Result result) { |
| Stream& stream = GetStream(result.stream_type); |
| if (stream.model && result.model_update_request) |
| stream.model->Update(std::move(result.model_update_request)); |
| |
| if (result.request_schedule) |
| SetRequestSchedule(stream.type, *result.request_schedule); |
| |
| metrics_reporter_->OnLoadMore( |
| result.stream_type, result.final_status, |
| stream.model ? stream.model->GetContentStats() : ContentStats()); |
| stream.surface_updater->SetLoadingMore(false); |
| std::vector<base::OnceCallback<void(bool)>> moved_callbacks = |
| std::move(stream.load_more_complete_callbacks); |
| bool success = result.final_status == LoadStreamStatus::kLoadedFromNetwork; |
| for (auto& callback : moved_callbacks) { |
| std::move(callback).Run(success); |
| } |
| } |
| |
| void FeedStream::ManualRefresh(SurfaceId surface_id, |
| base::OnceCallback<void(bool)> callback) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return; |
| } |
| Stream& stream = GetStream(surface->GetStreamType()); |
| |
| // Bail out immediately if loading in progress, or if no surfaces are |
| // attached. |
| if (stream.model_loading_in_progress || stream.surfaces.empty()) { |
| return std::move(callback).Run(false); |
| } |
| |
| // The user has manually refreshed. In this case we allow resetting |
| // the request throttler. Without this, it's likely the user will hit a |
| // request limit. |
| feed::prefs::SetThrottlerRequestCounts({}, *profile_prefs_); |
| |
| stream.model_loading_in_progress = true; |
| |
| stream.surface_updater->LoadStreamStarted(/*manual_refreshing=*/true); |
| |
| // Have at most one in-flight refresh request per stream. |
| stream.refresh_complete_callbacks.push_back(std::move(callback)); |
| if (stream.refresh_complete_callbacks.size() == 1) { |
| LoadStreamTask::Options options; |
| options.stream_type = surface->GetStreamType(); |
| options.load_type = LoadType::kManualRefresh; |
| task_queue_.AddTask(FROM_HERE, |
| std::make_unique<LoadStreamTask>( |
| options, this, |
| base::BindOnce(&FeedStream::StreamLoadComplete, |
| base::Unretained(this)))); |
| } |
| |
| last_refresh_scheduled_on_interaction_time_ = base::TimeTicks(); |
| |
| metrics_reporter_->OnManualRefresh(surface->GetStreamType(), metadata_, |
| stream.content_ids); |
| } |
| |
| void FeedStream::FetchResource( |
| const GURL& url, |
| const std::string& method, |
| const std::vector<std::string>& header_names_and_values, |
| const std::string& post_data, |
| base::OnceCallback<void(NetworkResponse)> callback) { |
| net::HttpRequestHeaders headers; |
| for (size_t i = 0; i + 1 < header_names_and_values.size(); i += 2) { |
| headers.SetHeader(header_names_and_values[i], |
| header_names_and_values[i + 1]); |
| } |
| feed_network_->SendAsyncDataRequest( |
| url, method, headers, post_data, GetAccountInfo(), |
| base::BindOnce(&FeedStream::FetchResourceComplete, base::Unretained(this), |
| std::move(callback))); |
| } |
| |
| void FeedStream::FetchResourceComplete( |
| base::OnceCallback<void(NetworkResponse)> callback, |
| FeedNetwork::RawResponse response) { |
| MetricsReporter::OnResourceFetched(response.response_info.status_code); |
| NetworkResponse network_response; |
| network_response.status_code = response.response_info.status_code; |
| network_response.response_bytes = std::move(response.response_bytes); |
| network_response.response_header_names_and_values = |
| std::move(response.response_info.response_header_names_and_values); |
| std::move(callback).Run(std::move(network_response)); |
| } |
| |
| void FeedStream::ExecuteOperations( |
| SurfaceId surface_id, |
| std::vector<feedstore::DataOperation> operations) { |
| Stream* stream = FindStream(surface_id); |
| if (!stream) { |
| return; |
| } |
| |
| StreamModel* model = GetModel(stream->type); |
| if (!model) { |
| DLOG(ERROR) << "Calling ExecuteOperations before the model is loaded"; |
| return; |
| } |
| // TODO(crbug.com/40777338): Convert this to a task. |
| return model->ExecuteOperations(std::move(operations)); |
| } |
| |
| EphemeralChangeId FeedStream::CreateEphemeralChange( |
| SurfaceId surface_id, |
| std::vector<feedstore::DataOperation> operations) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return {}; |
| } |
| StreamModel* model = GetModel(surface->GetStreamType()); |
| if (!model) { |
| DLOG(ERROR) << "Calling CreateEphemeralChange before the model is loaded"; |
| return {}; |
| } |
| metrics_reporter_->OtherUserAction(surface->GetStreamType(), |
| FeedUserActionType::kEphemeralChange); |
| return model->CreateEphemeralChange(std::move(operations)); |
| } |
| |
| EphemeralChangeId FeedStream::CreateEphemeralChangeFromPackedData( |
| SurfaceId surface_id, |
| std::string_view data) { |
| feedpacking::DismissData msg; |
| msg.ParseFromArray(data.data(), data.size()); |
| return CreateEphemeralChange(surface_id, |
| TranslateDismissData(base::Time::Now(), msg)); |
| } |
| |
| bool FeedStream::CommitEphemeralChange(SurfaceId surface_id, |
| EphemeralChangeId id) { |
| StreamModel* model = GetModel(surface_id); |
| if (!model) |
| return false; |
| metrics_reporter_->OtherUserAction( |
| model->GetStreamType(), FeedUserActionType::kEphemeralChangeCommited); |
| return model->CommitEphemeralChange(id); |
| } |
| |
| bool FeedStream::RejectEphemeralChange(SurfaceId surface_id, |
| EphemeralChangeId id) { |
| StreamModel* model = GetModel(surface_id); |
| if (!model) |
| return false; |
| metrics_reporter_->OtherUserAction( |
| model->GetStreamType(), FeedUserActionType::kEphemeralChangeRejected); |
| return model->RejectEphemeralChange(id); |
| } |
| |
| void FeedStream::ProcessThereAndBackAgain( |
| std::string_view data, |
| const LoggingParameters& logging_parameters) { |
| feedwire::ThereAndBackAgainData msg; |
| msg.ParseFromArray(data.data(), data.size()); |
| if (msg.has_action_payload()) { |
| feedwire::FeedAction action_msg; |
| *action_msg.mutable_action_payload() = std::move(msg.action_payload()); |
| UploadAction(std::move(action_msg), logging_parameters, |
| /*upload_now=*/true, |
| base::BindOnce(&FeedStream::UploadActionsComplete, |
| base::Unretained(this))); |
| } |
| } |
| |
| void FeedStream::ProcessViewAction( |
| std::string_view data, |
| const LoggingParameters& logging_parameters) { |
| if (!logging_parameters.view_actions_enabled) |
| return; |
| |
| feedwire::FeedAction msg; |
| msg.ParseFromArray(data.data(), data.size()); |
| UploadAction(std::move(msg), logging_parameters, |
| /*upload_now=*/false, |
| base::BindOnce(&FeedStream::UploadActionsComplete, |
| base::Unretained(this))); |
| } |
| |
| void FeedStream::UploadActionsComplete(UploadActionsTask::Result result) { |
| PopulateDebugStreamData(result, *profile_prefs_); |
| } |
| |
| bool FeedStream::WasUrlRecentlyNavigatedFromFeed(const GURL& url) { |
| return base::Contains(recent_feed_navigations_, url); |
| } |
| |
| void FeedStream::InvalidateContentCacheFor(StreamKind stream_kind) { |
| if (stream_kind != StreamKind::kUnknown) |
| SetStreamStale(StreamType(stream_kind), true); |
| } |
| void FeedStream::RecordContentViewed(SurfaceId /*surface_id*/, uint64_t docid) { |
| if (!store_) { |
| return; |
| } |
| WriteDocViewIfEnabled(*this, docid); |
| } |
| |
| DebugStreamData FeedStream::GetDebugStreamData() { |
| return ::feed::prefs::GetDebugStreamData(*profile_prefs_); |
| } |
| |
| void FeedStream::ForceRefreshForDebugging(const StreamType& stream_type) { |
| // Avoid request throttling for debug refreshes. |
| feed::prefs::SetThrottlerRequestCounts({}, *profile_prefs_); |
| task_queue_.AddTask( |
| FROM_HERE, std::make_unique<offline_pages::ClosureTask>( |
| base::BindOnce(&FeedStream::ForceRefreshForDebuggingTask, |
| base::Unretained(this), stream_type))); |
| } |
| |
| void FeedStream::ForceRefreshTask(const StreamType& stream_type) { |
| UnloadModel(stream_type); |
| store_->ClearStreamData(stream_type, base::DoNothing()); |
| GetStream(stream_type) |
| .surface_updater->launch_reliability_logger() |
| .LogFeedLaunchOtherStart(); |
| if (!GetStream(stream_type).surfaces.empty()) |
| TriggerStreamLoad(stream_type); |
| } |
| |
| void FeedStream::ForceRefreshForDebuggingTask(const StreamType& stream_type) { |
| UnloadModel(stream_type); |
| store_->ClearStreamData(stream_type, base::DoNothing()); |
| GetStream(stream_type) |
| .surface_updater->launch_reliability_logger() |
| .LogFeedLaunchOtherStart(); |
| TriggerStreamLoad(stream_type); |
| } |
| |
| std::string FeedStream::DumpStateForDebugging() { |
| Stream& stream = GetStream(StreamType(StreamKind::kForYou)); |
| std::stringstream ss; |
| if (stream.model) { |
| ss << "model loaded, " << stream.model->GetContentList().size() |
| << " contents, " |
| << "signed_in=" << stream.model->signed_in() |
| << ", logging_enabled=" << stream.model->logging_enabled() |
| << ", privacy_notice_fulfilled=" |
| << stream.model->privacy_notice_fulfilled(); |
| } |
| |
| auto print_refresh_schedule = [&](RefreshTaskId task_id) { |
| RequestSchedule schedule = |
| prefs::GetRequestSchedule(task_id, *profile_prefs_); |
| if (schedule.refresh_offsets.empty()) { |
| ss << "No request schedule\n"; |
| } else { |
| ss << "Request schedule reference " << schedule.anchor_time << '\n'; |
| for (base::TimeDelta entry : schedule.refresh_offsets) { |
| ss << " fetch at " << entry << '\n'; |
| } |
| } |
| }; |
| ss << "For You: "; |
| print_refresh_schedule(RefreshTaskId::kRefreshForYouFeed); |
| ss << "WebFeeds: "; |
| print_refresh_schedule(RefreshTaskId::kRefreshWebFeed); |
| ss << "WebFeedSubscriptions:\n"; |
| subscriptions().DumpStateForDebugging(ss); |
| return ss.str(); |
| } |
| |
| void FeedStream::SetForcedStreamUpdateForDebugging( |
| const feedui::StreamUpdate& stream_update) { |
| forced_stream_update_for_debugging_ = stream_update; |
| } |
| |
| base::Time FeedStream::GetLastFetchTime(SurfaceId surface_id) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return {}; |
| } |
| const base::Time fetch_time = |
| feedstore::GetLastFetchTime(metadata_, surface->GetStreamType()); |
| // Ignore impossible time values. |
| return (fetch_time > base::Time::Now()) ? base::Time() : fetch_time; |
| } |
| |
| std::vector<std::string> FeedStream::GetFeedUrls(SurfaceId surface_id) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return {}; |
| } |
| Stream& stream = GetStream(surface->GetStreamType()); |
| if (!stream.model) { |
| return {}; |
| } |
| |
| std::vector<std::string> urls; |
| for (ContentRevision content_revision : stream.model->GetContentList()) { |
| const feedstore::Content* content = |
| stream.model->FindContent(content_revision); |
| if (content) { |
| std::string url = content->prefetch_metadata(0).uri(); |
| // Exclude video streaming cards. |
| if (!url.starts_with("https://www.youtube.com") && |
| !url.starts_with("https://m.youtube.com")) { |
| urls.push_back(url); |
| } |
| } |
| } |
| return urls; |
| } |
| |
| void FeedStream::LoadModelForTesting(const StreamType& stream_type, |
| std::unique_ptr<StreamModel> model) { |
| LoadModel(stream_type, std::move(model)); |
| } |
| offline_pages::TaskQueue& FeedStream::GetTaskQueueForTesting() { |
| return task_queue_; |
| } |
| |
| void FeedStream::OnTaskQueueIsIdle() { |
| if (idle_callback_) |
| idle_callback_.Run(); |
| } |
| |
| void FeedStream::SubscribedWebFeedCount( |
| base::OnceCallback<void(int)> callback) { |
| subscriptions().SubscribedWebFeedCount(std::move(callback)); |
| } |
| void FeedStream::RegisterFeedUserSettingsFieldTrial(std::string_view group) { |
| delegate_->RegisterFeedUserSettingsFieldTrial(group); |
| } |
| |
| void FeedStream::SetIdleCallbackForTesting( |
| base::RepeatingClosure idle_callback) { |
| idle_callback_ = idle_callback; |
| } |
| |
| void FeedStream::OnStoreChange(StreamModel::StoreUpdate update) { |
| if (!update.operations.empty()) { |
| DCHECK(!update.update_request); |
| store_->WriteOperations(update.stream_type, update.sequence_number, |
| update.operations); |
| } else if (update.update_request) { |
| if (update.overwrite_stream_data) { |
| DCHECK_EQ(update.sequence_number, 0); |
| store_->OverwriteStream(update.stream_type, |
| std::move(update.update_request), |
| base::DoNothing()); |
| } else { |
| store_->SaveStreamUpdate(update.stream_type, update.sequence_number, |
| std::move(update.update_request), |
| base::DoNothing()); |
| } |
| } |
| } |
| |
| LaunchResult FeedStream::ShouldAttemptLoad(const StreamType& stream_type, |
| LoadType load_type, |
| bool model_loading) { |
| Stream& stream = GetStream(stream_type); |
| if (load_type == LoadType::kInitialLoad || |
| load_type == LoadType::kBackgroundRefresh) { |
| // For initial load or background refresh, the model should not be loaded |
| // or in the process of being loaded. Because |ShouldAttemptLoad()| is used |
| // both before and during the load process, we need to ignore this check |
| // when |model_loading| is true. |
| if (stream.model || (!model_loading && stream.model_loading_in_progress)) { |
| return {LoadStreamStatus::kModelAlreadyLoaded, |
| feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED}; |
| } |
| } |
| |
| if (!IsArticlesListVisible()) { |
| return {LoadStreamStatus::kLoadNotAllowedArticlesListHidden, |
| feedwire::DiscoverLaunchResult::FEED_HIDDEN}; |
| } |
| |
| if (!IsFeedEnabledByEnterprisePolicy()) { |
| return {LoadStreamStatus::kLoadNotAllowedDisabledByEnterprisePolicy, |
| feedwire::DiscoverLaunchResult:: |
| INELIGIBLE_DISCOVER_DISABLED_BY_ENTERPRISE_POLICY}; |
| } |
| |
| if (!IsFeedEnabled()) { |
| return {LoadStreamStatus::kLoadNotAllowedDisabled, |
| feedwire::DiscoverLaunchResult::INELIGIBLE_DISCOVER_DISABLED}; |
| } |
| |
| if (!IsFeedEnabledByDse()) { |
| return { |
| LoadStreamStatus::kLoadNotAllowedDisabledByDse, |
| feedwire::DiscoverLaunchResult::INELIGIBLE_DISCOVER_DISABLED_BY_DSE}; |
| } |
| |
| if (!delegate_->IsEulaAccepted()) { |
| return {LoadStreamStatus::kLoadNotAllowedEulaNotAccepted, |
| feedwire::DiscoverLaunchResult::INELIGIBLE_EULA_NOT_ACCEPTED}; |
| } |
| |
| // Skip this check if metadata_ is not initialized. ShouldAttemptLoad() will |
| // be called again from within the LoadStreamTask, and then the metadata |
| // will be initialized. |
| if (metadata_populated_ && |
| delegate_->GetAccountInfo().gaia != GaiaId(metadata_.gaia())) { |
| return {LoadStreamStatus::kDataInStoreIsForAnotherUser, |
| feedwire::DiscoverLaunchResult::DATA_IN_STORE_IS_FOR_ANOTHER_USER}; |
| } |
| |
| return {LoadStreamStatus::kNoStatus, |
| feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED}; |
| } |
| |
| bool FeedStream::MissedLastRefresh(const StreamType& stream_type) { |
| RefreshTaskId task_id; |
| if (!stream_type.GetRefreshTaskId(task_id)) |
| return false; |
| RequestSchedule schedule = |
| feed::prefs::GetRequestSchedule(task_id, *profile_prefs_); |
| if (schedule.refresh_offsets.empty()) |
| return false; |
| base::Time scheduled_time = |
| schedule.anchor_time + schedule.refresh_offsets[0]; |
| return scheduled_time < base::Time::Now(); |
| } |
| |
| LaunchResult FeedStream::ShouldMakeFeedQueryRequest( |
| const StreamType& stream_type, |
| LoadType load_type, |
| bool consume_quota) { |
| Stream& stream = GetStream(stream_type); |
| if (load_type == LoadType::kLoadMore) { |
| // LoadMore requires a next page token. |
| if (!stream.model || stream.model->GetNextPageToken().empty()) { |
| return {LoadStreamStatus::kCannotLoadMoreNoNextPageToken, |
| feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED}; |
| } |
| } else if (load_type != LoadType::kManualRefresh) { |
| // Time has passed since calling |ShouldAttemptLoad()|, call it again to |
| // confirm we should still attempt loading. |
| const LaunchResult should_not_attempt_reason = |
| ShouldAttemptLoad(stream_type, load_type, /*model_loading=*/true); |
| if (should_not_attempt_reason.load_stream_status != |
| LoadStreamStatus::kNoStatus) { |
| return should_not_attempt_reason; |
| } |
| } |
| |
| if (delegate_->IsOffline()) { |
| return {LoadStreamStatus::kCannotLoadFromNetworkOffline, |
| feedwire::DiscoverLaunchResult::NO_CARDS_REQUEST_ERROR_NO_INTERNET}; |
| } |
| |
| NetworkRequestType request_type; |
| switch (stream_type.GetKind()) { |
| case StreamKind::kUnknown: |
| DLOG(ERROR) << "Unknown stream kind"; |
| [[fallthrough]]; |
| case StreamKind::kForYou: |
| request_type = (load_type != LoadType::kLoadMore) |
| ? NetworkRequestType::kFeedQuery |
| : NetworkRequestType::kNextPage; |
| break; |
| case StreamKind::kFollowing: |
| request_type = NetworkRequestType::kWebFeedListContents; |
| break; |
| case StreamKind::kSingleWebFeed: |
| request_type = NetworkRequestType::kSingleWebFeedListContents; |
| break; |
| } |
| |
| if (consume_quota && !request_throttler_.RequestQuota(request_type)) { |
| return {LoadStreamStatus::kCannotLoadFromNetworkThrottled, |
| feedwire::DiscoverLaunchResult::NO_CARDS_REQUEST_ERROR_OTHER}; |
| } |
| |
| return {LoadStreamStatus::kNoStatus, |
| feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED}; |
| } |
| |
| feedwire::ChromeSignInStatus::SignInStatus FeedStream::GetSignInStatus() const { |
| if (IsSignedIn()) { |
| return feedwire::ChromeSignInStatus::SIGNED_IN; |
| } |
| if (!IsSigninAllowed()) { |
| return feedwire::ChromeSignInStatus::SIGNIN_DISALLOWED_BY_CONFIG; |
| } |
| return feedwire::ChromeSignInStatus::NOT_SIGNED_IN; |
| } |
| |
| feedwire::DefaultSearchEngine::SearchEngine FeedStream::GetDefaultSearchEngine() |
| const { |
| const TemplateURL* template_url = |
| template_url_service_->GetDefaultSearchProvider(); |
| if (template_url) { |
| SearchEngineType engine_type = |
| template_url->GetEngineType(template_url_service_->search_terms_data()); |
| if (engine_type == SEARCH_ENGINE_GOOGLE) { |
| return feedwire::DefaultSearchEngine::ENGINE_GOOGLE; |
| } |
| } |
| return feedwire::DefaultSearchEngine::ENGINE_OTHER; |
| } |
| |
| RequestMetadata FeedStream::GetCommonRequestMetadata( |
| bool signed_in_request, |
| bool allow_expired_session_id) const { |
| RequestMetadata result; |
| result.chrome_info = chrome_info_; |
| result.display_metrics = delegate_->GetDisplayMetrics(); |
| result.language_tag = delegate_->GetLanguageTag(); |
| result.notice_card_acknowledged = |
| privacy_notice_card_tracker_.HasAcknowledgedNoticeCard(); |
| result.tab_group_enabled_state = delegate_->GetTabGroupEnabledState(); |
| |
| if (signed_in_request) { |
| result.client_instance_id = prefs::GetClientInstanceId(*profile_prefs_); |
| } else { |
| std::string session_id = GetSessionId(); |
| if (!session_id.empty() && |
| (allow_expired_session_id || |
| feedstore::GetSessionIdExpiryTime(metadata_) > base::Time::Now())) { |
| result.session_id = session_id; |
| } |
| } |
| result.followed_from_web_page_menu_count = |
| metadata_.followed_from_web_page_menu_count(); |
| |
| DCHECK(result.session_id.empty() || result.client_instance_id.empty()); |
| return result; |
| } |
| |
| RequestMetadata FeedStream::GetSignedInRequestMetadata() const { |
| return GetCommonRequestMetadata(/*signed_in_request =*/true, |
| /*allow_expired_session_id =*/true); |
| } |
| |
| RequestMetadata FeedStream::GetRequestMetadata(const StreamType& stream_type, |
| bool is_for_next_page) const { |
| const Stream* stream = FindStream(stream_type); |
| // TODO(crbug.com/40869569) handle null single web feed streams |
| DCHECK(stream); |
| RequestMetadata result; |
| if (is_for_next_page) { |
| // If we are continuing an existing feed, use whatever session continuity |
| // mechanism is currently associated with the stream: client-instance-id |
| // for signed-in feed, session_id token for signed-out. |
| DCHECK(stream->model); |
| result = GetCommonRequestMetadata(stream->model->signed_in(), |
| /*allow_expired_session_id =*/true); |
| } else { |
| // The request is for the first page of the feed. Use client_instance_id |
| // for signed in requests and session_id token (if any, and not expired) |
| // for signed-out. |
| result = GetCommonRequestMetadata(IsSignedIn(), |
| /*allow_expired_session_id =*/false); |
| } |
| |
| result.content_order = GetContentOrder(stream_type); |
| |
| const feedstore::Metadata::StreamMetadata* stream_metadata = |
| FindMetadataForStream(GetMetadata(), stream_type); |
| if (stream_metadata != nullptr) { |
| result.info_card_tracking_states = info_card_tracker_.GetAllStates( |
| stream_metadata->last_server_response_time_millis(), |
| stream_metadata->last_fetch_time_millis()); |
| } |
| // Set sign in status for request metadata |
| result.sign_in_status = GetSignInStatus(); |
| |
| result.default_search_engine = GetDefaultSearchEngine(); |
| |
| result.country = delegate_->GetCountry(); |
| |
| return result; |
| } |
| |
| void FeedStream::OnEulaAccepted() { |
| for (auto& item : streams_) { |
| if (!item.second.surfaces.empty()) { |
| item.second.surface_updater->launch_reliability_logger() |
| .LogFeedLaunchOtherStart(); |
| TriggerStreamLoad(item.second.type); |
| } |
| } |
| } |
| |
| void FeedStream::OnAllHistoryDeleted() { |
| // We don't really need to delete StreamType(StreamKind::kFollowing) data |
| // here, but clearing all data because it's easy. |
| ClearAll(); |
| } |
| |
| void FeedStream::OnCacheDataCleared() { |
| ClearAll(); |
| } |
| |
| void FeedStream::OnSignedIn() { |
| // On sign-in, turn off activity logging. This avoids the possibility that we |
| // send logs with the wrong user info attached, but may cause us to lose |
| // buffered events. |
| for (auto& item : streams_) { |
| item.second.is_activity_logging_enabled = false; |
| } |
| |
| ClearAll(); |
| } |
| |
| void FeedStream::OnSignedOut() { |
| // On sign-out, turn off activity logging. This avoids the possibility that we |
| // send logs with the wrong user info attached, but may cause us to lose |
| // buffered events. |
| for (auto& item : streams_) { |
| item.second.is_activity_logging_enabled = false; |
| } |
| |
| ClearAll(); |
| } |
| |
| void FeedStream::ExecuteRefreshTask(RefreshTaskId task_id) { |
| StreamType stream_type = StreamType::ForTaskId(task_id); |
| LoadStreamStatus do_not_attempt_reason = |
| ShouldAttemptLoad(stream_type, LoadType::kBackgroundRefresh) |
| .load_stream_status; |
| |
| RequestSchedule request_schedule = |
| feed::prefs::GetRequestSchedule(task_id, *profile_prefs_); |
| LoadType load_type = RequestScheduleTypeToLoadType(request_schedule.type); |
| |
| // If `do_not_attempt_reason` indicates the stream shouldn't be loaded, it's |
| // unlikely that criteria will change, so we skip rescheduling. |
| if (do_not_attempt_reason == LoadStreamStatus::kNoStatus || |
| do_not_attempt_reason == LoadStreamStatus::kModelAlreadyLoaded) { |
| // Schedule the next refresh attempt. If a new refresh schedule is returned |
| // through this refresh, it will be overwritten. |
| SetRequestSchedule(task_id, std::move(request_schedule)); |
| } |
| |
| if (do_not_attempt_reason != LoadStreamStatus::kNoStatus) { |
| BackgroundRefreshComplete( |
| LoadStreamTask::Result(stream_type, do_not_attempt_reason)); |
| return; |
| } |
| |
| LoadStreamTask::Options options; |
| options.stream_type = stream_type; |
| options.load_type = load_type; |
| options.refresh_even_when_not_stale = true; |
| task_queue_.AddTask(FROM_HERE, |
| std::make_unique<LoadStreamTask>( |
| options, this, |
| base::BindOnce(&FeedStream::BackgroundRefreshComplete, |
| base::Unretained(this)))); |
| } |
| |
| void FeedStream::BackgroundRefreshComplete(LoadStreamTask::Result result) { |
| metrics_reporter_->OnBackgroundRefresh(result.stream_type, |
| result.final_status); |
| |
| LoadTaskComplete(result); |
| |
| // Add prefetch images to task queue without waiting to finish |
| // since we treat them as best-effort. |
| if (result.stream_type.IsForYou()) |
| task_queue_.AddTask(FROM_HERE, std::make_unique<PrefetchImagesTask>(this)); |
| |
| RefreshTaskId task_id; |
| if (result.stream_type.GetRefreshTaskId(task_id)) { |
| refresh_task_scheduler_->RefreshTaskComplete(task_id); |
| } |
| } |
| |
| // Performs work that is necessary for both background and foreground load |
| // tasks. |
| void FeedStream::LoadTaskComplete(const LoadStreamTask::Result& result) { |
| if (delegate_->GetAccountInfo().gaia != GaiaId(metadata_.gaia())) { |
| ClearAll(); |
| return; |
| } |
| PopulateDebugStreamData(result, *profile_prefs_); |
| if (!result.content_ids.IsEmpty()) { |
| GetStream(result.stream_type).content_ids = result.content_ids; |
| } |
| if (result.loaded_new_content_from_network) { |
| SetStreamStale(result.stream_type, false); |
| if (result.stream_type.IsForYou()) { |
| UpdateExperiments(result.experiments); |
| CheckDuplicatedContentsOnRefresh(); |
| } |
| } |
| |
| MaybeNotifyHasUnreadContent(result.stream_type); |
| } |
| |
| bool FeedStream::HasUnreadContent(const StreamType& stream_type) { |
| Stream& stream = GetStream(stream_type); |
| if (stream.content_ids.IsEmpty()) |
| return false; |
| if (feedstore::GetViewContentIds(metadata_, stream_type) |
| .ContainsAllOf(stream.content_ids)) { |
| return false; |
| } |
| |
| // If there is currently a surface already viewing the content, update the |
| // ViewContentIds to whatever the current set is. This can happen if the |
| // surface already shown is refreshed. |
| if (stream.model && stream.surfaces.HasSurfaceShowingContent()) { |
| SetMetadata(SetStreamViewContentHashes(metadata_, stream_type, |
| stream.model->GetContentIds())); |
| return false; |
| } |
| return true; |
| } |
| |
| void FeedStream::IncrementFollowedFromWebPageMenuCount() { |
| feedstore::Metadata metadata = GetMetadata(); |
| metadata.set_followed_from_web_page_menu_count( |
| metadata.followed_from_web_page_menu_count() + 1); |
| SetMetadata(std::move(metadata)); |
| } |
| |
| void FeedStream::ClearAll() { |
| clear_all_in_progress_ = true; |
| task_queue_.AddTask(FROM_HERE, std::make_unique<ClearAllTask>(this)); |
| } |
| |
| void FeedStream::FinishClearAll() { |
| // Clear any experiments stored. |
| has_stored_data_.SetValue(false); |
| feed::prefs::SetExperiments({}, *profile_prefs_); |
| feed::prefs::ClearClientInstanceId(*profile_prefs_); |
| SetMetadata( |
| feedstore::MakeMetadata(delegate_->GetAccountInfo().gaia.ToString())); |
| |
| delegate_->ClearAll(); |
| |
| clear_all_in_progress_ = false; |
| |
| for (auto& item : streams_) { |
| if (!item.second.surfaces.empty()) { |
| item.second.surface_updater->launch_reliability_logger() |
| .LogFeedLaunchOtherStart(); |
| TriggerStreamLoad(item.second.type); |
| } |
| } |
| web_feed_subscription_coordinator_->ClearAllFinished(); |
| } |
| |
| void FeedStream::FinishClearStream(const StreamType& stream_type) { |
| Stream* stream = FindStream(stream_type); |
| if (stream && stream_type.IsSingleWebFeed()) { |
| streams_.erase(stream_type); |
| } |
| } |
| |
| ImageFetchId FeedStream::FetchImage( |
| const GURL& url, |
| base::OnceCallback<void(NetworkResponse)> callback) { |
| return image_fetcher_->Fetch(url, std::move(callback)); |
| } |
| |
| PersistentKeyValueStoreImpl& FeedStream::GetPersistentKeyValueStore() { |
| return *persistent_key_value_store_; |
| } |
| |
| void FeedStream::CancelImageFetch(ImageFetchId id) { |
| image_fetcher_->Cancel(id); |
| } |
| |
| void FeedStream::UploadAction( |
| feedwire::FeedAction action, |
| const LoggingParameters& logging_parameters, |
| bool upload_now, |
| base::OnceCallback<void(UploadActionsTask::Result)> callback) { |
| UploadActionsTask::WireAction wire_action(action, logging_parameters, |
| upload_now); |
| task_queue_.AddTask( |
| FROM_HERE, |
| std::make_unique<UploadActionsTask>( |
| std::move(wire_action), |
| // Pass unknown string type to skip logging upload actions events. |
| StreamType(), this, std::move(callback))); |
| } |
| |
| void FeedStream::LoadModel(const StreamType& stream_type, |
| std::unique_ptr<StreamModel> model) { |
| DCHECK(model); |
| Stream& stream = GetStream(stream_type); |
| DCHECK(!stream.model); |
| stream.model = std::move(model); |
| stream.model->SetStreamType(stream_type); |
| stream.model->SetStoreObserver(this); |
| |
| stream.content_ids = stream.model->GetContentIds(); |
| stream.surface_updater->SetModel(stream.model.get()); |
| ScheduleModelUnloadIfNoSurfacesAttached(stream_type); |
| MaybeNotifyHasUnreadContent(stream_type); |
| } |
| |
| void FeedStream::SetRequestSchedule(const StreamType& stream_type, |
| RequestSchedule schedule) { |
| RefreshTaskId task_id; |
| if (!stream_type.GetRefreshTaskId(task_id)) { |
| DLOG(ERROR) << "Ignoring request schedule for this stream: " << stream_type; |
| return; |
| } |
| SetRequestSchedule(task_id, std::move(schedule)); |
| } |
| |
| void FeedStream::SetRequestSchedule(RefreshTaskId task_id, |
| RequestSchedule schedule) { |
| const base::Time now = base::Time::Now(); |
| base::Time run_time = NextScheduledRequestTime(now, &schedule); |
| if (!run_time.is_null()) { |
| refresh_task_scheduler_->EnsureScheduled(task_id, run_time - now); |
| } else { |
| refresh_task_scheduler_->Cancel(task_id); |
| } |
| feed::prefs::SetRequestSchedule(task_id, schedule, *profile_prefs_); |
| } |
| |
| void FeedStream::UnloadModel(const StreamType& stream_type) { |
| // Note: This should only be called from a running Task, as some tasks assume |
| // the model remains loaded. |
| Stream* stream = FindStream(stream_type); |
| if (!stream) |
| return; |
| |
| if (stream->model) { |
| stream->surface_updater->SetModel(nullptr); |
| stream->model.reset(); |
| } |
| } |
| |
| void FeedStream::ClearStream(const StreamType& stream_type, |
| int sequence_number) { |
| Stream* stream = FindStream(stream_type); |
| if (!stream || stream->unload_on_detach_sequence_number != sequence_number) { |
| return; |
| } |
| task_queue_.AddTask(FROM_HERE, |
| std::make_unique<ClearStreamTask>(this, stream_type)); |
| } |
| |
| void FeedStream::UnloadModels() { |
| for (auto& item : streams_) { |
| UnloadModel(item.second.type); |
| } |
| } |
| |
| LaunchReliabilityLogger& FeedStream::GetLaunchReliabilityLogger( |
| const StreamType& stream_type) { |
| return GetStream(stream_type).surface_updater->launch_reliability_logger(); |
| } |
| |
| void FeedStream::UpdateUserProfileOnLinkClick( |
| const GURL& url, |
| const std::vector<int64_t>& entity_mids) { |
| user_actions_collector_.UpdateUserProfileOnLinkClick(url, entity_mids); |
| } |
| |
| void FeedStream::ReportOpenAction(const GURL& url, |
| SurfaceId surface_id, |
| const std::string& slice_id, |
| OpenActionType action_type) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return; |
| } |
| recent_feed_navigations_.insert(recent_feed_navigations_.begin(), url); |
| recent_feed_navigations_.resize( |
| std::min(kMaxRecentFeedNavigations, recent_feed_navigations_.size())); |
| |
| Stream& stream = GetStream(surface->GetStreamType()); |
| |
| int index = stream.surface_updater->GetSliceIndexFromSliceId(slice_id); |
| if (index < 0) |
| index = MetricsReporter::kUnknownCardIndex; |
| metrics_reporter_->OpenAction(surface->GetStreamType(), index, action_type); |
| |
| if (stream.model) { |
| privacy_notice_card_tracker_.OnOpenAction( |
| stream.model->FindContentId(ToContentRevision(slice_id))); |
| } |
| ScheduleFeedCloseRefresh(surface->GetStreamType()); |
| } |
| |
| void FeedStream::ReportOpenVisitComplete(SurfaceId /*surface_id*/, |
| base::TimeDelta visit_time) { |
| metrics_reporter_->OpenVisitComplete(visit_time); |
| } |
| |
| void FeedStream::ReportSliceViewed(SurfaceId surface_id, |
| const std::string& slice_id) { |
| Stream* stream = FindStream(surface_id); |
| if (!stream) { |
| return; |
| } |
| int index = stream->surface_updater->GetSliceIndexFromSliceId(slice_id); |
| if (index < 0) |
| return; |
| |
| if (!stream->model) { |
| return; |
| } |
| |
| metrics_reporter_->ContentSliceViewed(stream->type, index, |
| stream->model->GetContentList().size()); |
| |
| ContentRevision content_revision = ToContentRevision(slice_id); |
| |
| privacy_notice_card_tracker_.OnCardViewed( |
| stream->model->signed_in(), |
| stream->model->FindContentId(content_revision)); |
| |
| if (stream->type.IsForYou()) { |
| const feedstore::Content* content = |
| stream->model->FindContent(content_revision); |
| if (content) |
| AddViewedContentHashes(*content); |
| } |
| } |
| |
| // Notifies observers if 'HasUnreadContent' has changed for `stream_type`. |
| // Stream content has been seen if StreamData::content_hash == |
| // Metadata::StreamMetadata::view_content_hash. This should be called: |
| // when initial metadata is loaded, when the model is loaded, when a refresh is |
| // attempted, and when content is viewed. |
| void FeedStream::MaybeNotifyHasUnreadContent(const StreamType& stream_type) { |
| Stream& stream = GetStream(stream_type); |
| if (!metadata_populated_ || stream.model_loading_in_progress) |
| return; |
| |
| const bool has_new_content = HasUnreadContent(stream_type); |
| for (auto& o : stream.unread_content_notifiers) { |
| o.NotifyIfValueChanged(has_new_content); |
| } |
| } |
| |
| void FeedStream::ReportFeedViewed(SurfaceId surface_id) { |
| metrics_reporter_->FeedViewed(surface_id); |
| Stream* stream = FindStream(surface_id); |
| if (!stream) { |
| return; |
| } |
| |
| stream->surfaces.FeedViewed(surface_id); |
| MaybeNotifyHasUnreadContent(stream->type); |
| } |
| |
| void FeedStream::ReportPageLoaded(SurfaceId /*surface_id*/) { |
| metrics_reporter_->PageLoaded(); |
| } |
| void FeedStream::ReportStreamScrolled(SurfaceId surface_id, int distance_dp) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return; |
| } |
| metrics_reporter_->StreamScrolled(surface->GetStreamType(), distance_dp); |
| if (GetStream(surface->GetStreamType()).surfaces.HasSurfaceShowingContent()) { |
| ScheduleFeedCloseRefresh(surface->GetStreamType()); |
| } |
| } |
| void FeedStream::ReportStreamScrollStart(SurfaceId /*surface_id*/) { |
| metrics_reporter_->StreamScrollStart(); |
| } |
| void FeedStream::ReportOtherUserAction(SurfaceId surface_id, |
| FeedUserActionType action_type) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return; |
| } |
| metrics_reporter_->OtherUserAction(surface->GetStreamType(), action_type); |
| } |
| |
| void FeedStream::ReportOtherUserAction(const StreamType& stream_type, |
| FeedUserActionType action_type) { |
| metrics_reporter_->OtherUserAction(stream_type, action_type); |
| } |
| |
| void FeedStream::ReportOtherUserAction(FeedUserActionType action_type) { |
| metrics_reporter_->OtherUserAction(action_type); |
| } |
| |
| void FeedStream::ReportInfoCardTrackViewStarted(SurfaceId surface_id, |
| int info_card_type) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return; |
| } |
| metrics_reporter_->OnInfoCardTrackViewStarted(surface->GetStreamType(), |
| info_card_type); |
| } |
| |
| void FeedStream::ReportInfoCardViewed(SurfaceId surface_id, |
| int info_card_type, |
| int minimum_view_interval_seconds) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return; |
| } |
| metrics_reporter_->OnInfoCardViewed(surface->GetStreamType(), info_card_type); |
| info_card_tracker_.OnViewed(info_card_type, minimum_view_interval_seconds); |
| } |
| |
| void FeedStream::ReportInfoCardClicked(SurfaceId surface_id, |
| int info_card_type) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return; |
| } |
| metrics_reporter_->OnInfoCardClicked(surface->GetStreamType(), |
| info_card_type); |
| info_card_tracker_.OnClicked(info_card_type); |
| } |
| |
| void FeedStream::ReportInfoCardDismissedExplicitly(SurfaceId surface_id, |
| int info_card_type) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return; |
| } |
| metrics_reporter_->OnInfoCardDismissedExplicitly(surface->GetStreamType(), |
| info_card_type); |
| info_card_tracker_.OnDismissed(info_card_type); |
| } |
| |
| void FeedStream::ResetInfoCardStates(SurfaceId surface_id, int info_card_type) { |
| FeedStreamSurface* surface = FindSurface(surface_id); |
| if (!surface) { |
| return; |
| } |
| metrics_reporter_->OnInfoCardStateReset(surface->GetStreamType(), |
| info_card_type); |
| info_card_tracker_.ResetState(info_card_type); |
| } |
| |
| void FeedStream::ReportContentSliceVisibleTimeForGoodVisits( |
| SurfaceId surface_id, |
| base::TimeDelta elapsed) { |
| metrics_reporter_->ReportStableContentSliceVisibilityTimeForGoodVisits( |
| elapsed); |
| } |
| |
| void FeedStream::SetContentOrder(const StreamType& stream_type, |
| ContentOrder content_order) { |
| if (!stream_type.IsWebFeed()) { |
| DLOG(ERROR) << "SetContentOrder is not supported for this stream_type " |
| << stream_type; |
| return; |
| } |
| |
| ContentOrder current_order = GetValidWebFeedContentOrder(*profile_prefs_); |
| prefs::SetWebFeedContentOrder(*profile_prefs_, content_order); |
| if (current_order == content_order) |
| return; |
| |
| // Note that ForceRefreshTask clears stored content and forces a network |
| // refresh. It is possible to instead cache each ordering of the Feed |
| // separately, so that users who switch back and forth can do so more quickly |
| // and efficiently. However, there are some reasons to avoid this |
| // optimization: |
| // * we want content to be fresh, so this optimization would have limited |
| // effect. |
| // * interactions with the feed can modify content; in these cases we would |
| // want a full refresh. |
| // * it will add quite a bit of complexity to do it right |
| task_queue_.AddTask( |
| FROM_HERE, |
| std::make_unique<offline_pages::ClosureTask>(base::BindOnce( |
| &FeedStream::ForceRefreshTask, base::Unretained(this), stream_type))); |
| } |
| |
| ContentOrder FeedStream::GetContentOrder(const StreamType& stream_type) const { |
| if (!stream_type.IsWebFeed()) |
| return ContentOrder::kUnspecified; |
| return GetValidWebFeedContentOrder(*profile_prefs_); |
| } |
| |
| ContentOrder FeedStream::GetContentOrderFromPrefs( |
| const StreamType& stream_type) { |
| if (!stream_type.IsWebFeed()) { |
| NOTREACHED() |
| << "GetContentOrderFromPrefs is not supported for this stream_type " |
| << stream_type; |
| } |
| return prefs::GetWebFeedContentOrder(*profile_prefs_); |
| } |
| |
| void FeedStream::ScheduleFeedCloseRefresh(const StreamType& type) { |
| // To avoid causing jank, only schedule the refresh once every several |
| // minutes. |
| base::TimeTicks now = base::TimeTicks::Now(); |
| if (now - last_refresh_scheduled_on_interaction_time_ < base::Minutes(5)) |
| return; |
| |
| last_refresh_scheduled_on_interaction_time_ = now; |
| |
| base::TimeDelta delay = kFeedCloseRefreshDelay; |
| RequestSchedule schedule; |
| schedule.anchor_time = base::Time::Now(); |
| schedule.refresh_offsets = {delay, delay * 2, delay * 3}; |
| schedule.type = RequestSchedule::Type::kFeedCloseRefresh; |
| SetRequestSchedule(type, std::move(schedule)); |
| } |
| |
| void FeedStream::CheckDuplicatedContentsOnRefresh() { |
| StreamType stream_type = StreamType(StreamKind::kForYou); |
| Stream& stream = GetStream(stream_type); |
| feedstore::Metadata metadata = GetMetadata(); |
| feedstore::Metadata::StreamMetadata& stream_metadata = |
| MetadataForStream(metadata, stream_type); |
| |
| // Update the most recent list to include the currently view contents. |
| // This is done in the following steps: |
| // 1) If the currently viewed content hash is found in the most recent list, |
| // it means that the content was viewed in previous sessions. We need to |
| // remove the content hash from its old position and add it back later. |
| // 2) Append the currently viewed content hashes. |
| std::vector<uint32_t> most_recent_viewed_content_hashes( |
| metadata.most_recent_viewed_content_hashes().begin(), |
| metadata.most_recent_viewed_content_hashes().end()); |
| base::flat_set<uint32_t> viewed_content_hashes( |
| stream_metadata.viewed_content_hashes().begin(), |
| stream_metadata.viewed_content_hashes().end()); |
| std::erase_if(most_recent_viewed_content_hashes, |
| [&viewed_content_hashes](uint32_t x) { |
| return viewed_content_hashes.contains(x); |
| }); |
| most_recent_viewed_content_hashes.insert( |
| most_recent_viewed_content_hashes.end(), |
| stream_metadata.viewed_content_hashes().begin(), |
| stream_metadata.viewed_content_hashes().end()); |
| |
| // Check and report the content duplication. |
| if (!stream.content_ids.IsEmpty()) { |
| base::flat_set<uint32_t> most_recent_content_hashes( |
| most_recent_viewed_content_hashes.begin(), |
| most_recent_viewed_content_hashes.end()); |
| bool is_duplicated_at_pos_1 = false; |
| bool is_duplicated_at_pos_2 = false; |
| bool is_duplicated_at_pos_3 = false; |
| int duplicate_count_for_top_10 = 0; |
| int duplicate_count_for_all = 0; |
| int total_count = 0; |
| int pos = 0; |
| for (const feedstore::StreamContentHashList& hash_list : |
| stream.content_ids.original_hashes()) { |
| if (hash_list.hashes_size() == 0) |
| continue; |
| |
| // For position specific metrics, only the first item is checked for |
| // duplication if there are more than one items in a row, like carousel, |
| // collection or 2-column. |
| if (pos < 10 && |
| most_recent_content_hashes.contains(hash_list.hashes(0))) { |
| duplicate_count_for_top_10++; |
| if (pos == 0) { |
| is_duplicated_at_pos_1 = true; |
| } else if (pos == 1) { |
| is_duplicated_at_pos_2 = true; |
| } else if (pos == 2) { |
| is_duplicated_at_pos_3 = true; |
| } |
| } |
| |
| for (uint32_t hash : hash_list.hashes()) { |
| total_count++; |
| if (most_recent_content_hashes.contains(hash)) { |
| duplicate_count_for_all++; |
| } |
| } |
| |
| pos++; |
| } |
| |
| // Don't report the duplication metrics for the first time. |
| if (most_recent_viewed_content_hashes.size() > 0 && total_count > 0) { |
| metrics_reporter_->ReportContentDuplication( |
| is_duplicated_at_pos_1, is_duplicated_at_pos_2, |
| is_duplicated_at_pos_3, |
| static_cast<int>(duplicate_count_for_top_10 * 10), |
| static_cast<int>(100 * duplicate_count_for_all / total_count)); |
| } |
| } |
| |
| // Reset `viewed_content_hashes` after each refresh. |
| stream_metadata.clear_viewed_content_hashes(); |
| stream.viewed_content_hashes.clear(); |
| |
| // Cap the most recent list. |
| size_t max_size = static_cast<size_t>( |
| GetFeedConfig().max_most_recent_viewed_content_hashes); |
| if (most_recent_viewed_content_hashes.size() > max_size) { |
| most_recent_viewed_content_hashes.erase( |
| most_recent_viewed_content_hashes.begin(), |
| most_recent_viewed_content_hashes.begin() + |
| (most_recent_viewed_content_hashes.size() - max_size)); |
| } |
| metadata.mutable_most_recent_viewed_content_hashes()->Assign( |
| most_recent_viewed_content_hashes.begin(), |
| most_recent_viewed_content_hashes.end()); |
| |
| SetMetadata(metadata); |
| } |
| |
| void FeedStream::AddViewedContentHashes(const feedstore::Content& content) { |
| // Count only the 1st item in the collection. |
| if (content.prefetch_metadata_size() == 0) |
| return; |
| StreamType stream_type = StreamType(StreamKind::kForYou); |
| const auto& prefetch_metadata = content.prefetch_metadata(0); |
| int32_t content_hash = |
| feedstore::ContentHashFromPrefetchMetadata(prefetch_metadata); |
| if (!content_hash) |
| return; |
| Stream& stream = GetStream(stream_type); |
| if (!stream.viewed_content_hashes.contains(content_hash)) { |
| feedstore::Metadata metadata = GetMetadata(); |
| stream.viewed_content_hashes.insert(content_hash); |
| feedstore::Metadata::StreamMetadata& stream_metadata = |
| MetadataForStream(metadata, stream_type); |
| stream_metadata.add_viewed_content_hashes(content_hash); |
| SetMetadata(metadata); |
| } |
| } |
| |
| } // namespace feed |