chromium/chrome/browser/ash/system_web_apps/apps/personalization_app/personalization_app_ambient_provider_impl.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/system_web_apps/apps/personalization_app/personalization_app_ambient_provider_impl.h"

#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "ash/ambient/ambient_controller.h"
#include "ash/ambient/metrics/ambient_metrics.h"
#include "ash/ambient/util/ambient_util.h"
#include "ash/constants/ambient_video.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/geolocation_access_level.h"
#include "ash/controls/contextual_tooltip.h"
#include "ash/public/cpp/ambient/ambient_backend_controller.h"
#include "ash/public/cpp/ambient/ambient_client.h"
#include "ash/public/cpp/ambient/ambient_prefs.h"
#include "ash/public/cpp/ambient/ambient_ui_model.h"
#include "ash/public/cpp/ambient/common/ambient_settings.h"
#include "ash/public/cpp/image_downloader.h"
#include "ash/public/cpp/wallpaper/wallpaper_controller.h"
#include "ash/shell.h"
#include "ash/webui/personalization_app/mojom/personalization_app.mojom.h"
#include "ash/webui/personalization_app/mojom/personalization_app_mojom_traits.h"
#include "base/barrier_closure.h"
#include "base/check.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/memory/ref_counted_memory.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "base/task/sequenced_task_runner.h"
#include "chrome/browser/ash/system_web_apps/apps/personalization_app/ambient_video_albums.h"
#include "chrome/browser/ash/system_web_apps/apps/personalization_app/personalization_app_manager.h"
#include "chrome/browser/ash/system_web_apps/apps/personalization_app/personalization_app_manager_factory.h"
#include "chrome/browser/ash/system_web_apps/apps/personalization_app/personalization_app_metrics.h"
#include "chrome/browser/ash/system_web_apps/apps/personalization_app/personalization_app_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "components/prefs/pref_service.h"
#include "mojo/public/cpp/bindings/message.h"
#include "net/base/backoff_entry.h"
#include "personalization_app_ambient_provider_impl.h"
#include "ui/base/webui/web_ui_util.h"
#include "url/gurl.h"

