chromium/ash/system/time/calendar_list_model.cc

// 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