| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/system/scheduled_feature/schedule_utils.h" |
| |
| #include <sstream> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/check.h" |
| #include "base/logging.h" |
| #include "base/notreached.h" |
| |
| namespace ash::schedule_utils { |
| |
| namespace { |
| |
| constexpr base::TimeDelta kOneDay = base::Days(1); |
| |
| // Pairs together a `ScheduleCheckpoint` and the time at which it's |
| // hit. |
| struct Slot { |
| ScheduleCheckpoint checkpoint; |
| base::Time time; |
| }; |
| |
| // For debugging purposes only. |
| std::string ToString(const std::vector<Slot>& schedule) { |
| std::stringstream ss; |
| ss << std::endl; |
| for (const Slot& slot : schedule) { |
| ss << static_cast<int>(slot.checkpoint) << ": " << slot.time << std::endl; |
| } |
| return ss.str(); |
| } |
| |
| // Working with null and infinite `base::Time` instances are invalid and cause |
| // undue complexity to account for. They should never be provided by the caller. |
| bool IsValidTimestamp(const base::Time t) { |
| return !t.is_null() && !t.is_inf(); |
| } |
| |
| // The returned vector has one `Slot` per `ScheduleCheckpoint` and is |
| // sorted by `Slot::time`. The time at which `Slot` <i> ends is by definition |
| // `Slot` <i + 1>'s `time`. Also note that: |
| // * The schedule is cyclic. The next `Slot` after the last one is the first |
| // `Slot`. |
| // * The schedule is guaranteed to be centered around "now": |
| // * `schedule[0].time` <= `now` < `schedule[0].time + kOneDay` |
| // * `schedule[0].time` <= `schedule[i].time` < `schedule[0].time + kOneDay` |
| // for all indices <i> in the returned `schedule`. |
| std::vector<Slot> BuildSchedule(const base::Time now, |
| base::Time start_time, |
| base::Time end_time, |
| const ScheduleType schedule_type) { |
| DCHECK(!now.is_null()); |
| // The `schedule` could theoretically start with any checkpoint because it's |
| // cyclic. `end_time` has been picked arbitrarily since it's easiest in the |
| // case of a `kSunsetToSunrise` to set the rest of the checkpoints relative to |
| // sunrise (`end_time` for that `ScheduleType`). |
| // |
| // `end_time` must first be shifted by a whole number of days such that |
| // `end_time` <= `now` < `end_time + kOneDay`. |
| // |
| // Example with `schedule_type` == `kSunsetToSunrise`: |
| // Start (sunset): 6:00 PM, End (sunrise): 6:00 AM, Now: 3:00 AM |
| // |
| // 3:00 6:00 18:00 |
| // <---------------------------------- + ----- + --------------- + -----> |
| // | | | |
| // now end_time start_time |
| const base::TimeDelta amount_to_advance_end_time = |
| (now - end_time).FloorToMultiple(kOneDay); |
| end_time += amount_to_advance_end_time; |
| // 6:00 3:00 18:00 |
| // <-- + ----------------------------- + ---------------------- + -----> |
| // | | | |
| // end_time now start_time |
| // (previous day) |
| |
| // Shift `start_time` such that |
| // `end_time` <= `start_time` < `end_time + kOneDay`. |
| start_time = ShiftWithinOneDayFrom(end_time, start_time); |
| // 6:00 18:00 3:00 6:00 |
| // <-- + ----------------- + --------- + ----- + ----------------------> |
| // | | | | |
| // end_time start_time now end_time |
| // (previous day) (current day) |
| |
| std::vector<Slot> schedule; |
| switch (schedule_type) { |
| case ScheduleType::kCustom: |
| schedule.push_back({ScheduleCheckpoint::kDisabled, end_time}); |
| schedule.push_back({ScheduleCheckpoint::kEnabled, start_time}); |
| break; |
| case ScheduleType::kSunsetToSunrise: { |
| const base::TimeDelta daylight_duration = start_time - end_time; |
| DCHECK_GE(daylight_duration, base::TimeDelta()); |
| schedule.push_back({ScheduleCheckpoint::kSunrise, end_time}); |
| schedule.push_back( |
| {ScheduleCheckpoint::kMorning, end_time + daylight_duration / 3}); |
| schedule.push_back({ScheduleCheckpoint::kLateAfternoon, |
| end_time + daylight_duration * 5 / 6}); |
| schedule.push_back({ScheduleCheckpoint::kSunset, start_time}); |
| break; |
| } |
| case ScheduleType::kNone: |
| NOTREACHED() << "kNone ScheduleType does not support any automatic " |
| "feature changes"; |
| } |
| // 6:00 10:00 16:00 18:00 3:00 6:00 |
| // <-- + --- + ----- + --- + ---------- + ----- + ----------------------> |
| // | | | | | | |
| // end_time morning late sunset now end_time |
| // (previous day) afternoon (current day) |
| DVLOG(1) << "Schedule: " << ToString(schedule); |
| return schedule; |
| } |
| |
| // Accounts for the fact that `schedule` is cyclic: When `current_idx` |
| // refers to the last `Slot`, the next `Slot` is actually the first `Slot` with |
| // its timestamp advanced by one day. |
| Slot GetNextSlot(const size_t current_idx, const std::vector<Slot>& schedule) { |
| DCHECK(!schedule.empty()); |
| DCHECK_LT(current_idx, schedule.size()); |
| for (size_t next_idx = current_idx + 1; next_idx < schedule.size(); |
| ++next_idx) { |
| // Some extremely rare corner cases where the next `Slot`'s time could be |
| // exactly equal to the current `Slot` instead of greater than it: |
| // * Sunrise and sunset are exactly the same time in a geolocation where |
| // there is literally no night or no daylight. |
| // * Sunrise and sunset are a couple microseconds apart, leaving |
| // `base::Time` without enough resolution to fit morning and afternoon |
| // between them at unique times. |
| // Therefore, this iterates from the current `Slot` until the next `Slot` is |
| // found with a greater time. |
| if (schedule[next_idx].time > schedule[current_idx].time) { |
| return schedule[next_idx]; |
| } |
| } |
| return {schedule.front().checkpoint, schedule.front().time + kOneDay}; |
| } |
| |
| } // namespace |
| |
| Position GetCurrentPosition(const base::Time now, |
| const base::Time start_time, |
| const base::Time end_time, |
| const ScheduleType schedule_type) { |
| CHECK(IsValidTimestamp(now)); |
| CHECK(IsValidTimestamp(start_time)); |
| CHECK(IsValidTimestamp(end_time)); |
| const std::vector<Slot> schedule = |
| BuildSchedule(now, start_time, end_time, schedule_type); |
| DCHECK(!schedule.empty()); |
| DCHECK_GE(now, schedule.front().time); |
| DCHECK_LT(now - schedule.front().time, kOneDay); |
| |
| for (size_t idx = 0; idx < schedule.size(); ++idx) { |
| const Slot next_slot = GetNextSlot(idx, schedule); |
| if (now >= schedule[idx].time && now < next_slot.time) { |
| return {schedule[idx].checkpoint, next_slot.checkpoint, |
| next_slot.time - now}; |
| } |
| } |
| NOTREACHED() << "Failed to find ScheduleCheckpoint for now=" << now |
| << " schedule:\n" |
| << ToString(schedule); |
| } |
| |
| base::Time ShiftWithinOneDayFrom(const base::Time origin, |
| const base::Time time_in) { |
| CHECK(IsValidTimestamp(origin)); |
| CHECK(IsValidTimestamp(time_in)); |
| const base::TimeDelta amount_to_advance_time_in = |
| (origin - time_in).CeilToMultiple(kOneDay); |
| return time_in + amount_to_advance_time_in; |
| } |
| |
| } // namespace ash::schedule_utils |