#include "ash/system/time/calendar_model.h"
#include <stdlib.h>
#include <cstddef>
#include <memory>
#include "ash/calendar/calendar_client.h"
#include "ash/calendar/calendar_controller.h"
#include "ash/constants/ash_features.h"
#include "ash/shell.h"
#include "ash/system/time/calendar_event_fetch.h"
#include "ash/system/time/calendar_utils.h"
#include "base/check.h"
#include "base/containers/contains.h"
#include "base/containers/fixed_flat_set.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "google_apis/calendar/calendar_api_response_types.h"
#include "google_apis/common/api_error_codes.h"
#undef ENABLED_VLOG_LEVEL
#define ENABLED_VLOG_LEVEL 1
namespace {
using ::google_apis::calendar::CalendarEvent;
constexpr auto kAllowedEventStatuses =
base::MakeFixedFlatSet<CalendarEvent::EventStatus>(
{CalendarEvent::EventStatus::kConfirmed,
CalendarEvent::EventStatus::kTentative});
constexpr auto kAllowedResponseStatuses =
base::MakeFixedFlatSet<CalendarEvent::ResponseStatus>(
{CalendarEvent::ResponseStatus::kAccepted,
CalendarEvent::ResponseStatus::kNeedsAction,
CalendarEvent::ResponseStatus::kTentative});
[[maybe_unused]] size_t GetEventMapSize(
const ash::CalendarModel::SingleMonthEventMap& event_map) {
size_t total_bytes = 0;
for (auto& event_list : event_map) {
total_bytes += sizeof(event_list);
for (auto& event : event_list.second) {
total_bytes += event.GetApproximateSizeInBytes();
}
}
return total_bytes;
}
auto SplitEventsIntoMultiDayAndSameDay(const ash::SingleDayEventList& list) {
std::list<CalendarEvent> multi_day_events;
std::list<CalendarEvent> same_day_events;
for (const CalendarEvent& event : list) {
if (event.all_day_event() || ash::calendar_utils::IsMultiDayEvent(&event))
multi_day_events.push_back(std::move(event));
else
same_day_events.push_back(std::move(event));
}
return std::make_tuple(std::move(multi_day_events),
std::move(same_day_events));
}
void SortByDateAscending(
std::list<google_apis::calendar::CalendarEvent>& events) {
events.sort([](google_apis::calendar::CalendarEvent& a,
google_apis::calendar::CalendarEvent& b) {
if (a.start_time().date_time() == b.start_time().date_time()) {
return a.end_time().date_time() < b.end_time().date_time();
}
return a.start_time().date_time() < b.start_time().date_time();
});
}
bool EventStartsInTenMins(const CalendarEvent& event,
const base::Time& now_local) {
const int start_time_difference_in_mins =
(ash::calendar_utils::GetStartTimeAdjusted(&event) - now_local)
.InMinutes();
return (0 <= start_time_difference_in_mins &&
start_time_difference_in_mins <= 10);
}
bool EventStartedLessThanOneHourAgo(const CalendarEvent& event,
const base::Time& now_local) {
const int start_time_difference_in_mins =
(ash::calendar_utils::GetStartTimeAdjusted(&event) - now_local)
.InMinutes();
const int end_time_difference_in_mins =
(ash::calendar_utils::GetEndTimeAdjusted(&event) - now_local).InMinutes();
return (0 <= end_time_difference_in_mins &&
0 > start_time_difference_in_mins &&
start_time_difference_in_mins >= -60);
}
auto FilterEventsStartingSoonOrRecentlyInProgress(
const ash::SingleDayEventList& list,
const base::Time& now_local) {
std::list<CalendarEvent> result;
for (const CalendarEvent& event : list) {
if (EventStartsInTenMins(event, now_local) ||
EventStartedLessThanOneHourAgo(event, now_local)) {
result.emplace_back(event);
}
}
return result;
}
}
namespace ash {
CalendarModel::CalendarModel() : session_observer_(this) {}
CalendarModel::~CalendarModel() = default;
void CalendarModel::OnSessionStateChanged(session_manager::SessionState state) {
ClearAllCachedEvents();
}
void CalendarModel::OnActiveUserSessionChanged(const AccountId& account_id) {
ClearAllCachedEvents();
}
void CalendarModel::AddObserver(Observer* observer) {
if (observer)
observers_.AddObserver(observer);
}
void CalendarModel::RemoveObserver(Observer* observer) {
if (observer)
observers_.RemoveObserver(observer);
}
void CalendarModel::PromoteMonth(base::Time start_of_month) {
if (non_prunable_months_.find(start_of_month) != non_prunable_months_.end())
return;
if (!mru_months_.empty() && mru_months_.front() == start_of_month)
return;
for (auto it = mru_months_.begin(); it != mru_months_.end(); ++it)
if (*it == start_of_month) {
mru_months_.erase(it);
break;
}
mru_months_.push_front(start_of_month);
}
void CalendarModel::AddNonPrunableMonth(const base::Time& month) {
if (base::Contains(non_prunable_months_, month))
return;
if (non_prunable_months_.size() < calendar_utils::kMaxNumNonPrunableMonths)
non_prunable_months_.emplace(month);
}
void CalendarModel::AddNonPrunableMonths(const std::set<base::Time>& months) {
for (auto& month : months)
AddNonPrunableMonth(month);
}
void CalendarModel::ClearAllCachedEvents() {
pending_fetches_.clear();
months_fetched_.clear();
non_prunable_months_.clear();
mru_months_.clear();
event_months_.clear();
}
void CalendarModel::ClearAllPrunableEvents() {
pending_fetches_.clear();
for (auto& month : mru_months_) {
event_months_.erase(month);
months_fetched_.erase(month);
}
mru_months_.clear();
}
void CalendarModel::UploadLifetimeMetrics() {
base::UmaHistogramCounts100000(
"Ash.Calendar.FetchEvents.TotalCacheSizeMonths", event_months_.size());
}
void CalendarModel::FetchEvents(base::Time start_of_month) {
if (!calendar_utils::ShouldFetchEvents())
return;
CalendarClient* client = Shell::Get()->calendar_controller()->GetClient();
if (!client)
return;
if (non_prunable_months_.find(start_of_month) == non_prunable_months_.end() &&
months_fetched_.find(start_of_month) != months_fetched_.end()) {
PromoteMonth(start_of_month);
return;
}
pending_fetches_.erase(start_of_month);
pending_fetches_.emplace(
start_of_month,
std::make_unique<CalendarEventFetch>(
start_of_month,
base::BindRepeating(&CalendarModel::OnEventsFetched,
base::Unretained(this)),
base::BindRepeating(&CalendarModel::OnEventFetchFailedInternalError,
base::Unretained(this)),
nullptr));
}
void CalendarModel::CancelFetch(const base::Time& start_of_month) {
if (base::Contains(pending_fetches_, start_of_month)) {
pending_fetches_[start_of_month]->Cancel();
}
}
int CalendarModel::EventsNumberOfDay(base::Time day,
SingleDayEventList* events) {
const SingleDayEventList& list = FindEvents(day);
if (list.empty())
return 0;
if (events) {
DCHECK(events->empty());
*events = list;
}
return list.size();
}
void CalendarModel::OnEventsFetched(
base::Time start_of_month,
google_apis::ApiErrorCode error,
const google_apis::calendar::EventList* events) {
base::UmaHistogramSparse("Ash.Calendar.FetchEvents.Result", error);
if (error != google_apis::HTTP_SUCCESS) {
pending_fetches_.erase(start_of_month);
for (auto& observer : observers_)
observer.OnEventsFetched(kNever, start_of_month, events);
return;
}
PruneEventCache();
event_months_.erase(start_of_month);
if (!events || events->items().empty()) {
SingleMonthEventMap empty_event_map;
event_months_.emplace(start_of_month, empty_event_map);
PromoteMonth(start_of_month);
} else {
for (const auto& event : events->items()) {
if (calendar_utils::IsMultiDayEvent(event.get()))
InsertMultiDayEvent(event.get(), start_of_month);
else {
base::Time start_time_midnight =
calendar_utils::GetStartTimeMidnightAdjusted(event.get());
InsertEventInMonth(
event.get(),
calendar_utils::GetStartOfMonthUTC(start_time_midnight),
start_time_midnight);
}
}
}
for (auto& observer : observers_)
observer.OnEventsFetched(kSuccess, start_of_month, events);
months_fetched_.emplace(start_of_month);
pending_fetches_.erase(start_of_month);
base::UmaHistogramCounts1M("Ash.Calendar.FetchEvents.SingleMonthSize",
GetEventMapSize(event_months_[start_of_month]));
}
void CalendarModel::OnEventFetchFailedInternalError(
base::Time start_of_month,
CalendarEventFetchInternalErrorCode error) {
pending_fetches_.erase(start_of_month);
for (auto& observer : observers_)
observer.OnTimeout(start_of_month);
}
bool CalendarModel::ShouldInsertEvent(const CalendarEvent* event) const {
if (!event)
return false;
return base::Contains(kAllowedEventStatuses, event->status()) &&
base::Contains(kAllowedResponseStatuses,
event->self_response_status());
}
void CalendarModel::InsertMultiDayEvent(
const google_apis::calendar::CalendarEvent* event,
const base::Time start_of_month) {
DCHECK(event);
if (event->all_day_event()) {
auto current_day_utc = calendar_utils::GetMaxTime(
start_of_month, event->start_time().date_time())
.UTCMidnight();
base::Time start_of_next_month =
calendar_utils::GetStartOfNextMonthUTC(current_day_utc);
auto last_day_utc = calendar_utils::GetMinTime(
start_of_next_month, event->end_time().date_time())
.UTCMidnight();
while (current_day_utc < last_day_utc) {
InsertEventInMonth(event, start_of_month, current_day_utc);
current_day_utc = calendar_utils::GetNextDayMidnight(current_day_utc);
}
return;
}
base::Time start_time_midnight =
calendar_utils::GetStartTimeMidnightAdjusted(event);
base::Time end_time_midnight =
calendar_utils::GetEndTimeMidnightAdjusted(event);
base::Time end_time = calendar_utils::GetEndTimeAdjusted(event);
base::Time current_day_midnight =
calendar_utils::GetMaxTime(start_of_month, start_time_midnight)
.UTCMidnight();
base::Time start_of_next_month =
calendar_utils::GetStartOfNextMonthUTC(current_day_midnight);
base::Time last_day_midnight =
calendar_utils::GetMinTime(start_of_next_month, end_time_midnight)
.UTCMidnight();
if (end_time == end_time_midnight)
last_day_midnight =
(last_day_midnight - calendar_utils::kDurationForGettingPreviousDay)
.UTCMidnight();
while (current_day_midnight <= last_day_midnight) {
InsertEventInMonth(event, start_of_month, current_day_midnight);
current_day_midnight =
calendar_utils::GetNextDayMidnight(current_day_midnight);
}
}
void CalendarModel::InsertEventInMonth(
const google_apis::calendar::CalendarEvent* event,
const base::Time start_of_month,
const base::Time start_time_midnight) {
DCHECK(event);
if (start_of_month != calendar_utils::GetStartOfMonthUTC(start_time_midnight))
return;
PromoteMonth(start_of_month);
auto it = event_months_.find(start_of_month);
if (it == event_months_.end()) {
SingleMonthEventMap month;
InsertEventInMonthEventList(month, event, start_time_midnight);
event_months_.emplace(start_of_month, month);
} else {
SingleMonthEventMap& month = it->second;
InsertEventInMonthEventList(month, event, start_time_midnight);
}
}
void CalendarModel::InsertEventInMonthEventList(
SingleMonthEventMap& month,
const google_apis::calendar::CalendarEvent* event,
const base::Time start_time_midnight) {
DCHECK(event);
if (!ShouldInsertEvent(event))
return;
auto it = month.find(start_time_midnight);
if (it == month.end()) {
SingleDayEventList list;
list.push_back(*event);
month.emplace(start_time_midnight, list);
} else {
SingleDayEventList& list = it->second;
list.push_back(*event);
}
}
SingleDayEventList CalendarModel::FindEvents(base::Time day) const {
SingleDayEventList event_list;
base::Time start_of_month = calendar_utils::GetStartOfMonthUTC(day);
auto it = event_months_.find(start_of_month);
if (it == event_months_.end()) {
return event_list;
}
base::Time midnight = day.UTCMidnight();
const SingleMonthEventMap& month = it->second;
auto it2 = month.find(midnight);
if (it2 == month.end()) {
return event_list;
}
auto events = it2->second;
SortByDateAscending(events);
return events;
}
std::tuple<SingleDayEventList, SingleDayEventList>
CalendarModel::FindEventsSplitByMultiDayAndSameDay(base::Time day) const {
return SplitEventsIntoMultiDayAndSameDay(FindEvents(day));
}
std::list<CalendarEvent> CalendarModel::FindUpcomingEvents(
base::Time now_local) const {
auto upcoming_events = FindEvents(now_local);
const base::Time now_in_ten_mins = now_local + base::Minutes(10);
if (now_in_ten_mins.UTCMidnight() != now_local.UTCMidnight()) {
auto tomorrows_events = FindEvents(now_local + base::Days(1));
upcoming_events.splice(upcoming_events.end(), tomorrows_events);
}
return FilterEventsStartingSoonOrRecentlyInProgress(upcoming_events,
now_local);
}
CalendarModel::FetchingStatus CalendarModel::FindFetchingStatus(
base::Time start_time) const {
if (!calendar_utils::ShouldFetchEvents())
return kNa;
if (pending_fetches_.count(start_time)) {
if (event_months_.count(start_time))
return kRefetching;
return kFetching;
}
if (event_months_.count(start_time))
return kSuccess;
return kNever;
}
void CalendarModel::RedistributeEvents() {
std::set<google_apis::calendar::CalendarEvent, CmpEvent>
to_be_redistributed_events;
for (auto& month : event_months_) {
SingleMonthEventMap& event_map = month.second;
for (auto& it : event_map) {
for (const google_apis::calendar::CalendarEvent& event : it.second) {
to_be_redistributed_events.insert(event);
}
}
}
event_months_.clear();
for (const google_apis::calendar::CalendarEvent& event :
to_be_redistributed_events) {
if (calendar_utils::IsMultiDayEvent(&event)) {
for (base::Time month : non_prunable_months_) {
InsertMultiDayEvent(&event, month);
}
} else {
base::Time start_time_midnight =
calendar_utils::GetStartTimeMidnightAdjusted(&event);
InsertEventInMonth(
&event, calendar_utils::GetStartOfMonthUTC(start_time_midnight),
start_time_midnight);
}
}
}
void CalendarModel::PruneEventCache() {
while (!mru_months_.empty() &&
mru_months_.size() > calendar_utils::kMaxNumPrunableMonths) {
base::Time lru_month = mru_months_.back();
pending_fetches_.erase(lru_month);
event_months_.erase(lru_month);
months_fetched_.erase(lru_month);
mru_months_.pop_back();
}
}
}