| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // 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 <set> |
| #include <string> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/containers/cxx20_erase.h" |
| #include "base/feature_list.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "base/time/time.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/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/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/feed_api.h" |
| #include "components/feed/core/v2/public/feed_stream_surface.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/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" |
| |
| namespace feed { |
| namespace { |
| constexpr size_t kMaxRecentFeedNavigations = 10; |
| |
| 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); |
| } |
| |
| } // namespace |
| |
| FeedStream::Stream::Stream() = default; |
| 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, |
| 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), |
| chrome_info_(chrome_info), |
| task_queue_(this), |
| request_throttler_(profile_prefs), |
| upload_criteria_(profile_prefs), |
| notice_card_tracker_(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; |
| |
| base::RepeatingClosure preference_change_callback = |
| base::BindRepeating(&FeedStream::EnabledPreferencesChanged, GetWeakPtr()); |
| enable_snippets_.Init(prefs::kEnableSnippets, profile_prefs, |
| preference_change_callback); |
| articles_list_visible_.Init(prefs::kArticlesListVisible, profile_prefs, |
| preference_change_callback); |
| has_stored_data_.Init(feed::prefs::kHasStoredData, profile_prefs); |
| |
| web_feed_subscription_coordinator_ = |
| std::make_unique<WebFeedSubscriptionCoordinator>(this); |
| |
| // Inserting this task first ensures that |store_| is initialized before |
| // it is used. |
| task_queue_.AddTask(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::GetStream(const StreamType& stream_type) { |
| auto iter = streams_.find(stream_type); |
| if (iter != streams_.end()) |
| return iter->second; |
| FeedStream::Stream& new_stream = streams_[stream_type]; |
| new_stream.type = stream_type; |
| new_stream.surface_updater = |
| std::make_unique<SurfaceUpdater>(metrics_reporter_); |
| return new_stream; |
| } |
| |
| StreamModel* FeedStream::GetModel(const StreamType& stream_type) { |
| Stream* stream = FindStream(stream_type); |
| return stream ? stream->model.get() : nullptr; |
| } |
| |
| feedwire::DiscoverLaunchResult FeedStream::TriggerStreamLoad( |
| const StreamType& stream_type) { |
| 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; |
| task_queue_.AddTask(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); |
| for (const feedstore::StreamData& stream_data : |
| result.startup_data.stream_data) { |
| StreamType stream_type = |
| feedstore::StreamTypeFromId(stream_data.stream_id()); |
| if (stream_type.IsValid()) { |
| GetStream(stream_type).content_ids = |
| feedstore::GetContentIds(stream_data); |
| } |
| } |
| metadata_populated_ = true; |
| 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::StreamTypeFromId(stream_data.stream_id()); |
| 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); |
| |
| 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_); |
| 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); |
| |
| int content_count = 0; |
| if (stream.model) { |
| content_count = stream.model->GetContentList().size(); |
| } |
| metrics_reporter_->OnLoadStream( |
| stream.type, result.load_from_store_status, result.final_status, |
| result.load_type == LoadType::kInitialLoad, |
| result.loaded_new_content_from_network, result.stored_content_age, |
| content_count, std::move(result.latencies)); |
| |
| UpdateIsActivityLoggingEnabled(result.stream_type); |
| 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 (base::FeatureList::IsEnabled(kWebFeed)) { |
| if (result.stream_type.IsForYou()) { |
| if (!HasUnreadContent(kWebFeedStream)) { |
| LoadStreamTask::Options options; |
| options.load_type = LoadType::kBackgroundRefresh; |
| options.stream_type = kWebFeedStream; |
| options.abort_if_unread_content = true; |
| task_queue_.AddTask(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(std::make_unique<UploadActionsTask>( |
| this, |
| /*launch_reliability_logger=*/nullptr, |
| base::BindOnce(&FeedStream::UploadActionsComplete, |
| base::Unretained(this)))); |
| } |
| } |
| |
| bool FeedStream::IsActivityLoggingEnabled(const StreamType& stream_type) const { |
| const Stream* stream = FindStream(stream_type); |
| return stream && stream->is_activity_logging_enabled && CanUploadActions(); |
| } |
| |
| void FeedStream::UpdateIsActivityLoggingEnabled(const StreamType& stream_type) { |
| Stream& stream = GetStream(stream_type); |
| stream.is_activity_logging_enabled = |
| stream.model && |
| ((stream.model->signed_in() && stream.model->logging_enabled()) || |
| (!stream.model->signed_in() && |
| GetFeedConfig().send_signed_out_session_logs)); |
| } |
| |
| 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()); |
| } |
| |
| 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); |
| SetMetadata(metadata); |
| } |
| } |
| |
| bool FeedStream::SetMetadata(absl::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_); |
| } |
| |
| void FeedStream::AttachSurface(FeedStreamSurface* surface) { |
| metrics_reporter_->SurfaceOpened(surface->GetStreamType(), |
| surface->GetSurfaceId()); |
| 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.surface_updater->SurfaceAdded( |
| surface, /*loading_not_allowed_reason=*/feedwire::DiscoverLaunchResult:: |
| CARDS_UNSPECIFIED); |
| surface->StreamUpdate(forced_stream_update_for_debugging_); |
| return; |
| } |
| |
| stream.surface_updater->SurfaceAdded( |
| surface, TriggerStreamLoad(surface->GetStreamType())); |
| |
| // Cancel any scheduled model unload task. |
| ++stream.unload_on_detach_sequence_number; |
| upload_criteria_.SurfaceOpenedOrClosed(); |
| } |
| |
| void FeedStream::DetachSurface(FeedStreamSurface* surface) { |
| Stream& stream = GetStream(surface->GetStreamType()); |
| metrics_reporter_->SurfaceClosed(surface->GetSurfaceId()); |
| stream.surface_updater->SurfaceRemoved(surface); |
| upload_criteria_.SurfaceOpenedOrClosed(); |
| ScheduleModelUnloadIfNoSurfacesAttached(surface->GetStreamType()); |
| } |
| |
| 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; |
| }; |
| base::EraseIf(stream.unread_content_notifiers, predicate); |
| } |
| |
| void FeedStream::ScheduleModelUnloadIfNoSurfacesAttached( |
| const StreamType& stream_type) { |
| Stream& stream = GetStream(stream_type); |
| if (stream.surface_updater->HasSurfaceAttached()) |
| return; |
| |
| base::ThreadTaskRunnerHandle::Get()->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(std::make_unique<offline_pages::ClosureTask>( |
| base::BindOnce(&FeedStream::UnloadModelIfNoSurfacesAttachedTask, |
| base::Unretained(this), stream_type))); |
| } |
| |
| void FeedStream::UnloadModelIfNoSurfacesAttachedTask( |
| const StreamType& stream_type) { |
| Stream& stream = GetStream(stream_type); |
| if (stream.surface_updater->HasSurfaceAttached()) |
| return; |
| UnloadModel(stream_type); |
| } |
| |
| bool FeedStream::IsArticlesListVisible() { |
| return articles_list_visible_.GetValue(); |
| } |
| |
| std::string FeedStream::GetClientInstanceId() const { |
| return prefs::GetClientInstanceId(*profile_prefs_); |
| } |
| |
| bool FeedStream::IsFeedEnabledByEnterprisePolicy() { |
| return enable_snippets_.GetValue(); |
| } |
| |
| bool FeedStream::IsEnabledAndVisible() { |
| return IsArticlesListVisible() && IsFeedEnabledByEnterprisePolicy(); |
| } |
| |
| 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(const FeedStreamSurface& surface, |
| base::OnceCallback<void(bool)> callback) { |
| 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); |
| } |
| |
| metrics_reporter_->OnLoadMoreBegin(surface.GetStreamType(), |
| surface.GetSurfaceId()); |
| 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(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); |
| |
| UpdateIsActivityLoggingEnabled(stream.type); |
| metrics_reporter_->OnLoadMore(result.final_status); |
| 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(const FeedStreamSurface& surface, |
| base::OnceCallback<void(bool)> callback) { |
| Stream& stream = GetStream(surface.GetStreamType()); |
| |
| // Bail out immediately if loading in progress. |
| if (stream.model_loading_in_progress) { |
| return std::move(callback).Run(false); |
| } |
| 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(std::make_unique<LoadStreamTask>( |
| options, this, |
| base::BindOnce(&FeedStream::StreamLoadComplete, |
| base::Unretained(this)))); |
| } |
| } |
| |
| void FeedStream::ExecuteOperations( |
| const StreamType& stream_type, |
| std::vector<feedstore::DataOperation> operations) { |
| StreamModel* model = GetModel(stream_type); |
| if (!model) { |
| DLOG(ERROR) << "Calling ExecuteOperations before the model is loaded"; |
| return; |
| } |
| // TODO(crbug.com/1227897): Convert this to a task. |
| return model->ExecuteOperations(std::move(operations)); |
| } |
| |
| EphemeralChangeId FeedStream::CreateEphemeralChange( |
| const StreamType& stream_type, |
| std::vector<feedstore::DataOperation> operations) { |
| StreamModel* model = GetModel(stream_type); |
| if (!model) { |
| DLOG(ERROR) << "Calling CreateEphemeralChange before the model is loaded"; |
| return {}; |
| } |
| metrics_reporter_->OtherUserAction(stream_type, |
| FeedUserActionType::kEphemeralChange); |
| return model->CreateEphemeralChange(std::move(operations)); |
| } |
| |
| EphemeralChangeId FeedStream::CreateEphemeralChangeFromPackedData( |
| const StreamType& stream_type, |
| base::StringPiece data) { |
| feedpacking::DismissData msg; |
| msg.ParseFromArray(data.data(), data.size()); |
| return CreateEphemeralChange(stream_type, |
| TranslateDismissData(base::Time::Now(), msg)); |
| } |
| |
| bool FeedStream::CommitEphemeralChange(const StreamType& stream_type, |
| EphemeralChangeId id) { |
| StreamModel* model = GetModel(stream_type); |
| if (!model) |
| return false; |
| metrics_reporter_->OtherUserAction( |
| stream_type, FeedUserActionType::kEphemeralChangeCommited); |
| return model->CommitEphemeralChange(id); |
| } |
| |
| bool FeedStream::RejectEphemeralChange(const StreamType& stream_type, |
| EphemeralChangeId id) { |
| StreamModel* model = GetModel(stream_type); |
| if (!model) |
| return false; |
| metrics_reporter_->OtherUserAction( |
| stream_type, FeedUserActionType::kEphemeralChangeRejected); |
| return model->RejectEphemeralChange(id); |
| } |
| |
| void FeedStream::ProcessThereAndBackAgain(base::StringPiece data) { |
| 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), /*upload_now=*/true, |
| base::BindOnce(&FeedStream::UploadActionsComplete, |
| base::Unretained(this))); |
| } |
| } |
| |
| void FeedStream::ProcessViewAction(base::StringPiece data) { |
| if (!CanLogViews()) { |
| return; |
| } |
| |
| feedwire::FeedAction msg; |
| msg.ParseFromArray(data.data(), data.size()); |
| UploadAction(std::move(msg), /*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 std::find(recent_feed_navigations_.begin(), |
| recent_feed_navigations_.end(), |
| url) != recent_feed_navigations_.end(); |
| } |
| |
| DebugStreamData FeedStream::GetDebugStreamData() { |
| return ::feed::prefs::GetDebugStreamData(*profile_prefs_); |
| } |
| |
| void FeedStream::ForceRefreshForDebugging() { |
| // Avoid request throttling for debug refreshes. |
| feed::prefs::SetThrottlerRequestCounts({}, *profile_prefs_); |
| task_queue_.AddTask( |
| std::make_unique<offline_pages::ClosureTask>(base::BindOnce( |
| &FeedStream::ForceRefreshForDebuggingTask, base::Unretained(this)))); |
| } |
| |
| void FeedStream::ForceRefreshForDebuggingTask() { |
| UnloadModel(kForYouStream); |
| store_->ClearStreamData(kForYouStream, base::DoNothing()); |
| GetStream(kForYouStream) |
| .surface_updater->launch_reliability_logger() |
| .LogFeedLaunchOtherStart(); |
| TriggerStreamLoad(kForYouStream); |
| |
| if (base::FeatureList::IsEnabled(kWebFeed)) { |
| UnloadModel(kWebFeedStream); |
| store_->ClearStreamData(kWebFeedStream, base::DoNothing()); |
| // WebFeed is refreshed automatically after for-you. |
| } |
| } |
| |
| std::string FeedStream::DumpStateForDebugging() { |
| Stream& stream = GetStream(kForYouStream); |
| 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); |
| return ss.str(); |
| } |
| |
| void FeedStream::SetForcedStreamUpdateForDebugging( |
| const feedui::StreamUpdate& stream_update) { |
| forced_stream_update_for_debugging_ = stream_update; |
| } |
| |
| base::Time FeedStream::GetLastFetchTime(const StreamType& stream_type) { |
| const base::Time fetch_time = |
| feedstore::GetLastFetchTime(metadata_, stream_type); |
| // Ignore impossible time values. |
| if (fetch_time > base::Time::Now()) |
| return base::Time(); |
| return fetch_time; |
| } |
| |
| 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::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 { |
| DCHECK(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 (!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_->GetSyncSignedInGaia() != 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}; |
| } |
| |
| if (consume_quota && |
| !request_throttler_.RequestQuota((load_type != LoadType::kLoadMore) |
| ? NetworkRequestType::kFeedQuery |
| : NetworkRequestType::kNextPage)) { |
| return {LoadStreamStatus::kCannotLoadFromNetworkThrottled, |
| feedwire::DiscoverLaunchResult::NO_CARDS_REQUEST_ERROR_OTHER}; |
| } |
| |
| return {LoadStreamStatus::kNoStatus, |
| feedwire::DiscoverLaunchResult::CARDS_UNSPECIFIED}; |
| } |
| |
| bool FeedStream::ShouldForceSignedOutFeedQueryRequest( |
| const StreamType& stream_type) const { |
| return stream_type.IsForYou() && |
| base::TimeTicks::Now() < signed_out_for_you_refreshes_until_; |
| } |
| |
| RequestMetadata FeedStream::GetRequestMetadata(const StreamType& stream_type, |
| bool is_for_next_page) const { |
| const Stream* stream = FindStream(stream_type); |
| DCHECK(stream); |
| RequestMetadata result; |
| result.chrome_info = chrome_info_; |
| result.display_metrics = delegate_->GetDisplayMetrics(); |
| result.language_tag = delegate_->GetLanguageTag(); |
| result.notice_card_acknowledged = |
| notice_card_tracker_.HasAcknowledgedNoticeCard(); |
| result.autoplay_enabled = delegate_->IsAutoplayEnabled(); |
| |
| 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); |
| if (stream->model->signed_in()) { |
| result.client_instance_id = GetClientInstanceId(); |
| } else { |
| result.session_id = GetSessionId(); |
| } |
| } 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. |
| if (IsSignedIn() && !ShouldForceSignedOutFeedQueryRequest(stream_type)) { |
| result.client_instance_id = GetClientInstanceId(); |
| } else if (!GetSessionId().empty() && feedstore::GetSessionIdExpiryTime( |
| metadata_) > base::Time::Now()) { |
| result.session_id = GetSessionId(); |
| } |
| } |
| |
| DCHECK(result.session_id.empty() || result.client_instance_id.empty()); |
| |
| return result; |
| } |
| |
| void FeedStream::OnEulaAccepted() { |
| for (auto& item : streams_) { |
| if (item.second.surface_updater->HasSurfaceAttached()) { |
| item.second.surface_updater->launch_reliability_logger() |
| .LogFeedLaunchOtherStart(); |
| TriggerStreamLoad(item.second.type); |
| } |
| } |
| } |
| |
| void FeedStream::OnAllHistoryDeleted() { |
| // Give sync the time to propagate the changes in history to the server. |
| // In the interim, only send signed-out FeedQuery requests. |
| signed_out_for_you_refreshes_until_ = |
| base::TimeTicks::Now() + kSuppressRefreshDuration; |
| // We don't really need to delete kWebFeedStream 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; |
| |
| // 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, feed::prefs::GetRequestSchedule(task_id, *profile_prefs_)); |
| } |
| |
| 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 = LoadType::kBackgroundRefresh; |
| options.refresh_even_when_not_stale = true; |
| task_queue_.AddTask(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(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_->GetSyncSignedInGaia() != metadata_.gaia()) { |
| ClearAll(); |
| return; |
| } |
| PopulateDebugStreamData(result, *profile_prefs_); |
| if (result.fetched_content_has_notice_card.has_value()) |
| feed::prefs::SetLastFetchHadNoticeCard( |
| *profile_prefs_, *result.fetched_content_has_notice_card); |
| 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); |
| } |
| |
| MaybeNotifyHasUnreadContent(result.stream_type); |
| } |
| |
| bool FeedStream::HasUnreadContent(const StreamType& stream_type) { |
| Stream& stream = GetStream(stream_type); |
| if (stream.content_ids.IsEmpty()) |
| return false; |
| return !feedstore::GetViewContentIds(metadata_, stream_type) |
| .ContainsAllOf(stream.content_ids); |
| } |
| |
| void FeedStream::ClearAll() { |
| metrics_reporter_->OnClearAll(base::Time::Now() - |
| GetLastFetchTime(kForYouStream)); |
| clear_all_in_progress_ = true; |
| task_queue_.AddTask(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_); |
| upload_criteria_.Clear(); |
| SetMetadata(feedstore::MakeMetadata(delegate_->GetSyncSignedInGaia())); |
| |
| delegate_->ClearAll(); |
| |
| clear_all_in_progress_ = false; |
| |
| for (auto& item : streams_) { |
| if (item.second.surface_updater->HasSurfaceAttached()) { |
| item.second.surface_updater->launch_reliability_logger() |
| .LogFeedLaunchOtherStart(); |
| TriggerStreamLoad(item.second.type); |
| } |
| } |
| web_feed_subscription_coordinator_->ClearAllFinished(); |
| } |
| |
| 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, |
| bool upload_now, |
| base::OnceCallback<void(UploadActionsTask::Result)> callback) { |
| if (!IsSignedIn()) { |
| DLOG(WARNING) |
| << "Called UploadActions while user is signed-out, dropping upload"; |
| return; |
| } |
| task_queue_.AddTask(std::make_unique<UploadActionsTask>( |
| std::move(action), upload_now, this, std::move(callback))); |
| } |
| |
| void FeedStream::LoadModel(const StreamType& stream_type, |
| std::unique_ptr<StreamModel> 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 || !stream->model) |
| return; |
| stream->surface_updater->SetModel(nullptr); |
| stream->model.reset(); |
| } |
| |
| 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::ReportOpenAction(const GURL& url, |
| const StreamType& stream_type, |
| const std::string& slice_id) { |
| recent_feed_navigations_.insert(recent_feed_navigations_.begin(), url); |
| recent_feed_navigations_.resize( |
| std::min(kMaxRecentFeedNavigations, recent_feed_navigations_.size())); |
| |
| Stream& stream = GetStream(stream_type); |
| |
| int index = stream.surface_updater->GetSliceIndexFromSliceId(slice_id); |
| if (index < 0) |
| index = MetricsReporter::kUnknownCardIndex; |
| metrics_reporter_->OpenAction(stream_type, index); |
| |
| if (stream.model) { |
| notice_card_tracker_.OnOpenAction( |
| stream.model->FindContentId(ToContentRevision(slice_id))); |
| } |
| } |
| void FeedStream::ReportOpenVisitComplete(base::TimeDelta visit_time) { |
| metrics_reporter_->OpenVisitComplete(visit_time); |
| } |
| void FeedStream::ReportOpenInNewTabAction(const GURL& url, |
| const StreamType& stream_type, |
| const std::string& slice_id) { |
| recent_feed_navigations_.insert(recent_feed_navigations_.begin(), url); |
| recent_feed_navigations_.resize( |
| std::min(kMaxRecentFeedNavigations, recent_feed_navigations_.size())); |
| |
| Stream& stream = GetStream(stream_type); |
| int index = stream.surface_updater->GetSliceIndexFromSliceId(slice_id); |
| if (index < 0) |
| index = MetricsReporter::kUnknownCardIndex; |
| metrics_reporter_->OpenInNewTabAction(stream_type, index); |
| |
| if (stream.model) { |
| notice_card_tracker_.OnOpenAction( |
| stream.model->FindContentId(ToContentRevision(slice_id))); |
| } |
| } |
| |
| void FeedStream::ReportSliceViewed(SurfaceId surface_id, |
| const StreamType& stream_type, |
| const std::string& slice_id) { |
| Stream& stream = GetStream(stream_type); |
| int index = stream.surface_updater->GetSliceIndexFromSliceId(slice_id); |
| if (index < 0) |
| return; |
| |
| if (stream.model) { |
| if (SetMetadata(SetStreamViewContentIds(metadata_, stream_type, |
| stream.model->GetContentIds()))) { |
| MaybeNotifyHasUnreadContent(stream_type); |
| } |
| |
| metrics_reporter_->ContentSliceViewed( |
| stream_type, index, stream.model->GetContentList().size()); |
| } |
| if (stream.model) { |
| notice_card_tracker_.OnCardViewed( |
| stream.model->signed_in(), |
| stream.model->FindContentId(ToContentRevision(slice_id))); |
| } |
| } |
| |
| // TODO(crbug/1147237): Rename this method and related members? |
| bool FeedStream::CanUploadActions() const { |
| return upload_criteria_.CanUploadActions(); |
| } |
| |
| bool FeedStream::CanLogViews() const { |
| // TODO(crbug/1152592): Determine notice card behavior with web feeds. |
| return CanUploadActions(); |
| } |
| |
| // 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); |
| } |
| void FeedStream::ReportPageLoaded() { |
| metrics_reporter_->PageLoaded(); |
| } |
| void FeedStream::ReportStreamScrolled(const StreamType& stream_type, |
| int distance_dp) { |
| metrics_reporter_->StreamScrolled(stream_type, distance_dp); |
| } |
| void FeedStream::ReportStreamScrollStart() { |
| metrics_reporter_->StreamScrollStart(); |
| } |
| void FeedStream::ReportOtherUserAction(const StreamType& stream_type, |
| FeedUserActionType action_type) { |
| metrics_reporter_->OtherUserAction(stream_type, action_type); |
| } |
| |
| } // namespace feed |