chromium/ash/birch/birch_model.cc

// 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/birch/birch_model.h"

#include <algorithm>
#include <limits>
#include <memory>
#include <vector>

#include "ash/birch/birch_coral_provider.h"
#include "ash/birch/birch_data_provider.h"
#include "ash/birch/birch_icon_cache.h"
#include "ash/birch/birch_item.h"
#include "ash/birch/birch_item_remover.h"
#include "ash/birch/birch_ranker.h"
#include "ash/birch/birch_weather_provider.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "base/functional/callback_forward.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chromeos/ash/components/geolocation/simple_geolocation_provider.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"

namespace ash {

namespace {

constexpr base::TimeDelta kDataFetchPostLoginTimeoutInMs =
    base::Milliseconds(3000);
constexpr base::TimeDelta kDataFetchTimeoutInMs = base::Milliseconds(1000);

// Returns the pref service to use for Birch prefs.
PrefService* GetPrefService() {
  return Shell::Get()->session_controller()->GetPrimaryUserPrefService();
}

}  // namespace

template <typename T>
BirchModel::DataTypeInfo<T>::DataTypeInfo(const std::string& pref_name,
                                          const std::string& metric_suffix)
    : pref_name(pref_name), metric_suffix(metric_suffix) {}

template <typename T>
BirchModel::DataTypeInfo<T>::~DataTypeInfo() = default;

BirchModel::PendingRequest::PendingRequest() = default;

BirchModel::PendingRequest::~PendingRequest() = default;

BirchModel::BirchModel()
    : calendar_data_(prefs::kBirchUseCalendar, "Calendar"),
      attachment_data_(prefs::kBirchUseFileSuggest, "Attachment"),
      file_suggest_data_(prefs::kBirchUseFileSuggest, "File"),
      recent_tab_data_(prefs::kBirchUseChromeTabs, "Tab"),
      last_active_data_(prefs::kBirchUseChromeTabs, "LastActive"),
      most_visited_data_(prefs::kBirchUseChromeTabs, "MostVisited"),
      self_share_data_(prefs::kBirchUseChromeTabs, "SelfShare"),
      lost_media_data_(prefs::kBirchUseLostMedia, "LostMedia"),
      release_notes_data_(prefs::kBirchUseReleaseNotes, "ReleaseNotes"),
      weather_data_(prefs::kBirchUseWeather, "Weather"),
      coral_data_(prefs::kBirchUseCoral, "Coral"),
      icon_cache_(std::make_unique<BirchIconCache>()) {
  if (features::IsBirchWeatherEnabled()) {
    weather_provider_ = std::make_unique<BirchWeatherProvider>(this);
  }
  if (features::IsBirchCoralEnabled()) {
    coral_provider_ = std::make_unique<BirchCoralProvider>(this);
  }
  Shell::Get()->session_controller()->AddObserver(this);
  SimpleGeolocationProvider::GetInstance()->AddObserver(this);
}

BirchModel::~BirchModel() {
  SimpleGeolocationProvider::GetInstance()->RemoveObserver(this);
  Shell::Get()->session_controller()->RemoveObserver(this);
}

void BirchModel::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void BirchModel::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

// static
void BirchModel::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(prefs::kBirchUseCalendar, true);
  registry->RegisterBooleanPref(prefs::kBirchUseFileSuggest, true);
  registry->RegisterBooleanPref(prefs::kBirchUseChromeTabs, true);
  registry->RegisterBooleanPref(prefs::kBirchUseLostMedia, true);
  registry->RegisterBooleanPref(prefs::kBirchUseWeather, true);
  registry->RegisterBooleanPref(prefs::kBirchUseReleaseNotes, true);
  // TODO(yulunwu): Change this to false once there is a way to opt-in.
  registry->RegisterBooleanPref(prefs::kBirchUseCoral, true);
  // NOTE: If you add a pref here, also update birch_browsertest.cc and
  // birch_model_unittest.cc which have code that disables all prefs.
}

void BirchModel::SetCalendarItems(
    const std::vector<BirchCalendarItem>& calendar_items) {
  if (!GetPrefService()->GetBoolean(prefs::kBirchUseCalendar)) {
    // If the kBirchUseCalendar pref is disabled, but the kBirchUseFileSuggest
    // pref is enabled, the call to StartDataFetchIfNeeded(attachment...) for
    // calendar attachments will cause the calendar provider to receive a data
    // fetch request.
    return;
  }
  SetItems(calendar_data_, calendar_items, /*record_latency=*/true);
}

