chromium/ash/system/focus_mode/sounds/focus_mode_soundscape_delegate.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/focus_mode/sounds/focus_mode_soundscape_delegate.h"

#include <optional>
#include <vector>

#include "ash/system/focus_mode/sounds/focus_mode_sounds_delegate.h"
#include "ash/system/focus_mode/sounds/soundscape/playlist_tracker.h"
#include "ash/system/focus_mode/sounds/soundscape/soundscape_types.h"
#include "ash/system/focus_mode/sounds/soundscape/soundscapes_downloader.h"
#include "base/functional/callback.h"
#include "base/logging.h"
#include "base/task/sequenced_task_runner.h"

namespace ash {

namespace {

// Length of time that we will retain a configuration before requesting a new
// one.
constexpr base::TimeDelta kCacheLifetime = base::Days(3);

FocusModeSoundsDelegate::Playlist ConvertPlaylist(
    const SoundscapePlaylist& playlist,
    SoundscapesDownloader* soundscapes_downloader) {
  const std::string& id = playlist.uuid.AsLowercaseString();
  const std::string& title = playlist.name;
  const GURL& thumbnail_url =
      soundscapes_downloader->ResolveUrl(playlist.thumbnail);

  return FocusModeSoundsDelegate::Playlist(id, title, thumbnail_url);
}

std::vector<FocusModeSoundsDelegate::Playlist> PlaylistsFromConfig(
    const SoundscapeConfiguration& configuration,
    SoundscapesDownloader* downloader) {
  std::vector<FocusModeSoundsDelegate::Playlist> requests;
  for (const auto& playlist : configuration.playlists) {
    requests.push_back(ConvertPlaylist(playlist, downloader));
  }
  return requests;
}

FocusModeSoundsDelegate::Track FromTrack(const SoundscapeTrack& track,
                                         const std::string& playlist_thumbnail,
                                         SoundscapesDownloader& resolver) {
  return FocusModeSoundsDelegate::Track(
      /*title=*/track.name,
      // `artist` is always empty for soundscapes.
      /*artist=*/"",
      /*source=*/"Focus Sounds",
      /*thumbnail_url*/ resolver.ResolveUrl(playlist_thumbnail),
      /*source_url=*/resolver.ResolveUrl(track.path),
      // Soundscapes does not require playback reporting.
      /*enable_playback_reporting=*/false);
}

}  // namespace

// static
std::unique_ptr<FocusModeSoundscapeDelegate>
FocusModeSoundscapeDelegate::Create(const std::string& locale) {
  return std::make_unique<FocusModeSoundscapeDelegate>(
      SoundscapesDownloader::Create(locale));
}

FocusModeSoundscapeDelegate::FocusModeSoundscapeDelegate(
    std::unique_ptr<SoundscapesDownloader> downloader)
    : downloader_(std::move(downloader)) {}

FocusModeSoundscapeDelegate::~FocusModeSoundscapeDelegate() {
  // Free the tracker before we release `cached_configuration_`.
  playlist_tracker_.reset();
}

void FocusModeSoundscapeDelegate::GetNextTrack(
    const std::string& playlist_id,
    FocusModeSoundsDelegate::TrackCallback callback) {
  if (!cached_configuration_) {
    // TODO(b/342467806): Support fetching a configuration here.
    LOG(WARNING) << "Track requested before configuration download";

    // The callback must be invoked no matter what since the mojom
    // interface pipe is still waiting for response. Please see bug:
    // b/358625939.
    std::move(callback).Run(std::nullopt);

    return;
  }

  if (!playlist_tracker_ || playlist_id != playlist_tracker_->id()) {
    // When switching playlists, create a new tracker.
    const std::vector<SoundscapePlaylist>& playlists =
        cached_configuration_->playlists;
    auto iter =
        std::find_if(playlists.cbegin(), playlists.cend(),
                     [playlist_id](const SoundscapePlaylist& playlist) {
                       return playlist_id == playlist.uuid.AsLowercaseString();
                     });

    if (iter == playlists.end() || iter->tracks.empty()) {
      LOG(WARNING)
          << (iter == playlists.end()
                  ? "Could not find playlist in the cached configuration."
                  : "The playlist has no tracks.");

      // Must invoke the callback.
      std::move(callback).Run(std::nullopt);

      return;
    }

    const SoundscapePlaylist& playlist = *iter;
    playlist_tracker_.emplace(playlist);
  }

  const SoundscapeTrack& next_track = playlist_tracker_->NextTrack();
  std::optional<FocusModeSoundsDelegate::Track> track = FromTrack(
      next_track, playlist_tracker_->playlist().thumbnail, *downloader_);
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(std::move(callback), std::move(track)));
}

void FocusModeSoundscapeDelegate::GetPlaylists(PlaylistsCallback callback) {
  if (cached_configuration_) {
    base::TimeDelta update_age = base::Time::Now() - last_update_;
    if (update_age < kCacheLifetime) {
      // Return the cached playlists.
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce(std::move(callback),
                                    PlaylistsFromConfig(*cached_configuration_,
                                                        downloader_.get())));
      return;
    }
  }

  // Configuration is outdated. Clear it.
  playlist_tracker_.reset();
  cached_configuration_.reset();

  downloader_->FetchConfiguration(
      base::BindOnce(&FocusModeSoundscapeDelegate::HandleConfiguration,
                     weak_factory_.GetWeakPtr(), std::move(callback)));
}

void FocusModeSoundscapeDelegate::HandleConfiguration(
    PlaylistsCallback callback,
    std::optional<SoundscapeConfiguration> configuration) {
  if (!configuration) {
    std::move(callback).Run({});
    return;
  }

  last_update_ = base::Time::Now();
  cached_configuration_ = std::move(configuration);

  std::move(callback).Run(
      PlaylistsFromConfig(*cached_configuration_, downloader_.get()));
}

}  // namespace ash