// Copyright 2024 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/time/calendar_list_model.h"
#include <iterator>
#include <optional>
#include <string>
#include "ash/ash_export.h"
#include "ash/calendar/calendar_client.h"
#include "ash/calendar/calendar_controller.h"
#include "ash/public/cpp/session/session_observer.h"
#include "ash/shell.h"
#include "ash/system/time/calendar_event_fetch_types.h"
#include "ash/system/time/calendar_metrics.h"
#include "ash/system/time/calendar_utils.h"
#include "base/check_is_test.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "base/timer/timer.h"
#include "google_apis/calendar/calendar_api_response_types.h"
#include "google_apis/common/api_error_codes.h"
namespace {
// Compares two calendars by primary field first, then alphabetically by
// summary (i.e. Calendar name) if neither calendar is primary.
bool CompareByPrimaryAndSummary(
google_apis::calendar::SingleCalendar calendar_one,
google_apis::calendar::SingleCalendar calendar_two) {
if (calendar_one.primary()) {
return true;
}
if (calendar_two.primary()) {
return false;
}
return base::CompareCaseInsensitiveASCII(calendar_one.summary(),
calendar_two.summary()) < 0;
}
void FilterForSelectedCalendars(ash::CalendarList& calendars) {
calendars.remove_if([](google_apis::calendar::SingleCalendar calendar) {
return !calendar.selected();
});
}
} // namespace
namespace ash {
CalendarListModel::CalendarListModel()
: timeout_(nullptr), session_observer_(this) {}
CalendarListModel::~CalendarListModel() = default;
void CalendarListModel::OnSessionStateChanged(
session_manager::SessionState state) {
ClearCacheAndCancelFetch();
}
void CalendarListModel::OnActiveUserSessionChanged(
const AccountId& account_id) {
ClearCacheAndCancelFetch();
}
void CalendarListModel::AddObserver(Observer* observer) {
if (observer) {
observers_.AddObserver(observer);
}
}
void CalendarListModel::RemoveObserver(Observer* observer) {
if (observer) {
observers_.RemoveObserver(observer);
}
}
void CalendarListModel::FetchCalendars() {
if (!calendar_utils::ShouldFetchCalendarData()) {
return;
}
CancelFetch();
fetch_in_progress_ = true;
fetch_start_time_ = base::TimeTicks::Now();
CalendarClient* client = Shell::Get()->calendar_controller()->GetClient();
// Bail out early if there is no CalendarClient. This will be the case in
// most unit tests.
if (!client) {
CHECK_IS_TEST();
return;
}
cancel_closure_ = client->GetCalendarList(base::BindOnce(
&CalendarListModel::OnCalendarListFetched, weak_factory_.GetWeakPtr()));
CHECK(cancel_closure_);
timeout_.Start(FROM_HERE, calendar_utils::kCalendarDataFetchTimeout,
base::BindOnce(&CalendarListModel::OnCalendarListFetchTimeout,
weak_factory_.GetWeakPtr()));
}
void CalendarListModel::CancelFetch() {
timeout_.Stop();
if (cancel_closure_) {
std::move(cancel_closure_).Run();
}
}
CalendarList CalendarListModel::GetCachedCalendarList() {
// Since the calendar list is kept until it is replaced during a re-fetch
// (or removed during a session change), we check is_cached_ before returning
// the list.
if (get_is_cached()) {
return calendar_list_;
}
CalendarList empty_calendar_list;
return empty_calendar_list;
}
void CalendarListModel::OnCalendarListFetched(
google_apis::ApiErrorCode error,
std::unique_ptr<google_apis::calendar::CalendarList> calendars) {
// Cancel timeout timer unless it was previously stopped on Cancel.
if (error != google_apis::CANCELLED) {
timeout_.Stop();
}
calendar_metrics::RecordCalendarListFetchDuration(base::TimeTicks::Now() -
fetch_start_time_);
calendar_metrics::RecordCalendarListFetchErrorCode(error);
calendar_metrics::RecordCalendarListFetchTimeout(false);
if (error == google_apis::HTTP_SUCCESS) {
if (calendars && !calendars->items().empty()) {
ClearCachedCalendarList();
for (auto& calendar : calendars->items()) {
calendar_list_.push_back(*calendar.get());
}
FilterForSelectedCalendars(calendar_list_);
calendar_metrics::RecordTotalSelectedCalendars(calendar_list_.size());
// The ordering of the calendar list is not always consistent between
// API calls, so calendar lists are sorted to maintain consistency of
// which events are shown. The sorter also moves the primary calendar
// to the top of the list.
calendar_list_.sort(CompareByPrimaryAndSummary);
if (calendar_list_.size() > calendar_utils::kMultipleCalendarsLimit) {
calendar_list_.resize(calendar_utils::kMultipleCalendarsLimit);
}
is_cached_ = true;
}
}
// In case of error, we fallback to a previously cached calendar list if it
// exists. So we still notify observers of completion in all cases.
fetch_in_progress_ = false;
for (auto& observer : observers_) {
observer.OnCalendarListFetchComplete();
}
}
void CalendarListModel::OnCalendarListFetchTimeout() {
calendar_metrics::RecordCalendarListFetchTimeout(true);
fetch_in_progress_ = false;
for (auto& observer : observers_) {
observer.OnCalendarListFetchComplete();
}
}
void CalendarListModel::ClearCachedCalendarList() {
calendar_list_.clear();
is_cached_ = false;
}
void CalendarListModel::ClearCacheAndCancelFetch() {
CancelFetch();
fetch_in_progress_ = false;
ClearCachedCalendarList();
}
} // namespace ash