void BirchModel::SetAttachmentItems(
    const std::vector<BirchAttachmentItem>& attachment_items) {
  if (!GetPrefService()->GetBoolean(prefs::kBirchUseFileSuggest)) {
    // The fetch is controlled by the calendar prefs, but attachments are
    // considered file suggestions in the UI. Don't SetItems() so we don't
    // MaybeRespondToDataFetchRequest() for a data type that's disabled by pref.
    return;
  }
  // There is no separate latency measurement for attachments because they come
  // from the calendar provider.
  SetItems(attachment_data_, attachment_items, /*record_latency=*/false);
}

void BirchModel::SetFileSuggestItems(
    const std::vector<BirchFileItem>& file_suggest_items) {
  SetItems(file_suggest_data_, file_suggest_items,
           /*record_latency=*/true);
}

void BirchModel::SetRecentTabItems(
    const std::vector<BirchTabItem>& recent_tab_items) {
  SetItems(recent_tab_data_, recent_tab_items, /*record_latency=*/true);
}

void BirchModel::SetLastActiveItems(
    const std::vector<BirchLastActiveItem>& items) {
  SetItems(last_active_data_, items, /*record_latency=*/true);
}

void BirchModel::SetMostVisitedItems(
    const std::vector<BirchMostVisitedItem>& items) {
  SetItems(most_visited_data_, items, /*record_latency=*/true);
}

void BirchModel::SetSelfShareItems(
    const std::vector<BirchSelfShareItem>& self_share_items) {
  SetItems(self_share_data_, self_share_items, /*record_latency=*/true);
}

void BirchModel::SetLostMediaItems(
    const std::vector<BirchLostMediaItem>& lost_media_items) {
  SetItems(lost_media_data_, lost_media_items, /*record_latency=*/true);
}

void BirchModel::SetWeatherItems(
    const std::vector<BirchWeatherItem>& weather_items) {
  SetItems(weather_data_, weather_items, /*record_latency=*/true);
}

void BirchModel::SetReleaseNotesItems(
    const std::vector<BirchReleaseNotesItem>& release_notes_items) {
  SetItems(release_notes_data_, release_notes_items,
           /*record_latency=*/true);
}

void BirchModel::SetCoralItems(const std::vector<BirchCoralItem>& coral_items) {
  SetItems(coral_data_, coral_items, /*record_latency=*/true);
}

void BirchModel::SetClientAndInit(BirchClient* client) {
  birch_client_ = client;

  if (birch_client_) {
    // `BirchItemRemover` calls `MaybeRespondToDataFetchRequest` once it
    // has completed initializing, this way any data fetch requests which have
    // completed can be responded to.
    item_remover_ = std::make_unique<BirchItemRemover>(
        birch_client_->GetRemovedItemsFilePath(),
        /*on_init_callback=*/base::BindOnce(
            &BirchModel::MaybeRespondToDataFetchRequest,
            base::Unretained(this)));
    for (auto& observer : observers_) {
      observer.OnBirchClientSet();
    }
  } else {
    item_remover_.reset();
  }
}

template <typename T>
void BirchModel::StartDataFetchIfNeeded(DataTypeInfo<T>& data_info,
                                        BirchDataProvider* data_provider) {
  // If the data type is disabled by pref, no data fetch is expected, so treat
  // the data as fresh.
  if (!GetPrefService()->GetBoolean(data_info.pref_name)) {
    data_info.items.clear();
    data_info.is_fresh = true;
    return;
  }

  const bool model_fetch_in_progress = !pending_requests_.empty();
  if (model_fetch_in_progress) {
    // Clear update request if there is a data fetch.
    data_info.update_request = std::nullopt;
  } else if (!data_info.update_request) {
    // If no data fetch or update request is currently in progress, avoid
    // fetching data for this type when the pref was toggled but no items need
    // to be shown.
    return;
  }

  if (!data_provider) {
    return;
  }

  // TODO(b/336712820): Return early and avoid requesting a data fetch if a
  // fetch is in progress and it has been ongoing for less than some short
  // amount of time.

  data_info.is_fresh = false;
  data_info.fetch_start_time = GetNow();
  data_info.fetch_in_progress = true;
  data_provider->RequestBirchDataFetch();
}