namespace ash::personalization_app {

namespace {

// The max possible preview image container is 460x290.
// Double the fetched image W/H to stay sharp when scaled down.
constexpr int kBannerWidthPx = 920;
constexpr int kBannerHeightPx = 580;

constexpr int kMaxRetries = 3;

constexpr net::BackoffEntry::Policy kRetryBackoffPolicy = {
    0,          // Number of initial errors to ignore.
    500,        // Initial delay in ms.
    2.0,        // Factor by which the waiting time will be multiplied.
    0.2,        // Fuzzing percentage.
    60 * 1000,  // Maximum delay in ms.
    -1,         // Never discard the entry.
    true,       // Use initial delay.
};

DurationOption GetDurationMetrics(int minutes) {
  switch (minutes) {
    case (0): {
      return DurationOption::kForever;
    }
    case (5): {
      return DurationOption::kFiveMin;
    }
    case (10): {
      return DurationOption::kTenMin;
    }
    case (30): {
      return DurationOption::kThirtyMin;
    }
    case (60): {
      return DurationOption::kOneHour;
    }
    default: {
      return DurationOption::kError;
    }
  }
}

}  // namespace

PersonalizationAppAmbientProviderImpl::PersonalizationAppAmbientProviderImpl(
    content::WebUI* web_ui)
    : profile_(Profile::FromWebUI(web_ui)),
      fetch_settings_retry_backoff_(&kRetryBackoffPolicy),
      update_settings_retry_backoff_(&kRetryBackoffPolicy) {
  pref_change_registrar_.Init(profile_->GetPrefs());
  pref_change_registrar_.Add(
      ash::ambient::prefs::kAmbientModeEnabled,
      base::BindRepeating(
          &PersonalizationAppAmbientProviderImpl::OnAmbientModeEnabledChanged,
          base::Unretained(this)));
  pref_change_registrar_.Add(
      ash::ambient::prefs::kAmbientUiSettings,
      base::BindRepeating(
          &PersonalizationAppAmbientProviderImpl::OnAmbientUiSettingsChanged,
          base::Unretained(this)));
  pref_change_registrar_.Add(
      ash::prefs::kUserGeolocationAccessLevel,
      base::BindRepeating(&PersonalizationAppAmbientProviderImpl::
                              NotifyGeolocationPermissionChanged,
                          base::Unretained(this)));
  ambient_ui_model_observer_.Observe(
      Shell::Get()->ambient_controller()->ambient_ui_model());
}

PersonalizationAppAmbientProviderImpl::
    ~PersonalizationAppAmbientProviderImpl() {
  if (page_viewed_) {
    ::ash::personalization_app::PersonalizationAppManagerFactory::
        GetForBrowserContext(profile_)
            ->MaybeStartHatsTimer(
                ::ash::personalization_app::HatsSurveyType::kScreensaver);
  }
}

void PersonalizationAppAmbientProviderImpl::BindInterface(
    mojo::PendingReceiver<ash::personalization_app::mojom::AmbientProvider>
        receiver) {
  ambient_receiver_.reset();
  ambient_receiver_.Bind(std::move(receiver));
}

void PersonalizationAppAmbientProviderImpl::IsAmbientModeEnabled(
    IsAmbientModeEnabledCallback callback) {
  PrefService* pref_service = profile_->GetPrefs();
  DCHECK(pref_service);
  std::move(callback).Run(
      pref_service->GetBoolean(ash::ambient::prefs::kAmbientModeEnabled));
}

bool PersonalizationAppAmbientProviderImpl::
    IsGeolocationEnabledForSystemServices() {
  PrefService* pref_service = profile_->GetPrefs();
  CHECK(pref_service);
  const auto access_level = static_cast<GeolocationAccessLevel>(
      pref_service->GetInteger(prefs::kUserGeolocationAccessLevel));

  switch (access_level) {
    case ash::GeolocationAccessLevel::kAllowed:
    case ash::GeolocationAccessLevel::kOnlyAllowedForSystem:
      return true;
    case ash::GeolocationAccessLevel::kDisallowed:
      return false;
  }
}

void PersonalizationAppAmbientProviderImpl::
    NotifyGeolocationPermissionChanged() {
  if (!ambient_observer_remote_.is_bound()) {
    return;
  }

  ambient_observer_remote_->OnGeolocationPermissionForSystemServicesChanged(
      IsGeolocationEnabledForSystemServices());
}

void PersonalizationAppAmbientProviderImpl::SetAmbientObserver(
    mojo::PendingRemote<ash::personalization_app::mojom::AmbientObserver>
        observer) {
  if (!AmbientClient::Get() || !AmbientClient::Get()->IsAmbientModeAllowed()) {
    ambient_receiver_.ReportBadMessage(
        "Ambient observer set when ambient is not allowed");
    return;
  }
  // May already be bound if user refreshes page.
  ambient_observer_remote_.reset();
  ambient_observer_remote_.Bind(std::move(observer));

  // Call it once to get the current ambient mode enabled status.
  BroadcastAmbientModeEnabledStatus(IsAmbientModeEnabled());

  // Call it once to get the current ambient ui settings.
  OnAmbientUiSettingsChanged();

  // Call it once to get the current ambient duration settings.
  OnScreenSaverDurationChanged();

  ResetLocalSettings();
}

void PersonalizationAppAmbientProviderImpl::SetAmbientModeEnabled(
    bool enabled) {
  PrefService* pref_service = profile_->GetPrefs();
  DCHECK(pref_service);
  pref_service->SetBoolean(ash::ambient::prefs::kAmbientModeEnabled, enabled);
}

void PersonalizationAppAmbientProviderImpl::SetAmbientTheme(
    mojom::AmbientTheme to_theme) {
  PrefService* pref_service = profile_->GetPrefs();
  DCHECK(pref_service);
  LogAmbientModeTheme(to_theme);
  AmbientUiSettings orig_settings = GetCurrentUiSettings();
  mojom::AmbientTheme from_theme = orig_settings.theme();
  if (from_theme == to_theme) {
    return;
  }

  // Attempt to retrieve the previously selected video. If not, fallback to the
  // default video. Only applicable when target theme is `AmbientTheme::kVideo`.
  AmbientVideo video = orig_settings.video().value_or(kDefaultAmbientVideo);
  if (to_theme == mojom::AmbientTheme::kVideo) {
    LogAmbientModeVideo(video);
  }
  AmbientUiSettings(to_theme, video).WriteToPrefService(*pref_service);

  // `kVideo` theme is special and automatically means a switch to the `kVideo`
  // topic source. None of the other topic sources are possible with this theme.
  //
  // If `settings_` is null, the next call to `FetchSettingsAndAlbums()` will
  // broadcast the `OnTopicSourceChanged()` call that's being done here.
  if (settings_ && (to_theme == mojom::AmbientTheme::kVideo ||
                    from_theme == mojom::AmbientTheme::kVideo)) {
    OnTopicSourceChanged();
  }
}

void PersonalizationAppAmbientProviderImpl::SetTopicSource(
    mojom::TopicSource topic_source) {
  mojom::AmbientTheme current_theme = GetCurrentUiSettings().theme();
  // The presence of the `kVideo` theme in pref automatically means the `kVideo`
  // topic source is active. `settings_` should be kept as the server's view of
  // the user's ambient settings, and `SetAmbientTheme(kVideo)` already
  // broadcasts an `OnTopicSourceChanged()`, so there's no work to do here.
  if (current_theme == mojom::AmbientTheme::kVideo) {
    if (topic_source != mojom::TopicSource::kVideo) {
      LOG(ERROR) << "Cannot set topic source to "
                 << static_cast<int>(topic_source) << " for video theme";
    }
    return;
  }

  if (topic_source == mojom::TopicSource::kVideo) {
    LOG(ERROR) << "Video topic source does not apply to theme "
               << ambient::util::AmbientThemeToString(current_theme);
    return;
  }

  // If this is an Art gallery album page, will select art gallery topic source.
  if (topic_source == mojom::TopicSource::kArtGallery) {
    MaybeUpdateTopicSource(topic_source);
    return;
  }

  // If this is a Google Photos album page, will
  // 1. Select art gallery topic source if no albums or no album is selected.
  if (settings_->selected_album_ids.empty()) {
    MaybeUpdateTopicSource(mojom::TopicSource::kArtGallery);
    return;
  }

  // 2. Select Google Photos topic source if at least one album is selected.
  MaybeUpdateTopicSource(mojom::TopicSource::kGooglePhotos);
}

void PersonalizationAppAmbientProviderImpl::SetScreenSaverDuration(
    int minutes) {
  Shell::Get()->ambient_controller()->SetScreenSaverDuration(minutes);
  OnScreenSaverDurationChanged();
  LogAmbientModeScreenSaverDuration(GetDurationMetrics(minutes));
}

void PersonalizationAppAmbientProviderImpl::SetTemperatureUnit(
    ash::AmbientModeTemperatureUnit temperature_unit) {
  if (settings_->temperature_unit != temperature_unit) {
    settings_->temperature_unit = temperature_unit;
    UpdateSettings();
    OnTemperatureUnitChanged();
  }
}

void PersonalizationAppAmbientProviderImpl::SetAlbumSelected(
    const std::string& id,
    mojom::TopicSource topic_source,
    bool selected) {
  switch (topic_source) {
    case (mojom::TopicSource::kGooglePhotos): {
      ash::PersonalAlbum* target_personal_album = FindPersonalAlbumById(id);
      if (!target_personal_album) {
        ambient_receiver_.ReportBadMessage("Invalid album id.");
        return;
      }
      target_personal_album->selected = selected;

      // For Google Photos, we will populate the |selected_album_ids| with IDs
      // of selected albums.
      settings_->selected_album_ids.clear();
      for (const auto& personal_album : personal_albums_.albums) {
        if (personal_album.selected) {
          settings_->selected_album_ids.push_back(personal_album.album_id);
        }
      }

      // Update topic source based on selections.
      if (settings_->selected_album_ids.empty()) {
        settings_->topic_source = mojom::TopicSource::kArtGallery;
      } else {
        settings_->topic_source = mojom::TopicSource::kGooglePhotos;
      }

      ash::ambient::RecordAmbientModeTotalNumberOfAlbums(
          personal_albums_.albums.size());
      ash::ambient::RecordAmbientModeSelectedNumberOfAlbums(
          settings_->selected_album_ids.size());
      break;
    }
    case (mojom::TopicSource::kArtGallery): {
      // For Art gallery, we set the corresponding setting to be enabled or not
      // based on the selections.
      auto* art_setting = FindArtAlbumById(id);
      if (!art_setting || !art_setting->visible) {
        ambient_receiver_.ReportBadMessage("Invalid album id.");
        return;
      }
      art_setting->enabled = selected;
      break;
    }
    case mojom::TopicSource::kVideo:
      if (!selected) {
        DVLOG(4) << "Exactly one video must be selected at all times. Setting "
                    "the desired video to selected==true automatically "
                    "unselects all other videos.";
        return;
      }
      std::optional<AmbientVideo> video = FindAmbientVideoByAlbumId(id);
      if (!video) {
        ambient_receiver_.ReportBadMessage("Invalid album id.");
        return;
      }
      // Even if the current `AmbientTheme` is not `kVideo`, pref storage can
      // still be updated with the requested video, and it will be applied
      // if/when the user selects the video theme later.
      PrefService* pref_service = profile_->GetPrefs();
      DCHECK(pref_service);
      AmbientUiSettings(GetCurrentUiSettings().theme(), *video)
          .WriteToPrefService(*pref_service);
      LogAmbientModeVideo(*video);
      break;
  }

  UpdateSettings();
  OnTopicSourceChanged();
  OnAlbumsChanged();
}

void PersonalizationAppAmbientProviderImpl::SetPageViewed() {
  page_viewed_ = true;
}

void PersonalizationAppAmbientProviderImpl::FetchSettingsAndAlbums() {
  // If there is an ongoing update, do not fetch. If update succeeds, it will
  // update the UI with the new settings. If update fails, it will restore
  // previous settings and update UI.
  if (is_updating_backend_) {
    return;
  }

  ash::AmbientBackendController::Get()->FetchSettingsAndAlbums(
      kBannerWidthPx, kBannerHeightPx, /*num_albums=*/100,
      base::BindOnce(
          &PersonalizationAppAmbientProviderImpl::OnSettingsAndAlbumsFetched,
          read_weak_factory_.GetWeakPtr()));
}

void PersonalizationAppAmbientProviderImpl::OnAmbientModeEnabledChanged() {
  const bool enabled = IsAmbientModeEnabled();
  if (enabled) {
    // The usage metrics for the user's `AmbientUiSettings` should be
    // incremented whenever ambient mode is enabled. They should not be
    // incremented though every time the hub is simply opened.
    AmbientUiSettings current_ui_settings = GetCurrentUiSettings();
    LogAmbientModeTheme(current_ui_settings.theme());
    if (current_ui_settings.theme() == mojom::AmbientTheme::kVideo) {
      LogAmbientModeVideo(*current_ui_settings.video());
    }
  }
  BroadcastAmbientModeEnabledStatus(enabled);
  if (!enabled) {
    // UpdateSettings assumes that ambient is enabled. Since ambient is now
    // disabled, cancel any requests to update settings.
    write_weak_factory_.InvalidateWeakPtrs();
    is_updating_backend_ = false;
  }
}

void PersonalizationAppAmbientProviderImpl::BroadcastAmbientModeEnabledStatus(
    bool enabled) {
  if (ambient_observer_remote_.is_bound()) {
    ambient_observer_remote_->OnAmbientModeEnabledChanged(enabled);
  }

  // Call |UpdateSettings| when Ambient mode is enabled to make sure that
  // settings are properly synced to the server even if the user never touches
  // the other controls. Please see b/177456397.
  if (settings_ && enabled) {
    UpdateSettings();
  }
}

void PersonalizationAppAmbientProviderImpl::OnAmbientUiSettingsChanged() {
  if (!ambient_observer_remote_.is_bound()) {
    return;
  }

  ambient_observer_remote_->OnAmbientThemeChanged(
      GetCurrentUiSettings().theme());
}

void PersonalizationAppAmbientProviderImpl::OnScreenSaverDurationChanged() {
  if (!ambient_observer_remote_.is_bound()) {
    return;
  }

  PrefService* pref_service = profile_->GetPrefs();
  DCHECK(pref_service);
  int duration_minutes = pref_service->GetInteger(
      ambient::prefs::kAmbientModeRunningDurationMinutes);
  CHECK(duration_minutes >= 0);
  ambient_observer_remote_->OnScreenSaverDurationChanged(duration_minutes);
}

void PersonalizationAppAmbientProviderImpl::OnTemperatureUnitChanged() {
  if (!ambient_observer_remote_.is_bound()) {
    return;
  }

  ambient_observer_remote_->OnTemperatureUnitChanged(
      settings_->temperature_unit);
}

void PersonalizationAppAmbientProviderImpl::OnTopicSourceChanged() {
  if (!ambient_observer_remote_.is_bound()) {
    return;
  }

  // Empty the WebUI store so it doesn't show the previously selected albums'
  // previews.
  OnPreviewsFetched(std::vector<GURL>());
  if (is_updating_backend_) {
    // Once settings updated, fetch preview images.
    needs_update_previews_ = true;
  } else {
    // Fetch preview images if settings have been updated.
    FetchPreviewImages();
  }

  ambient_observer_remote_->OnTopicSourceChanged(GetCurrentTopicSource());
}

void PersonalizationAppAmbientProviderImpl::OnAlbumsChanged() {
  if (!ambient_observer_remote_.is_bound()) {
    return;
  }

  std::vector<ash::personalization_app::mojom::AmbientModeAlbumPtr> albums;
  // Google photos:
  for (const auto& personal_album : personal_albums_.albums) {
    // `url` will be updated when preview image is downloaded.
    ash::personalization_app::mojom::AmbientModeAlbumPtr album =
        ash::personalization_app::mojom::AmbientModeAlbum::New();
    album->id = personal_album.album_id;
    album->checked = personal_album.selected;
    album->title = personal_album.album_name;
    album->description = personal_album.description;
    album->number_of_photos = personal_album.number_of_photos;
    album->url = GURL(personal_album.banner_image_url);
    album->topic_source = mojom::TopicSource::kGooglePhotos;
    albums.emplace_back(std::move(album));
  }

  // Art gallery:
  for (const auto& setting : settings_->art_settings) {
    if (!setting.visible) {
      continue;
    }

    // `url` will be updated when preview image is downloaded.
    ash::personalization_app::mojom::AmbientModeAlbumPtr album =
        ash::personalization_app::mojom::AmbientModeAlbum::New();
    album->id = setting.album_id;
    album->checked = setting.enabled;
    album->title = setting.title;
    album->description = setting.description;
    album->url = GURL(setting.preview_image_url);
    album->topic_source = mojom::TopicSource::kArtGallery;
    albums.emplace_back(std::move(album));
  }

  // Video:
  AppendAmbientVideoAlbums(
      /*currently_selected_video*/ GetCurrentUiSettings().video().value_or(
          kDefaultAmbientVideo),
      albums);

  ambient_observer_remote_->OnAlbumsChanged(std::move(albums));
}

void PersonalizationAppAmbientProviderImpl::
    OnRecentHighlightsPreviewsChanged() {
  NOTIMPLEMENTED();
}

bool PersonalizationAppAmbientProviderImpl::IsAmbientModeEnabled() {
  PrefService* pref_service = profile_->GetPrefs();
  DCHECK(pref_service);
  return pref_service->GetBoolean(ash::ambient::prefs::kAmbientModeEnabled);
}

AmbientUiSettings PersonalizationAppAmbientProviderImpl::GetCurrentUiSettings()
    const {
  PrefService* pref_service = profile_->GetPrefs();
  DCHECK(pref_service);
  return AmbientUiSettings::ReadFromPrefService(*pref_service);
}

void PersonalizationAppAmbientProviderImpl::UpdateSettings() {
  DCHECK(IsAmbientModeEnabled())
      << "Ambient mode must be enabled to update settings";
  DCHECK(settings_);
  DCHECK_NE(settings_->topic_source, mojom::TopicSource::kVideo)
      << "Ambient backend is not aware of the video topic source";

  // Prevent fetch settings callback changing `settings_` and `personal_albums_`
  // while updating.
  read_weak_factory_.InvalidateWeakPtrs();
  // Cancel in-flight write requests, as this newer update will overwrite them.
  write_weak_factory_.InvalidateWeakPtrs();

  is_updating_backend_ = true;

  // Explicitly set show_weather to true to force server to respond with
  // weather information. See: b/158630188.
  settings_->show_weather = true;

  ash::AmbientBackendController::Get()->UpdateSettings(
      *settings_,
      base::BindOnce(&PersonalizationAppAmbientProviderImpl::OnUpdateSettings,
                     write_weak_factory_.GetWeakPtr()));
}

void PersonalizationAppAmbientProviderImpl::OnUpdateSettings(
    bool success,
    const AmbientSettings& settings) {
  is_updating_backend_ = false;

  if (success) {
    update_settings_retry_backoff_.Reset();
    OnSettingsAndAlbumsFetched(settings, std::move(personal_albums_));
    // The request to fetch preview images came in during |UpdateSettings|. Call
    // it now that updating has finished.
    if (needs_update_previews_) {
      FetchPreviewImages();
    }
  } else {
    update_settings_retry_backoff_.InformOfRequest(/*succeeded=*/false);
    // When the update fails, send a retry or revert to cached settings.
    if (update_settings_retry_backoff_.failure_count() <= kMaxRetries) {
      const base::TimeDelta kDelay =
          update_settings_retry_backoff_.GetTimeUntilRelease();
      base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE,
          base::BindOnce(&PersonalizationAppAmbientProviderImpl::UpdateSettings,
                         write_weak_factory_.GetWeakPtr()),
          kDelay);
    } else {
      OnSettingsAndAlbumsFetched(cached_settings_, std::move(personal_albums_));
    }
  }
}

