| // 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/scheduling.h" |
| |
| #include "base/json/values_util.h" |
| #include "base/time/time.h" |
| #include "base/types/cxx23_to_underlying.h" |
| #include "base/values.h" |
| #include "components/feed/core/v2/config.h" |
| #include "components/feed/core/v2/feedstore_util.h" |
| #include "components/feed/feed_feature_list.h" |
| |
| namespace feed { |
| namespace { |
| |
| base::Value::List VectorToList(const std::vector<base::TimeDelta>& values) { |
| base::Value::List result; |
| for (base::TimeDelta delta : values) { |
| result.Append(base::TimeDeltaToValue(delta)); |
| } |
| return result; |
| } |
| |
| bool ListToVector(const base::Value::List& value, |
| std::vector<base::TimeDelta>* result) { |
| for (const base::Value& entry : value) { |
| std::optional<base::TimeDelta> delta = base::ValueToTimeDelta(entry); |
| if (!delta) |
| return false; |
| result->push_back(*delta); |
| } |
| return true; |
| } |
| |
| base::TimeDelta GetThresholdTime(base::TimeDelta default_threshold, |
| base::TimeDelta server_threshold) { |
| if (server_threshold <= base::TimeDelta() || |
| server_threshold > default_threshold) { |
| return default_threshold; |
| } |
| return server_threshold; |
| } |
| |
| RequestSchedule::Type GetScheduleType(const base::Value* value) { |
| if (value && value->is_int()) { |
| int int_value = value->GetInt(); |
| if (int_value >= 0 && |
| int_value <= base::to_underlying(RequestSchedule::Type::kMaxValue)) { |
| return static_cast<RequestSchedule::Type>(int_value); |
| } |
| } |
| return RequestSchedule::Type::kScheduledRefresh; |
| } |
| |
| } // namespace |
| |
| RequestSchedule::RequestSchedule() = default; |
| RequestSchedule::~RequestSchedule() = default; |
| RequestSchedule::RequestSchedule(const RequestSchedule&) = default; |
| RequestSchedule& RequestSchedule::operator=(const RequestSchedule&) = default; |
| RequestSchedule::RequestSchedule(RequestSchedule&&) = default; |
| RequestSchedule& RequestSchedule::operator=(RequestSchedule&&) = default; |
| |
| base::Value::Dict RequestScheduleToDict(const RequestSchedule& schedule) { |
| base::Value::Dict result; |
| result.Set("anchor", base::TimeToValue(schedule.anchor_time)); |
| result.Set("offsets", VectorToList(schedule.refresh_offsets)); |
| result.Set("type", base::to_underlying(schedule.type)); |
| return result; |
| } |
| |
| RequestSchedule RequestScheduleFromDict(const base::Value::Dict& value) { |
| RequestSchedule result; |
| std::optional<base::Time> anchor = base::ValueToTime(value.Find("anchor")); |
| const base::Value::List* offsets = value.FindList("offsets"); |
| result.type = GetScheduleType(value.Find("type")); |
| |
| if (!anchor || !offsets || !ListToVector(*offsets, &result.refresh_offsets)) |
| return {}; |
| result.anchor_time = *anchor; |
| return result; |
| } |
| |
| base::Time NextScheduledRequestTime(base::Time now, RequestSchedule* schedule) { |
| if (schedule->refresh_offsets.empty()) |
| return now + GetFeedConfig().default_background_refresh_interval; |
| // Attempt to detect system clock changes. If |anchor_time| is in the future, |
| // or too far in the past, we reset |anchor_time| to now. |
| if (now < schedule->anchor_time || |
| schedule->anchor_time + base::Days(7) < now) { |
| schedule->anchor_time = now; |
| } |
| while (!schedule->refresh_offsets.empty()) { |
| base::Time request_time = |
| schedule->anchor_time + schedule->refresh_offsets[0]; |
| if (request_time <= now) { |
| // The schedule time is in the past. This can happen if the scheduled |
| // request already ran, or if the scheduled task was missed. Just ignore |
| // this fetch so that we don't risk multiple fetches at a time. |
| schedule->refresh_offsets.erase(schedule->refresh_offsets.begin()); |
| continue; |
| } |
| return request_time; |
| } |
| return now + GetFeedConfig().default_background_refresh_interval; |
| } |
| |
| bool ShouldWaitForNewContent(const feedstore::Metadata& metadata, |
| const StreamType& stream_type, |
| base::TimeDelta content_age, |
| bool is_web_feed_subscriber) { |
| const feedstore::Metadata::StreamMetadata* stream_metadata = |
| feedstore::FindMetadataForStream(metadata, stream_type); |
| if (stream_metadata && stream_metadata->is_known_stale()) |
| return true; |
| |
| base::TimeDelta staleness_threshold = GetFeedConfig().GetStalenessThreshold( |
| stream_type, is_web_feed_subscriber); |
| if (stream_metadata && stream_metadata->has_content_lifetime()) { |
| staleness_threshold = GetThresholdTime( |
| staleness_threshold, |
| base::Milliseconds(stream_metadata->content_lifetime().stale_age_ms())); |
| } |
| |
| return content_age > staleness_threshold; |
| } |
| |
| bool ContentInvalidFromAge(const feedstore::Metadata& metadata, |
| const StreamType& stream_type, |
| base::TimeDelta content_age, |
| bool is_web_feed_subscriber) { |
| const feedstore::Metadata::StreamMetadata* stream_metadata = |
| feedstore::FindMetadataForStream(metadata, stream_type); |
| |
| base::TimeDelta content_expiration_threshold = |
| GetFeedConfig().content_expiration_threshold; |
| if (base::FeatureList::IsEnabled(kWebFeedOnboarding) && |
| !is_web_feed_subscriber && stream_type.IsWebFeed()) { |
| content_expiration_threshold = |
| GetFeedConfig().subscriptionless_content_expiration_threshold; |
| } |
| if (stream_metadata && stream_metadata->has_content_lifetime()) { |
| content_expiration_threshold = GetThresholdTime( |
| content_expiration_threshold, |
| base::Milliseconds( |
| stream_metadata->content_lifetime().invalid_age_ms())); |
| } |
| |
| return content_age > content_expiration_threshold; |
| } |
| |
| } // namespace feed |