void BirchModel::RequestBirchDataFetch(bool is_post_login,
                                       base::OnceClosure callback) {
  if (!Shell::Get()->session_controller()->IsUserPrimary()) {
    // Fetches are only supported for the primary user. Return with empty data.
    ClearAllItems();
    std::move(callback).Run();
    return;
  }

  PrefService* prefs = GetPrefService();
  if (!prefs) {
    std::move(callback).Run();
    return;
  }

  const bool model_fetch_in_progress = !pending_requests_.empty();

  size_t request_id = next_request_id_++;

  PendingRequest& request = pending_requests_[request_id];
  request.callback = std::move(callback);
  request.timer = std::make_unique<base::OneShotTimer>();
  request.timer->Start(
      FROM_HERE,
      is_post_login ? kDataFetchPostLoginTimeoutInMs : kDataFetchTimeoutInMs,
      base::BindOnce(&BirchModel::HandleRequestTimeout, base::Unretained(this),
                     request_id));

  if (model_fetch_in_progress) {
    return;
  }

  // Data for a type can only ever be marked fresh if its pref is disabled, or
  // if its data items are set. Start here by marking all data as not fresh to
  // avoid any preemptive request responses until we determine the initial
  // freshness values after all calls to `StartDataFetchIfNeeded()`.
  MarkDataNotFresh();

  is_post_login_fetch_ = is_post_login;
  fetch_start_time_ = GetNow();

  if (birch_client_) {
    StartDataFetchIfNeeded(calendar_data_,
                           birch_client_->GetCalendarProvider());
    StartDataFetchIfNeeded(attachment_data_,
                           birch_client_->GetCalendarProvider());
    StartDataFetchIfNeeded(file_suggest_data_,
                           birch_client_->GetFileSuggestProvider());
    StartDataFetchIfNeeded(recent_tab_data_,
                           birch_client_->GetRecentTabsProvider());
    StartDataFetchIfNeeded(last_active_data_,
                           birch_client_->GetLastActiveProvider());
    StartDataFetchIfNeeded(most_visited_data_,
                           birch_client_->GetMostVisitedProvider());
    StartDataFetchIfNeeded(self_share_data_,
                           birch_client_->GetSelfShareProvider());
    StartDataFetchIfNeeded(lost_media_data_,
                           birch_client_->GetLostMediaProvider());
    StartDataFetchIfNeeded(release_notes_data_,
                           birch_client_->GetReleaseNotesProvider());
  }

  StartDataFetchIfNeeded(coral_data_, coral_provider_.get());
  StartDataFetchIfNeeded(weather_data_, weather_provider_.get());
  MaybeRespondToDataFetchRequest();
}