void PersonalizationAppAmbientProviderImpl::OnSettingsAndAlbumsFetched(
    const std::optional<ash::AmbientSettings>& settings,
    ash::PersonalAlbums personal_albums) {
  // `settings` value implies success.
  if (!settings) {
    fetch_settings_retry_backoff_.InformOfRequest(/*succeeded=*/false);
    if (fetch_settings_retry_backoff_.failure_count() > kMaxRetries) {
      return;
    }

    const base::TimeDelta kDelay =
        fetch_settings_retry_backoff_.GetTimeUntilRelease();
    base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(
            &PersonalizationAppAmbientProviderImpl::FetchSettingsAndAlbums,
            read_weak_factory_.GetWeakPtr()),
        kDelay);
    return;
  }

  fetch_settings_retry_backoff_.Reset();
  settings_ = settings;
  cached_settings_ = settings;
  personal_albums_ = std::move(personal_albums);
  SyncSettingsAndAlbums();

  OnTemperatureUnitChanged();

  // Notify `OnAlbumsChanged()` first because the albums info is needed to
  // render the description text of the topic source buttons. E.g. if the Google
  // Photos album is empty, it will show different text.
  OnAlbumsChanged();
  OnTopicSourceChanged();

  // If weather info is disabled, call `UpdateSettings()` immediately to force
  // it to true. Please see b/177456397.
  if (!settings_->show_weather && IsAmbientModeEnabled()) {
    UpdateSettings();
  }
}