std::vector<std::unique_ptr<BirchItem>> BirchModel::GetAllItems() {
  if (!IsItemRemoverInitialized()) {
    // With no initialized item remover, return an empty list of items to avoid
    // returning items previously removed by the user.
    return {};
  }

  item_remover_->FilterRemovedCalendarItems(&calendar_data_.items);
  item_remover_->FilterRemovedAttachmentItems(&attachment_data_.items);
  item_remover_->FilterRemovedFileItems(&file_suggest_data_.items);
  item_remover_->FilterRemovedTabs(&recent_tab_data_.items);
  item_remover_->FilterRemovedLastActiveItems(&last_active_data_.items);
  item_remover_->FilterRemovedMostVisitedItems(&most_visited_data_.items);
  item_remover_->FilterRemovedSelfShareItems(&self_share_data_.items);

  BirchRanker ranker(GetNow());
  ranker.RankCalendarItems(&calendar_data_.items);
  ranker.RankAttachmentItems(&attachment_data_.items);
  ranker.RankFileSuggestItems(&file_suggest_data_.items);
  ranker.RankRecentTabItems(&recent_tab_data_.items);
  ranker.RankLastActiveItems(&last_active_data_.items);
  ranker.RankMostVisitedItems(&most_visited_data_.items);
  ranker.RankSelfShareItems(&self_share_data_.items);
  ranker.RankWeatherItems(&weather_data_.items);
  ranker.RankReleaseNotesItems(&release_notes_data_.items);
  ranker.RankLostMediaItems(&lost_media_data_.items);
  ranker.RankCoralItems(&coral_data_.items);

  // Avoid showing a duplicate last-active tab with a recent tab by removing
  // the item with the higher ranking.
  std::unordered_map<std::string, BirchLastActiveItem> url_to_last_active_item;
  for (auto& item : last_active_data_.items) {
    url_to_last_active_item.emplace(item.page_url().spec(), item);
  }
  std::erase_if(recent_tab_data_.items, [&url_to_last_active_item](
                                            const auto& recent_tab_item) {
    if (auto iter = url_to_last_active_item.find(recent_tab_item.url().spec());
        iter != url_to_last_active_item.end()) {
      if (recent_tab_item.ranking() > iter->second.ranking()) {
        return true;
      }
      url_to_last_active_item.erase(iter);
    }
    return false;
  });

  // Avoid showing a duplicate most-visited tab with a recent tab by removing
  // the item with the higher ranking.
  std::unordered_map<std::string, BirchMostVisitedItem>
      url_to_most_visited_item;
  for (auto& item : most_visited_data_.items) {
    url_to_most_visited_item.emplace(item.page_url().spec(), item);
  }
  std::erase_if(recent_tab_data_.items, [&url_to_most_visited_item](
                                            const auto& recent_tab_item) {
    if (auto iter = url_to_most_visited_item.find(recent_tab_item.url().spec());
        iter != url_to_most_visited_item.end()) {
      if (recent_tab_item.ranking() > iter->second.ranking()) {
        return true;
      }
      url_to_most_visited_item.erase(iter);
    }
    return false;
  });

  // Avoid showing a duplicate tab item by removing the item with the higher
  // ranking.
  std::unordered_map<std::string, BirchSelfShareItem> url_to_self_share_item;
  for (auto& item : self_share_data_.items) {
    url_to_self_share_item.emplace(item.url().spec(), item);
  }
  std::erase_if(recent_tab_data_.items, [&url_to_self_share_item](
                                            const auto& recent_tab_item) {
    if (auto iter = url_to_self_share_item.find(recent_tab_item.url().spec());
        iter != url_to_self_share_item.end()) {
      if (recent_tab_item.ranking() > iter->second.ranking()) {
        return true;
      }

      url_to_self_share_item.erase(iter);
    }
    return false;
  });

  // Avoid showing a duplicate file which is both an attachment and file
  // suggestion by erasing the item with the higher ranking.
  std::unordered_map<std::string, BirchAttachmentItem>
      file_id_to_attachment_item;
  for (auto& attachment : attachment_data_.items) {
    file_id_to_attachment_item.emplace(attachment.file_id(), attachment);
  }
  std::erase_if(file_suggest_data_.items, [&file_id_to_attachment_item](
                                              const auto& file_suggest_item) {
    if (file_id_to_attachment_item.contains(file_suggest_item.file_id())) {
      if (file_suggest_item.ranking() >
          file_id_to_attachment_item.at(file_suggest_item.file_id())
              .ranking()) {
        // Duplicate item with a higher ranked file suggest item. Erase the file
        // suggest item.
        return true;
      }
      // Duplicate item with a higher ranked attachment item. Erase the
      // attachment item.
      file_id_to_attachment_item.erase(file_suggest_item.file_id());
    }
    return false;
  });

  // Check prefs before returning items (the user might have toggled a pref
  // off while the data fetch was in-flight so we check again here).
  std::vector<std::unique_ptr<BirchItem>> all_items;
  PrefService* prefs = GetPrefService();
  if (prefs->GetBoolean(prefs::kBirchUseCalendar)) {
    for (auto& event : calendar_data_.items) {
      all_items.push_back(std::make_unique<BirchCalendarItem>(event));
    }
  }

  if (prefs->GetBoolean(prefs::kBirchUseFileSuggest)) {
    for (auto& event : file_id_to_attachment_item) {
      all_items.push_back(std::make_unique<BirchAttachmentItem>(event.second));
    }
    for (auto& file_suggestion : file_suggest_data_.items) {
      all_items.push_back(std::make_unique<BirchFileItem>(file_suggestion));
    }
  }

  if (prefs->GetBoolean(prefs::kBirchUseChromeTabs)) {
    for (auto& tab : recent_tab_data_.items) {
      all_items.push_back(std::make_unique<BirchTabItem>(tab));
    }
    if (ShouldShowLastActive()) {
      for (auto& item : url_to_last_active_item) {
        all_items.push_back(std::make_unique<BirchLastActiveItem>(item.second));
      }
      last_active_last_shown_ = GetNow();
    }
    if (ShouldShowMostVisited()) {
      for (auto& item : url_to_most_visited_item) {
        all_items.push_back(
            std::make_unique<BirchMostVisitedItem>(item.second));
      }
      most_visited_last_shown_ = GetNow();
    }
    for (auto& event : url_to_self_share_item) {
      all_items.push_back(std::make_unique<BirchSelfShareItem>(event.second));
    }
  }

  if (prefs->GetBoolean(prefs::kBirchUseLostMedia)) {
    for (auto& item : lost_media_data_.items) {
      all_items.push_back(std::make_unique<BirchLostMediaItem>(item));
    }
  }

  if (prefs->GetBoolean(prefs::kBirchUseWeather)) {
    for (auto& weather_item : weather_data_.items) {
      all_items.push_back(std::make_unique<BirchWeatherItem>(weather_item));
    }
  }

  if (prefs->GetBoolean(prefs::kBirchUseReleaseNotes)) {
    for (auto& release_notes_item : release_notes_data_.items) {
      all_items.push_back(
          std::make_unique<BirchReleaseNotesItem>(release_notes_item));
    }
  }
  if (prefs->GetBoolean(prefs::kBirchUseCoral)) {
    for (auto& coral_item : coral_data_.items) {
      all_items.push_back(std::make_unique<BirchCoralItem>(coral_item));
    }
  }

  // Sort items by ranking.
  std::sort(all_items.begin(), all_items.end(),
            [](const auto& item_a, const auto& item_b) {
              return item_a->ranking() < item_b->ranking();
            });

  return all_items;
}

std::vector<std::unique_ptr<BirchItem>> BirchModel::GetItemsForDisplay() {
  std::vector<std::unique_ptr<BirchItem>> results = GetAllItems();

  // Remove any items with no ranking, as these should not be shown.
  std::erase_if(results, [](const auto& item) {
    return item->ranking() == std::numeric_limits<float>::max();
  });

  // Remove any items with no title. These aren't useful to users.
  std::erase_if(results,
                [](const auto& item) { return item->title().empty(); });

  return results;
}

bool BirchModel::IsDataFresh() {
  PrefService* prefs = GetPrefService();
  if (!prefs) {
    return false;
  }

  bool is_birch_client_fresh =
      !birch_client_ ||
      (calendar_data_.is_fresh && attachment_data_.is_fresh &&
       file_suggest_data_.is_fresh && recent_tab_data_.is_fresh &&
       last_active_data_.is_fresh && most_visited_data_.is_fresh &&
       self_share_data_.is_fresh && lost_media_data_.is_fresh &&
       release_notes_data_.is_fresh);

  // Use the same logic for weather.
  bool is_weather_fresh = !weather_provider_ || weather_data_.is_fresh;
  return is_birch_client_fresh && is_weather_fresh;
}

void BirchModel::RemoveItem(BirchItem* item) {
  if (!IsItemRemoverInitialized()) {
    return;
  }
  // Record that the user hid a chip, with the type of the chip.
  base::UmaHistogramEnumeration("Ash.Birch.Chip.Hidden", item->GetType());

  item_remover_->RemoveItem(item);

  // File items must be removed from launcher suggestions.
  if (item->GetType() == BirchItemType::kFile) {
    auto* file_item = static_cast<BirchFileItem*>(item);
    if (birch_client_) {
      birch_client_->RemoveFileItemFromLauncher(file_item->file_path());
    }
  }
}

void BirchModel::SetLostMediaDataChangedCallback(
    LostMediaDataChangedCallback callback) {
  CHECK(birch_client_);

  auto* lost_media_data_provider = birch_client_->GetLostMediaProvider();
  if (lost_media_data_provider && !lost_media_data_changed_callback_) {
    lost_media_data_changed_callback_ = std::move(callback);
    lost_media_data_provider->SetDataProviderChangedCallback(
        base::BindRepeating(&BirchModel::OnLostMediaDataProviderChanged,
                            weak_ptr_factory_.GetWeakPtr()));
  }
}