void PersonalizationAppAmbientProviderImpl::SyncSettingsAndAlbums() {
  // Clear the `selected` field, which will be populated with new value below.
  // It is necessary if `UpdateSettings()` failed and we need to reset the
  // cached settings.
  for (auto& album : personal_albums_.albums) {
    album.selected = false;
  }

  auto it = settings_->selected_album_ids.begin();
  while (it != settings_->selected_album_ids.end()) {
    const std::string& album_id = *it;
    ash::PersonalAlbum* album = FindPersonalAlbumById(album_id);
    if (album) {
      album->selected = true;
      ++it;
    } else {
      // The selected album does not exist any more.
      it = settings_->selected_album_ids.erase(it);
    }
  }

  if (settings_->selected_album_ids.empty()) {
    MaybeUpdateTopicSource(mojom::TopicSource::kArtGallery);
  }
}

void PersonalizationAppAmbientProviderImpl::MaybeUpdateTopicSource(
    mojom::TopicSource topic_source) {
  DCHECK_NE(settings_->topic_source, mojom::TopicSource::kVideo)
      << "Video topic source should automatically get set via the video "
         "AmbientTheme. Should not be reflected in the server.";
  // If the setting is the same, no need to update.
  if (settings_->topic_source != topic_source) {
    settings_->topic_source = topic_source;
    if (IsAmbientModeEnabled()) {
      // Only send update to server if ambient mode is currently enabled.
      UpdateSettings();
    }
  }

  OnTopicSourceChanged();
}

void PersonalizationAppAmbientProviderImpl::FetchPreviewImages() {
  needs_update_previews_ = false;
  previews_weak_factory_.InvalidateWeakPtrs();
  if (GetCurrentUiSettings().theme() == mojom::AmbientTheme::kVideo) {
    std::optional<AmbientVideo> video = GetCurrentUiSettings().video();
    DCHECK(video.has_value());
    auto url_arr =
        AmbientBackendController::Get()->GetTimeOfDayVideoPreviewImageUrls(
            video.value());
    std::vector<GURL> previews;
    base::ranges::transform(url_arr, std::back_inserter(previews),
                            [](const char* url) { return GURL(url); });
    OnPreviewsFetched(std::move(previews));
    return;
  }

  const gfx::Size image_size = gfx::Size(kBannerWidthPx, kBannerHeightPx);
  ash::AmbientBackendController::Get()->FetchPreviewImages(
      image_size,
      base::BindOnce(&PersonalizationAppAmbientProviderImpl::OnPreviewsFetched,
                     previews_weak_factory_.GetWeakPtr()));
}

void PersonalizationAppAmbientProviderImpl::OnPreviewsFetched(
    const std::vector<GURL>& preview_urls) {
  DVLOG(4) << __func__ << " preview_urls_size=" << preview_urls.size();
  ambient_observer_remote_->OnPreviewsFetched(preview_urls);
}