void BirchModel::ResetLostMediaDataChangedCallback() {
  if (lost_media_data_changed_callback_) {
    lost_media_data_changed_callback_.Reset();
  }

  if (!birch_client_) {
    return;
  }

  if (auto* lost_media_data_provider = birch_client_->GetLostMediaProvider()) {
    lost_media_data_provider->ResetDataProviderChangedCallback();
  }
}

void BirchModel::OnActiveUserSessionChanged(const AccountId& account_id) {
  if (!has_active_user_session_changed_) {
    // This is the initial notification on signin.
    has_active_user_session_changed_ = true;
    InitPrefChangeRegistrars();
    RecordProviderHiddenHistograms();
    return;
  }

  // On multi-profile switch, first cancel any pending requests.
  pending_requests_.clear();

  // Clear the existing data and mark the data as not fresh.
  ClearAllItems();
  MarkDataNotFresh();
}

void BirchModel::OnGeolocationPermissionChanged(bool enabled) {
  // If geolocation permission is disabled, remove any cached weather data.
  if (!enabled) {
    weather_data_.items.clear();
    weather_data_.is_fresh = false;
  }
}

BirchDataProvider* BirchModel::GetWeatherProviderForTest() {
  return weather_provider_.get();
}

void BirchModel::OverrideWeatherProviderForTest(
    std::unique_ptr<BirchDataProvider> weather_provider) {
  CHECK(weather_provider_);
  weather_provider_ = std::move(weather_provider);
}

BirchDataProvider* BirchModel::GetCoralProviderForTest() {
  return coral_provider_.get();
}

void BirchModel::OverrideCoralProviderForTest(
    std::unique_ptr<BirchDataProvider> coral_provider) {
  CHECK(coral_provider_);
  coral_provider_ = std::move(coral_provider);
}

void BirchModel::OverrideClockForTest(base::Clock* clock) {
  clock_override_ = clock;
}

void BirchModel::SetDataFetchCallbackForTest(base::OnceClosure callback) {
  data_fetch_callback_for_test_ = std::move(callback);
}

template <typename T>
void BirchModel::SetItems(DataTypeInfo<T>& data_info,
                          const std::vector<T>& items,
                          bool record_latency) {
  if (data_info.fetch_in_progress) {
    base::UmaHistogramCounts100(
        "Ash.Birch.ResultsReturned." + data_info.metric_suffix, items.size());
    if (record_latency) {
      base::UmaHistogramTimes("Ash.Birch.Latency." + data_info.metric_suffix,
                              GetNow() - data_info.fetch_start_time);
    }
    data_info.fetch_in_progress = false;
  }
  data_info.items = std::move(items);
  data_info.is_fresh = true;
  MaybeRespondToDataFetchRequest();
  if (data_info.update_request) {
    std::move(data_info.update_request->callback).Run();
  }
}

void BirchModel::HandleRequestTimeout(size_t request_id) {
  auto request = pending_requests_.find(request_id);
  if (request == pending_requests_.end()) {
    return;
  }

  base::OnceClosure callback = std::move(request->second.callback);
  pending_requests_.erase(request);
  std::move(callback).Run();
}

void BirchModel::HandleLostMediaUpdateRequest() {
  if (lost_media_data_changed_callback_ && lost_media_data_.is_fresh) {
    lost_media_data_changed_callback_.Run(
        lost_media_data_.items.size()
            ? std::make_unique<BirchLostMediaItem>(lost_media_data_.items[0])
            : nullptr);
  }
  lost_media_data_.update_request = std::nullopt;
}