ash::PersonalAlbum*
PersonalizationAppAmbientProviderImpl::FindPersonalAlbumById(
    const std::string& album_id) {
  auto it = base::ranges::find(personal_albums_.albums, album_id,
                               &ash::PersonalAlbum::album_id);

  if (it == personal_albums_.albums.end()) {
    return nullptr;
  }

  return &(*it);
}

ash::ArtSetting* PersonalizationAppAmbientProviderImpl::FindArtAlbumById(
    const std::string& album_id) {
  auto it = base::ranges::find(settings_->art_settings, album_id,
                               &ash::ArtSetting::album_id);
  // Album does not exist any more.
  if (it == settings_->art_settings.end()) {
    return nullptr;
  }

  return &(*it);
}

void PersonalizationAppAmbientProviderImpl::ResetLocalSettings() {
  write_weak_factory_.InvalidateWeakPtrs();
  read_weak_factory_.InvalidateWeakPtrs();
  previews_weak_factory_.InvalidateWeakPtrs();

  settings_.reset();
  cached_settings_.reset();
  update_settings_retry_backoff_.Reset();
  fetch_settings_retry_backoff_.Reset();
  is_updating_backend_ = false;
}

void PersonalizationAppAmbientProviderImpl::StartScreenSaverPreview() {
  Shell::Get()->ambient_controller()->SetUiVisibilityPreview();
}