void BirchModel::MaybeRespondToDataFetchRequest() {
  if (!IsDataFresh() || !IsItemRemoverInitialized()) {
    return;
  }

  // Was this a real fetch being completed (rather than a provider supplying
  // data outside of a fetch)?
  bool was_model_fetch = !pending_requests_.empty();
  if (was_model_fetch) {
    // All data providers have replied, so compute total latency.
    base::TimeDelta latency = GetNow() - fetch_start_time_;
    if (is_post_login_fetch_) {
      base::UmaHistogramTimes("Ash.Birch.TotalLatencyPostLogin", latency);
    } else {
      base::UmaHistogramTimes("Ash.Birch.TotalLatency", latency);
    }
  }

  std::vector<base::OnceClosure> callbacks;
  for (auto& request : pending_requests_) {
    callbacks.push_back(std::move(request.second.callback));
  }
  pending_requests_.clear();

  for (auto& callback : callbacks) {
    std::move(callback).Run();
  }

  if (data_fetch_callback_for_test_) {
    std::move(data_fetch_callback_for_test_).Run();
    data_fetch_callback_for_test_.Reset();
  }
}

base::Time BirchModel::GetNow() const {
  if (clock_override_) {
    return clock_override_->Now();
  }
  return base::Time::Now();
}

void BirchModel::ClearAllItems() {
  calendar_data_.items.clear();
  attachment_data_.items.clear();
  file_suggest_data_.items.clear();
  recent_tab_data_.items.clear();
  last_active_data_.items.clear();
  most_visited_data_.items.clear();
  weather_data_.items.clear();
  self_share_data_.items.clear();
  lost_media_data_.items.clear();
  release_notes_data_.items.clear();
  coral_data_.items.clear();
}

void BirchModel::MarkDataNotFresh() {
  calendar_data_.is_fresh = false;
  attachment_data_.is_fresh = false;
  file_suggest_data_.is_fresh = false;
  recent_tab_data_.is_fresh = false;
  last_active_data_.is_fresh = false;
  most_visited_data_.is_fresh = false;
  weather_data_.is_fresh = false;
  self_share_data_.is_fresh = false;
  lost_media_data_.is_fresh = false;
  release_notes_data_.is_fresh = false;
  coral_data_.is_fresh = false;
}

void BirchModel::InitPrefChangeRegistrars() {
  PrefService* prefs = GetPrefService();
  if (!prefs) {
    return;
  }

  calendar_pref_registrar_.Init(prefs);
  calendar_pref_registrar_.Add(
      prefs::kBirchUseCalendar,
      base::BindRepeating(&BirchModel::OnCalendarPrefChanged,
                          base::Unretained(this)));
  file_suggest_pref_registrar_.Init(prefs);
  file_suggest_pref_registrar_.Add(
      prefs::kBirchUseFileSuggest,
      base::BindRepeating(&BirchModel::OnFileSuggestPrefChanged,
                          base::Unretained(this)));
  chrome_tabs_pref_registrar_.Init(prefs);
  chrome_tabs_pref_registrar_.Add(
      prefs::kBirchUseChromeTabs,
      base::BindRepeating(&BirchModel::OnChromeTabsPrefChanged,
                          base::Unretained(this)));
  lost_media_pref_registrar_.Init(prefs);
  lost_media_pref_registrar_.Add(
      prefs::kBirchUseLostMedia,
      base::BindRepeating(&BirchModel::OnLostMediaPrefChanged,
                          base::Unretained(this)));
  weather_pref_registrar_.Init(prefs);
  weather_pref_registrar_.Add(
      prefs::kBirchUseWeather,
      base::BindRepeating(&BirchModel::OnWeatherPrefChanged,
                          base::Unretained(this)));
  release_notes_pref_registrar_.Init(prefs);
  release_notes_pref_registrar_.Add(
      prefs::kBirchUseReleaseNotes,
      base::BindRepeating(&BirchModel::OnReleaseNotesPrefChanged,
                          base::Unretained(this)));
  coral_pref_registrar_.Init(prefs);
  coral_pref_registrar_.Add(prefs::kBirchUseCoral,
                            base::BindRepeating(&BirchModel::OnCoralPrefChanged,
                                                base::Unretained(this)));
}

void BirchModel::OnCalendarPrefChanged() {
  if (birch_client_) {
    StartDataFetchIfNeeded(calendar_data_,
                           birch_client_->GetCalendarProvider());
  }
}

void BirchModel::OnFileSuggestPrefChanged() {
  if (birch_client_) {
    StartDataFetchIfNeeded(file_suggest_data_,
                           birch_client_->GetFileSuggestProvider());
    // Event attachments are considered file suggestions.
    StartDataFetchIfNeeded(attachment_data_,
                           birch_client_->GetCalendarProvider());
  }
}