void PersonalizationAppAmbientProviderImpl::ShouldShowTimeOfDayBanner(
    ShouldShowTimeOfDayBannerCallback callback) {
  // Time of day banner should not display for the users with policy managed
  // wallpapers who cannot change their wallpapers. Note that although
  // enterprise users are not able to access screen saver, some of them are able
  // to access wallpaper subpage and change wallpapers.
  std::move(callback).Run(
      !WallpaperController::Get()->IsWallpaperControlledByPolicy(
          GetAccountId(profile_)) &&
      features::IsTimeOfDayScreenSaverEnabled() &&
      contextual_tooltip::ShouldShowNudge(
          profile_->GetPrefs(),
          contextual_tooltip::TooltipType::kTimeOfDayFeatureBanner,
          /*recheck_delay=*/nullptr));
}

void PersonalizationAppAmbientProviderImpl::HandleTimeOfDayBannerDismissed() {
  contextual_tooltip::HandleGesturePerformed(
      profile_->GetPrefs(),
      contextual_tooltip::TooltipType::kTimeOfDayFeatureBanner);
}

void PersonalizationAppAmbientProviderImpl::
    IsGeolocationEnabledForSystemServices(
        IsGeolocationEnabledForSystemServicesCallback callback) {
  std::move(callback).Run(IsGeolocationEnabledForSystemServices());
}

void PersonalizationAppAmbientProviderImpl::
    EnableGeolocationForSystemServices() {
  PrefService* pref_service = profile_->GetPrefs();
  pref_service->SetInteger(
      prefs::kUserGeolocationAccessLevel,
      static_cast<int>(GeolocationAccessLevel::kOnlyAllowedForSystem));
}

void PersonalizationAppAmbientProviderImpl::OnAmbientUiVisibilityChanged(
    ash::AmbientUiVisibility visibility) {
  if (ambient_observer_remote_.is_bound()) {
    ambient_observer_remote_->OnAmbientUiVisibilityChanged(visibility);
  }
}

mojom::TopicSource
PersonalizationAppAmbientProviderImpl::GetCurrentTopicSource() const {
  if (GetCurrentUiSettings().theme() == mojom::AmbientTheme::kVideo) {
    return mojom::TopicSource::kVideo;
  } else {
    DCHECK(settings_);
    return settings_->topic_source;
  }
}

}  // namespace ash::personalization_app