void BirchModel::OnChromeTabsPrefChanged() {
  if (birch_client_) {
    StartDataFetchIfNeeded(recent_tab_data_,
                           birch_client_->GetRecentTabsProvider());
    StartDataFetchIfNeeded(last_active_data_,
                           birch_client_->GetLastActiveProvider());
    StartDataFetchIfNeeded(most_visited_data_,
                           birch_client_->GetMostVisitedProvider());
    StartDataFetchIfNeeded(self_share_data_,
                           birch_client_->GetSelfShareProvider());
  }
}

void BirchModel::OnLostMediaPrefChanged() {
  if (birch_client_) {
    StartDataFetchIfNeeded(lost_media_data_,
                           birch_client_->GetLostMediaProvider());
  }
}

void BirchModel::OnCoralPrefChanged() {
  StartDataFetchIfNeeded(coral_data_, coral_provider_.get());
}

void BirchModel::OnWeatherPrefChanged() {
  StartDataFetchIfNeeded(weather_data_, weather_provider_.get());
}

void BirchModel::OnReleaseNotesPrefChanged() {
  if (birch_client_) {
    StartDataFetchIfNeeded(release_notes_data_,
                           birch_client_->GetReleaseNotesProvider());
  }
}

void BirchModel::RecordProviderHiddenHistograms() {
  PrefService* prefs = GetPrefService();
  if (!prefs) {
    return;
  }

  base::UmaHistogramBoolean("Ash.Birch.ProviderHidden.Calendar",
                            !prefs->GetBoolean(prefs::kBirchUseCalendar));
  base::UmaHistogramBoolean("Ash.Birch.ProviderHidden.FileSuggest",
                            !prefs->GetBoolean(prefs::kBirchUseFileSuggest));
  base::UmaHistogramBoolean("Ash.Birch.ProviderHidden.ChromeTabs",
                            !prefs->GetBoolean(prefs::kBirchUseChromeTabs));
  base::UmaHistogramBoolean("Ash.Birch.ProviderHidden.LostMedia",
                            !prefs->GetBoolean(prefs::kBirchUseLostMedia));
  base::UmaHistogramBoolean("Ash.Birch.ProviderHidden.Weather",
                            !prefs->GetBoolean(prefs::kBirchUseWeather));
  base::UmaHistogramBoolean("Ash.Birch.ProviderHidden.ReleaseNotes",
                            !prefs->GetBoolean(prefs::kBirchUseReleaseNotes));
  base::UmaHistogramBoolean("Ash.Birch.ProviderHidden.Coral",
                            !prefs->GetBoolean(prefs::kBirchUseCoral));
}

bool BirchModel::IsItemRemoverInitialized() {
  return item_remover_ && item_remover_->Initialized();
}

bool BirchModel::ShouldShowLastActive() {
  if (last_active_last_shown_.is_null()) {
    return true;  // Never been shown.
  }
  // Re-show for up to 2 minutes.
  return GetNow() - last_active_last_shown_ < base::Minutes(2);
}

bool BirchModel::ShouldShowMostVisited() {
  if (most_visited_last_shown_.is_null()) {
    return true;  // Never been shown.
  }
  // Re-show for up to 2 minutes.
  return GetNow() - most_visited_last_shown_ < base::Minutes(2);
}

void BirchModel::OnLostMediaDataProviderChanged() {
  CHECK(birch_client_);

  if (lost_media_data_changed_callback_ &&
      !lost_media_data_.fetch_in_progress) {
    lost_media_data_.update_request.emplace();
    lost_media_data_.update_request->callback = base::BindOnce(
        &BirchModel::HandleLostMediaUpdateRequest, base::Unretained(this));
    lost_media_data_.update_request->timer =
        std::make_unique<base::OneShotTimer>();
    lost_media_data_.update_request->timer->Start(
        FROM_HERE, kDataFetchTimeoutInMs,
        base::BindOnce(&BirchModel::HandleLostMediaUpdateRequest,
                       base::Unretained(this)));
    StartDataFetchIfNeeded(lost_media_data_,
                           birch_client_->GetLostMediaProvider());
  }
}

}  // namespace ash