chromium/ash/birch/birch_item.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/birch/birch_item.h"

#include <limits>
#include <sstream>
#include <string>

#include "ash/birch/birch_icon_cache.h"
#include "ash/birch/birch_model.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/image_downloader.h"
#include "ash/public/cpp/new_window_delegate.h"
#include "ash/public/cpp/resources/grit/ash_public_unscaled_resources.h"
#include "ash/public/cpp/style/dark_light_mode_controller.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/wm/overview/overview_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "base/i18n/time_formatting.h"
#include "base/json/json_writer.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/ui/base/file_icon_util.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "net/traffic_annotation/network_traffic_annotation.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/models/image_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/gfx/image/image_skia_operations.h"

namespace ash {
namespace {

constexpr net::NetworkTrafficAnnotationTag kIconDownloaderTrafficTag =
    net::DefineNetworkTrafficAnnotation("glanceables_icon_downloader", R"(
        semantics {
          sender: "Post-login glanceables"
          description:
            "Downloads icons for suggestion chip buttons for activities the "
            "user might want to perform after login or from overview mode "
            "(e.g. view a calendar event or open a file)."
          trigger: "User logs in to device or enters overview mode."
          data: "None."
          destination: GOOGLE_OWNED_SERVICE
          user_data {
            type: NONE
          }
          internal {
            contacts {
              email: "[email protected]"
            }
          }
          last_reviewed: "2024-05-29"
        }
        policy {
          cookies_allowed: NO
          setting:
            "This feature can be enabled/disabled by the user in the "
            "suggestion chip button context menu."
          chrome_policy {
            ContextualGoogleIntegrationsEnabled {
              ContextualGoogleIntegrationsEnabled: false
            }
          }
        })");

// Handles when an `image` is downloaded, by converting it to a ui::ImageModel
// and running `callback`.
void OnImageDownloaded(
    const GURL& url,
    const ui::ImageModel& backup_icon,
    SecondaryIconType secondary_icon_type,
    base::OnceCallback<void(const ui::ImageModel&, SecondaryIconType)> callback,
    const gfx::ImageSkia& image) {
  if (image.isNull()) {
    std::move(callback).Run(backup_icon, secondary_icon_type);
    return;
  }
  // Add the image to the cache.
  Shell::Get()->birch_model()->icon_cache()->Put(url.spec(), image);
  std::move(callback).Run(ui::ImageModel::FromImageSkia(image),
                          secondary_icon_type);
}

// Downloads an image from `url` and invokes `callback` with the image. If the
// `url` is invalid, invokes `callback` with an error image.
void DownloadImageFromUrl(
    const GURL& url,
    const ui::ImageModel& backup_icon,
    SecondaryIconType secondary_icon_type,
    base::OnceCallback<void(const ui::ImageModel&, SecondaryIconType)>
        callback) {
  if (!url.is_valid()) {
    // For tab item types, we retrieve the backup chrome icon, or supply an
    // empty icon.
    std::move(callback).Run(backup_icon, secondary_icon_type);
    return;
  }

  // Look for the icon in the cache.
  gfx::ImageSkia icon =
      Shell::Get()->birch_model()->icon_cache()->Get(url.spec());
  if (!icon.isNull()) {
    // Use the cached icon.
    std::move(callback).Run(ui::ImageModel::FromImageSkia(icon),
                            secondary_icon_type);
    return;
  }

  // Download the icon.
  const UserSession* active_user_session =
      Shell::Get()->session_controller()->GetUserSession(0);
  CHECK(active_user_session);

  ImageDownloader::Get()->Download(
      url, kIconDownloaderTrafficTag, active_user_session->user_info.account_id,
      base::BindOnce(&OnImageDownloaded, url, backup_icon, secondary_icon_type,
                     std::move(callback)));
}

// Callback for the favicon load request in `GetFaviconImage()`. If the load
// failed, requests the icon off the network.
void OnGotFaviconImage(
    const GURL& url,
    const ui::ImageModel& backup_icon,
    SecondaryIconType secondary_icon_type,
    base::OnceCallback<void(const ui::ImageModel&, SecondaryIconType)>
        load_icon_callback,
    const ui::ImageModel& image) {
  // Favicon lookup in the FaviconService failed. Fall back to downloading the
  // asset off the network.
  if (image.IsEmpty()) {
    DownloadImageFromUrl(url, backup_icon, secondary_icon_type,
                         std::move(load_icon_callback));
    return;
  }
  std::move(load_icon_callback).Run(image, secondary_icon_type);
}

// Loads a favicon image based on the `page_url` or `icon_url` with the
// FaviconService. Invokes the callback either with a valid image (success) or
// an empty image (failure).
void GetFaviconImage(
    const GURL& url,
    const bool is_page_url,
    const ui::ImageModel& backup_icon,
    SecondaryIconType secondary_icon_type,
    base::OnceCallback<void(const ui::ImageModel&, SecondaryIconType)>
        load_icon_callback) {
  BirchClient* client = Shell::Get()->birch_model()->birch_client();
  client->GetFaviconImage(
      url, is_page_url,
      base::BindOnce(&OnGotFaviconImage, url, backup_icon, secondary_icon_type,
                     std::move(load_icon_callback)));
}

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

std::string SecondaryIconTypeToString(SecondaryIconType type) {
  switch (type) {
    case SecondaryIconType::kTabFromDesktop:
      return "kTabFromDesktop";
    case SecondaryIconType::kTabFromPhone:
      return "kTabFromPhone";
    case SecondaryIconType::kTabFromTablet:
      return "kTabFromTablet";
    case SecondaryIconType::kTabFromUnknown:
      return "kTabFromUnknown";
    case SecondaryIconType::kLostMediaAudio:
      return "kLostMediaAudio";
    case SecondaryIconType::kLostMediaVideo:
      return "kLostMediaVideo";
    case SecondaryIconType::kLostMediaVideoConference:
      return "kLostMediaVideoConference";
    case SecondaryIconType::kNoIcon:
      return "kNoIcon";
  }
}

const ui::ImageModel GetChromeBackupIcon() {
  return ui::ImageModel::FromVectorIcon(kBirchChromeBackupIcon);
}

}  // namespace

int BirchItem::action_count_ = 0;

BirchItem::BirchItem(const std::u16string& title,
                     const std::u16string& subtitle)
    : title_(title),
      subtitle_(subtitle),
      ranking_(std::numeric_limits<float>::max()) {}

BirchItem::BirchItem(BirchItem&&) = default;

BirchItem& BirchItem::operator=(BirchItem&&) = default;

BirchItem::BirchItem(const BirchItem&) = default;

BirchItem& BirchItem::operator=(const BirchItem&) = default;

BirchItem::~BirchItem() = default;

bool BirchItem::operator==(const BirchItem& rhs) const = default;

// static
void BirchItem::RegisterProfilePrefs(PrefRegistrySimple* registry) {
  registry->RegisterBooleanPref(prefs::kBirchUseCelsius, false);
}

std::u16string BirchItem::GetAccessibleName() const {
  return title_ + u" " + subtitle_;
}

void BirchItem::PerformAddonAction() {}

BirchAddonType BirchItem::GetAddonType() const {
  return BirchAddonType::kNone;
}

std::u16string BirchItem::GetAddonAccessibleName() const {
  CHECK(addon_label_.has_value());
  return *addon_label_;
}

void BirchItem::RecordActionMetrics() {
  // Record that the whole bar was activated.
  base::UmaHistogramBoolean("Ash.Birch.Bar.Activate", true);
  // Record which chip type was activated.
  base::UmaHistogramEnumeration("Ash.Birch.Chip.Activate", GetType());
  // Record the ranking of the activated chip.
  base::UmaHistogramCounts100("Ash.Birch.Chip.ActivatedRanking",
                              static_cast<int>(ranking()));
  // Record the types of the first 3 actions in a session.
  ++action_count_;
  if (action_count_ == 1) {
    base::UmaHistogramEnumeration("Ash.Birch.Chip.ActivateFirst", GetType());
  } else if (action_count_ == 2) {
    base::UmaHistogramEnumeration("Ash.Birch.Chip.ActivateSecond", GetType());
  } else if (action_count_ == 3) {
    base::UmaHistogramEnumeration("Ash.Birch.Chip.ActivateThird", GetType());
  }
}

////////////////////////////////////////////////////////////////////////////////

BirchCalendarItem::BirchCalendarItem(const std::u16string& title,
                                     const base::Time& start_time,
                                     const base::Time& end_time,
                                     const GURL& calendar_url,
                                     const GURL& conference_url,
                                     const std::string& event_id,
                                     const bool all_day_event,
                                     ResponseStatus response_status)
    : BirchItem(title, GetSubtitle(start_time, end_time, all_day_event)),
      start_time_(start_time),
      end_time_(end_time),
      all_day_event_(all_day_event),
      calendar_url_(calendar_url),
      conference_url_(conference_url),
      event_id_(event_id),
      response_status_(response_status) {
  if (ShouldShowJoinButton()) {
    set_addon_label(
        l10n_util::GetStringUTF16(IDS_ASH_BIRCH_CALENDAR_JOIN_BUTTON));
  }
}

BirchCalendarItem::BirchCalendarItem(BirchCalendarItem&&) = default;

BirchCalendarItem::BirchCalendarItem(const BirchCalendarItem&) = default;

BirchCalendarItem& BirchCalendarItem::operator=(const BirchCalendarItem&) =
    default;

BirchCalendarItem::~BirchCalendarItem() = default;

BirchItemType BirchCalendarItem::GetType() const {
  return BirchItemType::kCalendar;
}

std::string BirchCalendarItem::ToString() const {
  std::stringstream ss;
  using base::UTF16ToUTF8;
  ss << "Calendar item: {ranking: " << ranking()
     << ", title: " << UTF16ToUTF8(title()) << ", start: "
     << UTF16ToUTF8(base::TimeFormatShortDateAndTime(start_time_))
     << ", end: " << UTF16ToUTF8(base::TimeFormatShortDateAndTime(end_time_))
     << ", conference_url: " << conference_url_.spec()
     << ", event_id: " << event_id_ << "}";
  return ss.str();
}

void BirchCalendarItem::PerformAction() {
  if (!calendar_url_.is_valid()) {
    LOG(ERROR) << "No valid URL for calendar item";
    return;
  }
  RecordActionMetrics();
  NewWindowDelegate::GetPrimary()->OpenUrl(
      calendar_url_, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kNewForegroundTab);
}

void BirchCalendarItem::PerformAddonAction() {
  if (!conference_url_.is_valid()) {
    LOG(ERROR) << "No conference URL for calendar item";
    return;
  }
  // TODO(jamescook): Decide if we want differerent metrics for secondary
  // actions.
  RecordActionMetrics();
  NewWindowDelegate::GetPrimary()->OpenUrl(
      conference_url_, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kNewForegroundTab);
}

void BirchCalendarItem::LoadIcon(LoadIconCallback callback) const {
  std::move(callback).Run(ui::ImageModel::FromVectorIcon(kCalendarEventIcon),
                          SecondaryIconType::kNoIcon);
}

BirchAddonType BirchCalendarItem::GetAddonType() const {
  return addon_label().has_value() ? BirchAddonType::kButton
                                   : BirchAddonType::kNone;
}

std::u16string BirchCalendarItem::GetAddonAccessibleName() const {
  return l10n_util::GetStringUTF16(IDS_ASH_BIRCH_CALENDAR_JOIN_BUTTON_TOOLTIP);
}

// static
std::u16string BirchCalendarItem::GetSubtitle(base::Time start_time,
                                              base::Time end_time,
                                              bool all_day_event) {
  base::Time now = base::Time::Now();
  if (start_time < now && now < end_time) {
    // This event is set to last all day.
    if (all_day_event) {
      return l10n_util::GetStringUTF16(IDS_ASH_BIRCH_CALENDAR_ALL_DAY);
    }

    // This is an ongoing event. Return "Now · Ends 11:20 AM".
    return l10n_util::GetStringFUTF16(IDS_ASH_BIRCH_CALENDAR_ONGOING_SUBTITLE,
                                      base::TimeFormatTimeOfDay(end_time));
  }
  if (start_time < now + base::Minutes(30)) {
    // This event is starting soon. Return "In 5 mins · 10:00 AM - 10:30 AM".
    int minutes = (start_time - now).InMinutes();
    return l10n_util::GetPluralStringFUTF16(IDS_ASH_BIRCH_CALENDAR_MINUTES,
                                            minutes) +
           u" · " + GetStartEndString(start_time, end_time);
  }
  if (now.LocalMidnight() + base::Days(1) < start_time) {
    // This event starts tomorrow. We don't show events more than 1 day in the
    // future, so we don't need to worry about days other than "tomorrow".
    // Return "Tomorrow · 10:00 AM - 11:30 AM"
    return l10n_util::GetStringUTF16(IDS_ASH_BIRCH_CALENDAR_TOMORROW) + u" · " +
           GetStartEndString(start_time, end_time);
  }
  // Otherwise return "10:00 AM - 11:30 AM".
  return GetStartEndString(start_time, end_time);
}

// static
std::u16string BirchCalendarItem::GetStartEndString(base::Time start_time,
                                                    base::Time end_time) {
  // Return "10:00 AM - 10:30 AM".
  return base::TimeFormatTimeOfDay(start_time) + u" - " +
         base::TimeFormatTimeOfDay(end_time);
}

bool BirchCalendarItem::ShouldShowJoinButton() const {
  if (!conference_url_.is_valid()) {
    return false;
  }
  // Only show "Join" if the meeting is starting soon or happening right now.
  base::Time start_adjusted = start_time_ - base::Minutes(5);
  base::Time now = base::Time::Now();
  return start_adjusted < now && now < end_time_;
}

////////////////////////////////////////////////////////////////////////////////

BirchAttachmentItem::BirchAttachmentItem(const std::u16string& title,
                                         const GURL& file_url,
                                         const GURL& icon_url,
                                         const base::Time& start_time,
                                         const base::Time& end_time,
                                         const std::string& file_id)
    : BirchItem(title, GetSubtitle(start_time, end_time)),
      file_url_(file_url),
      icon_url_(icon_url),
      start_time_(start_time),
      end_time_(end_time),
      file_id_(file_id) {}

BirchAttachmentItem::BirchAttachmentItem(BirchAttachmentItem&&) = default;

BirchAttachmentItem& BirchAttachmentItem::operator=(BirchAttachmentItem&&) =
    default;

BirchAttachmentItem::BirchAttachmentItem(const BirchAttachmentItem&) = default;

BirchAttachmentItem& BirchAttachmentItem::operator=(
    const BirchAttachmentItem&) = default;

BirchAttachmentItem::~BirchAttachmentItem() = default;

BirchItemType BirchAttachmentItem::GetType() const {
  return BirchItemType::kAttachment;
}

std::string BirchAttachmentItem::ToString() const {
  std::stringstream ss;
  using base::UTF16ToUTF8;
  ss << "Attachment item: {ranking: " << ranking()
     << ", title: " << UTF16ToUTF8(title())
     << ", file_url: " << file_url_.spec() << ", icon_url: " << icon_url_.spec()
     << ", start: "
     << UTF16ToUTF8(base::TimeFormatShortDateAndTime(start_time_))
     << ", end: " << UTF16ToUTF8(base::TimeFormatShortDateAndTime(end_time_))
     << ", file_id: " << file_id_ << "}";
  return ss.str();
}

void BirchAttachmentItem::PerformAction() {
  if (!file_url_.is_valid()) {
    LOG(ERROR) << "No valid URL for attachment item";
  }
  RecordActionMetrics();
  NewWindowDelegate::GetPrimary()->OpenUrl(
      file_url_, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kNewForegroundTab);
}

void BirchAttachmentItem::LoadIcon(LoadIconCallback callback) const {
  const auto backup_icon = ui::ImageModel::FromImageSkia(
      chromeos::GetIconFromType(chromeos::IconType::kGeneric, true));
  DownloadImageFromUrl(icon_url_, backup_icon, SecondaryIconType::kNoIcon,
                       std::move(callback));
}

// static
std::u16string BirchAttachmentItem::GetSubtitle(base::Time start_time,
                                                base::Time end_time) {
  base::Time now = base::Time::Now();
  if (start_time < now && now < end_time) {
    // This event is happening now.
    return l10n_util::GetStringUTF16(
        IDS_ASH_BIRCH_CALENDAR_ATTACHMENT_NOW_SUBTITLE);
  }
  // This event will happen in the future.
  return l10n_util::GetStringUTF16(
      IDS_ASH_BIRCH_CALENDAR_ATTACHMENT_UPCOMING_SUBTITLE);
}

////////////////////////////////////////////////////////////////////////////////

BirchFileItem::BirchFileItem(const base::FilePath& file_path,
                             const std::optional<std::string>& title,
                             const std::u16string& justification,
                             base::Time timestamp,
                             const std::string& file_id,
                             const std::string& icon_url)
    : BirchItem(GetTitle(file_path, title), justification),
      file_id_(file_id),
      icon_url_(icon_url),
      file_path_(file_path),
      timestamp_(timestamp) {}

BirchFileItem::BirchFileItem(BirchFileItem&&) = default;

BirchFileItem::BirchFileItem(const BirchFileItem&) = default;

BirchFileItem& BirchFileItem::operator=(const BirchFileItem&) = default;

bool BirchFileItem::operator==(const BirchFileItem& rhs) const = default;

BirchFileItem::~BirchFileItem() = default;

BirchItemType BirchFileItem::GetType() const {
  return BirchItemType::kFile;
}

std::string BirchFileItem::ToString() const {
  std::stringstream ss;
  ss << "File item : {ranking: " << ranking()
     << ", title: " << base::UTF16ToUTF8(title())
     << ", file_path:" << file_path_ << ", timestamp: "
     << base::UTF16ToUTF8(base::TimeFormatShortDateAndTime(timestamp_))
     << ", file_id: " << file_id_ << "}" << ", icon_url: " << icon_url_ << "}";
  return ss.str();
}

void BirchFileItem::PerformAction() {
  RecordActionMetrics();
  NewWindowDelegate::GetPrimary()->OpenFile(file_path_);
}

void BirchFileItem::LoadIcon(LoadIconCallback callback) const {
  const auto backup_icon =
      ui::ImageModel::FromImageSkia(chromeos::GetIconForPath(file_path_, true));
  DownloadImageFromUrl(GURL(icon_url_), backup_icon, SecondaryIconType::kNoIcon,
                       std::move(callback));
}

// static
std::u16string BirchFileItem::GetTitle(
    const base::FilePath& file_path,
    const std::optional<std::string>& title) {
  if (title.has_value()) {
    return base::UTF8ToUTF16(title.value());
  }
  // Convert "/path/to/foo.txt" into just "foo".
  std::string filename = file_path.BaseName().RemoveExtension().value();
  return base::UTF8ToUTF16(filename);
}

////////////////////////////////////////////////////////////////////////////////

BirchWeatherItem::BirchWeatherItem(const std::u16string& weather_description,
                                   float temp_f,
                                   const GURL& icon_url)
    : BirchItem(weather_description,
                l10n_util::GetStringUTF16(IDS_ASH_BIRCH_WEATHER_SUBTITLE)),
      temp_f_(temp_f),
      icon_url_(icon_url) {
  set_addon_label(base::NumberToString16(GetTemperature(temp_f)));
}

BirchWeatherItem::BirchWeatherItem(BirchWeatherItem&&) = default;

BirchWeatherItem::BirchWeatherItem(const BirchWeatherItem&) = default;

BirchWeatherItem& BirchWeatherItem::operator=(const BirchWeatherItem&) =
    default;

bool BirchWeatherItem::operator==(const BirchWeatherItem& rhs) const = default;

BirchWeatherItem::~BirchWeatherItem() = default;

BirchItemType BirchWeatherItem::GetType() const {
  return BirchItemType::kWeather;
}

std::string BirchWeatherItem::ToString() const {
  std::stringstream ss;
  ss << "Weather item: {ranking: " << ranking()
     << ", title : " << base::UTF16ToUTF8(title()) << ", temp_f:" << temp_f_
     << "}";
  return ss.str();
}

void BirchWeatherItem::PerformAction() {
  RecordActionMetrics();
  // TODO(jamescook): Localize the query string.
  GURL url("https://google.com/search?q=weather");
  NewWindowDelegate::GetPrimary()->OpenUrl(
      url, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kNewForegroundTab);
}

void BirchWeatherItem::LoadIcon(LoadIconCallback callback) const {
  DownloadImageFromUrl(icon_url_, GetChromeBackupIcon(),
                       SecondaryIconType::kNoIcon, std::move(callback));
}

std::u16string BirchWeatherItem::GetAccessibleName() const {
  const int temp = GetTemperature(temp_f_);
  std::u16string temp_str =
      UseCelsius()
          ? l10n_util::GetStringFUTF16Int(
                IDS_ASH_AMBIENT_MODE_WEATHER_TEMPERATURE_IN_CELSIUS, temp)
          : l10n_util::GetStringFUTF16Int(
                IDS_ASH_AMBIENT_MODE_WEATHER_TEMPERATURE_IN_FAHRENHEIT, temp);
  return subtitle() + u" " + title() + u" " + temp_str;
}

void BirchWeatherItem::PerformAddonAction() {
  // Perform same action as the item.
  PerformAction();
}

BirchAddonType BirchWeatherItem::GetAddonType() const {
  return UseCelsius() ? BirchAddonType::kWeatherTempLabelC
                      : BirchAddonType::kWeatherTempLabelF;
}

// static
int BirchWeatherItem::GetTemperature(float temp_f) {
  return static_cast<int>(UseCelsius() ? (temp_f - 32) * 5 / 9 : temp_f);
}

// static
bool BirchWeatherItem::UseCelsius() {
  // Tests may not have a pref service.
  bool use_celsius = false;
  PrefService* pref_service = GetPrefService();
  if (pref_service) {
    use_celsius = pref_service->GetBoolean(prefs::kBirchUseCelsius);
  } else {
    CHECK_IS_TEST();
  }
  return use_celsius;
}

////////////////////////////////////////////////////////////////////////////////

BirchTabItem::BirchTabItem(const std::u16string& title,
                           const GURL& url,
                           const base::Time& timestamp,
                           const GURL& favicon_url,
                           const std::string& session_name,
                           const DeviceFormFactor& form_factor)
    : BirchItem(title, GetSubtitle(session_name, timestamp)),
      url_(url),
      timestamp_(timestamp),
      favicon_url_(favicon_url),
      session_name_(session_name),
      form_factor_(form_factor) {
  switch (form_factor) {
    case BirchTabItem::DeviceFormFactor::kDesktop:
      secondary_icon_type_ = SecondaryIconType::kTabFromDesktop;
      break;
    case BirchTabItem::DeviceFormFactor::kPhone:
      secondary_icon_type_ = SecondaryIconType::kTabFromPhone;
      break;
    case BirchTabItem::DeviceFormFactor::kTablet:
      secondary_icon_type_ = SecondaryIconType::kTabFromTablet;
      break;
    default:
      secondary_icon_type_ = SecondaryIconType::kNoIcon;
  }
}

BirchTabItem::BirchTabItem(BirchTabItem&&) = default;

BirchTabItem::BirchTabItem(const BirchTabItem&) = default;

BirchTabItem& BirchTabItem::operator=(const BirchTabItem&) = default;

bool BirchTabItem::operator==(const BirchTabItem& rhs) const = default;

BirchTabItem::~BirchTabItem() = default;

BirchItemType BirchTabItem::GetType() const {
  return BirchItemType::kTab;
}

std::string BirchTabItem::ToString() const {
  std::stringstream ss;
  ss << "Tab item: {ranking: " << ranking()
     << ", title: " << base::UTF16ToUTF8(title()) << ", url:" << url_
     << ", timestamp:" << timestamp_ << ", favicon_url:" << favicon_url_
     << ", session_name:" << session_name_
     << ", form_factor:" << static_cast<int>(form_factor_)
     << ", Secondary Icon Type: "
     << SecondaryIconTypeToString(secondary_icon_type_) << "}";
  return ss.str();
}

void BirchTabItem::PerformAction() {
  if (!url_.is_valid()) {
    LOG(ERROR) << "No valid URL for tab item";
    return;
  }
  RecordActionMetrics();
  NewWindowDelegate::GetPrimary()->OpenUrl(
      url_, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kSwitchToTab);
}

void BirchTabItem::LoadIcon(LoadIconCallback callback) const {
  GetFaviconImage(favicon_url_, /*is_page_url=*/false, GetChromeBackupIcon(),
                  secondary_icon_type_, std::move(callback));
}

// static
std::u16string BirchTabItem::GetSubtitle(const std::string& session_name,
                                         base::Time timestamp) {
  std::u16string prefix;
  if (timestamp < base::Time::Now().LocalMidnight()) {
    // Builds the string "Yesterday". We only show tabs within the last 24 hours
    // so we don't need to worry about days before yesterday.
    prefix =
        l10n_util::GetStringUTF16(IDS_ASH_BIRCH_RECENT_TAB_SUBTITLE_YESTERDAY);
  } else {
    // Builds a string like "12 hours ago". We only show tabs within the last
    // 24 hours so we don't need to worry about a day count.
    int hours = (base::Time::Now() - timestamp).InHours();
    prefix = l10n_util::GetPluralStringFUTF16(
        IDS_ASH_BIRCH_RECENT_TAB_SUBTITLE_PREFIX, hours);
  }

  // Builds a string like "From Chromebook".
  std::u16string suffix =
      l10n_util::GetStringFUTF16(IDS_ASH_BIRCH_RECENT_TAB_SUBTITLE_SUFFIX,
                                 base::UTF8ToUTF16(session_name));
  return prefix + u" · " + suffix;
}

////////////////////////////////////////////////////////////////////////////////

BirchLastActiveItem::BirchLastActiveItem(const std::u16string& title,
                                         const GURL& page_url,
                                         base::Time last_visit)
    : BirchItem(title, GetSubtitle(last_visit)), page_url_(page_url) {}

BirchLastActiveItem::BirchLastActiveItem(BirchLastActiveItem&&) = default;

BirchLastActiveItem::BirchLastActiveItem(const BirchLastActiveItem&) = default;

BirchLastActiveItem& BirchLastActiveItem::operator=(
    const BirchLastActiveItem&) = default;

bool BirchLastActiveItem::operator==(const BirchLastActiveItem& rhs) const =
    default;

BirchLastActiveItem::~BirchLastActiveItem() = default;

BirchItemType BirchLastActiveItem::GetType() const {
  return BirchItemType::kLastActive;
}

std::string BirchLastActiveItem::ToString() const {
  std::stringstream ss;
  ss << "Last active item: {ranking: " << ranking()
     << ", Title: " << base::UTF16ToUTF8(title()) << ", URL: " << page_url_
     << "}";
  return ss.str();
}

void BirchLastActiveItem::PerformAction() {
  if (!page_url_.is_valid()) {
    LOG(ERROR) << "No valid URL for last active item";
    return;
  }
  RecordActionMetrics();
  NewWindowDelegate::GetPrimary()->OpenUrl(
      page_url_, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kSwitchToTab);
}

void BirchLastActiveItem::LoadIcon(LoadIconCallback callback) const {
  GetFaviconImage(page_url_, /*is_page_url=*/true, GetChromeBackupIcon(),
                  SecondaryIconType::kNoIcon, std::move(callback));
}

// static
std::u16string BirchLastActiveItem::GetSubtitle(base::Time last_visit) {
  std::u16string prefix;
  if (last_visit < base::Time::Now().LocalMidnight() - base::Days(1)) {
    // If the last visit was before yesterday, show "X days ago".
    int days = (base::Time::Now() - last_visit).InDays();
    prefix = l10n_util::GetPluralStringFUTF16(
        IDS_ASH_BIRCH_LAST_ACTIVE_SUBTITLE_DAYS_AGO, days);
  } else if (last_visit < base::Time::Now().LocalMidnight()) {
    // If the last visit was yesterday show "Yesterday", which is a common case
    // in the mornings.
    prefix =
        l10n_util::GetStringUTF16(IDS_ASH_BIRCH_LAST_ACTIVE_SUBTITLE_YESTERDAY);
  } else {
    // Builds a string like "12 hours ago".
    int hours = (base::Time::Now() - last_visit).InHours();
    prefix = l10n_util::GetPluralStringFUTF16(
        IDS_ASH_BIRCH_LAST_ACTIVE_SUBTITLE_PREFIX, hours);
  }

  // Builds a string like "Continue browsing".
  std::u16string suffix =
      l10n_util::GetStringUTF16(IDS_ASH_BIRCH_LAST_ACTIVE_SUBTITLE_SUFFIX);
  return prefix + u" · " + suffix;
}

////////////////////////////////////////////////////////////////////////////////

BirchMostVisitedItem::BirchMostVisitedItem(const std::u16string& title,
                                           const GURL& page_url)
    : BirchItem(title, GetSubtitle()), page_url_(page_url) {}

BirchMostVisitedItem::BirchMostVisitedItem(BirchMostVisitedItem&&) = default;

BirchMostVisitedItem::BirchMostVisitedItem(const BirchMostVisitedItem&) =
    default;

BirchMostVisitedItem& BirchMostVisitedItem::operator=(
    const BirchMostVisitedItem&) = default;

bool BirchMostVisitedItem::operator==(const BirchMostVisitedItem& rhs) const =
    default;

BirchMostVisitedItem::~BirchMostVisitedItem() = default;

BirchItemType BirchMostVisitedItem::GetType() const {
  return BirchItemType::kMostVisited;
}

std::string BirchMostVisitedItem::ToString() const {
  std::stringstream ss;
  ss << "Most Visited item: {ranking: " << ranking()
     << ", Title: " << base::UTF16ToUTF8(title()) << ", Page URL: " << page_url_
     << "}";
  return ss.str();
}

void BirchMostVisitedItem::PerformAction() {
  if (!page_url_.is_valid()) {
    LOG(ERROR) << "No valid URL for most visited item";
    return;
  }
  RecordActionMetrics();
  NewWindowDelegate::GetPrimary()->OpenUrl(
      page_url_, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kSwitchToTab);
}

void BirchMostVisitedItem::LoadIcon(LoadIconCallback callback) const {
  GetFaviconImage(page_url_, /*is_page_url=*/true, GetChromeBackupIcon(),
                  SecondaryIconType::kNoIcon, std::move(callback));
}

// static
std::u16string BirchMostVisitedItem::GetSubtitle() {
  return l10n_util::GetStringUTF16(IDS_ASH_BIRCH_MOST_VISITED_SUBTITLE);
}

////////////////////////////////////////////////////////////////////////////////

BirchSelfShareItem::BirchSelfShareItem(
    const std::u16string& guid,
    const std::u16string& title,
    const GURL& url,
    const base::Time& shared_time,
    const std::u16string& device_name,
    const SecondaryIconType& secondary_icon_type,
    base::RepeatingClosure callback)
    : BirchItem(title, GetSubtitle(device_name, shared_time)),
      guid_(guid),
      url_(url),
      shared_time_(shared_time),
      secondary_icon_type_(secondary_icon_type),
      activation_callback_(std::move(callback)) {}

BirchSelfShareItem::BirchSelfShareItem(BirchSelfShareItem&&) = default;

BirchSelfShareItem::BirchSelfShareItem(const BirchSelfShareItem&) = default;

BirchSelfShareItem& BirchSelfShareItem::operator=(const BirchSelfShareItem&) =
    default;

bool BirchSelfShareItem::operator==(const BirchSelfShareItem& rhs) const =
    default;

BirchSelfShareItem::~BirchSelfShareItem() = default;

BirchItemType BirchSelfShareItem::GetType() const {
  return BirchItemType::kSelfShare;
}

std::string BirchSelfShareItem::ToString() const {
  std::stringstream ss;
  ss << "Self Share item: {ranking: " << ranking()
     << ", Title: " << base::UTF16ToUTF8(title())
     << ", Device Name: " << base::UTF16ToUTF8(subtitle())
     << ", GUID: " << guid_ << ", Shared Time: " << shared_time_
     << ", URL: " << url_ << ", Secondary Icon Type: "
     << SecondaryIconTypeToString(secondary_icon_type_) << "}";
  return ss.str();
}

void BirchSelfShareItem::PerformAction() {
  if (!url_.is_valid()) {
    LOG(ERROR) << "No valid URL for self "
                  "share item";
    return;
  }
  if (activation_callback_) {
    activation_callback_.Run();
  }
  RecordActionMetrics();
  NewWindowDelegate::GetPrimary()->OpenUrl(
      url_, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kSwitchToTab);
}

void BirchSelfShareItem::LoadIcon(LoadIconCallback callback) const {
  GetFaviconImage(url_, /*is_page_url=*/true, GetChromeBackupIcon(),
                  secondary_icon_type_, std::move(callback));
}

// static
std::u16string BirchSelfShareItem::GetSubtitle(
    const std::u16string& device_name,
    base::Time shared_time) {
  std::u16string prefix;
  if (shared_time < base::Time::Now().LocalMidnight()) {
    // Builds the string "Yesterday". We only show tabs within the last 24 hours
    // so we don't need to worry about days before yesterday.
    prefix =
        l10n_util::GetStringUTF16(IDS_ASH_BIRCH_SELF_SHARE_SUBTITLE_YESTERDAY);
  } else {
    // Builds a string like "12 hours ago". We only show tabs within the last
    // 24 hours so we don't need to worry about a day count.
    const int hours = (base::Time::Now() - shared_time).InHours();
    prefix = l10n_util::GetPluralStringFUTF16(
        IDS_ASH_BIRCH_SELF_SHARE_SUBTITLE_PREFIX, hours);
  }

  // Builds a string like "Sent from Chromebook".
  std::u16string suffix = l10n_util::GetStringFUTF16(
      IDS_ASH_BIRCH_SELF_SHARE_SUBTITLE_SUFFIX, device_name);
  return prefix + u" · " + suffix;
}

////////////////////////////////////////////////////////////////////////////////

BirchLostMediaItem::BirchLostMediaItem(
    const GURL& source_url,
    const std::u16string& media_title,
    const std::optional<ui::ImageModel>& backup_icon,
    const SecondaryIconType& secondary_icon_type,
    base::RepeatingClosure activation_callback)
    : BirchItem(media_title, GetSubtitle(secondary_icon_type)),
      source_url_(source_url),
      media_title_(media_title),
      backup_icon_(backup_icon),
      secondary_icon_type_(secondary_icon_type),
      activation_callback_(std::move(activation_callback)) {}

BirchLostMediaItem::BirchLostMediaItem(BirchLostMediaItem&&) = default;

BirchLostMediaItem::BirchLostMediaItem(const BirchLostMediaItem&) = default;

BirchLostMediaItem& BirchLostMediaItem::operator=(const BirchLostMediaItem&) =
    default;

bool BirchLostMediaItem::operator==(const BirchLostMediaItem& rhs) const =
    default;

BirchLostMediaItem::~BirchLostMediaItem() = default;

BirchItemType BirchLostMediaItem::GetType() const {
  return BirchItemType::kLostMedia;
}

std::string BirchLostMediaItem::ToString() const {
  std::stringstream ss;
  ss << "Lost Media item: {ranking: " << ranking()
     << ", Source Url: " << source_url_ << ", Media Title: " << media_title_
     << ", Secondary Icon Type: "
     << SecondaryIconTypeToString(secondary_icon_type_) << "}";
  return ss.str();
}

void BirchLostMediaItem::PerformAction() {
  // This needs to be called before running `activation_callback_` because
  // running the callback may cause the item to be deleted.
  RecordActionMetrics();
  if (activation_callback_) {
    activation_callback_.Run();
  }
}

void BirchLostMediaItem::LoadIcon(LoadIconCallback callback) const {
  GetFaviconImage(source_url_, /*is_page_url=*/true,
                  backup_icon_.value_or(GetChromeBackupIcon()),
                  secondary_icon_type_, std::move(callback));
}

// static
std::u16string BirchLostMediaItem::GetSubtitle(SecondaryIconType type) {
  return l10n_util::GetStringUTF16(
      type == SecondaryIconType::kLostMediaVideoConference
          ? IDS_ASH_BIRCH_LOST_MEDIA_VIDEO_CONFERENCE_TAB_SUBTITLE
          : IDS_ASH_BIRCH_LOST_MEDIA_MEDIA_TAB_SUBTITLE);
}

////////////////////////////////////////////////////////////////////////////////

BirchCoralItem::BirchCoralItem(const std::u16string& coral_title,
                               const std::u16string& coral_text)
    : BirchItem(coral_title, coral_text) {
  set_addon_label(u"Show");
}

BirchCoralItem::BirchCoralItem(BirchCoralItem&&) = default;

BirchCoralItem::BirchCoralItem(const BirchCoralItem&) = default;

BirchCoralItem& BirchCoralItem::operator=(const BirchCoralItem&) = default;

bool BirchCoralItem::operator==(const BirchCoralItem& rhs) const = default;

BirchCoralItem::~BirchCoralItem() = default;

BirchItemType BirchCoralItem::GetType() const {
  return BirchItemType::kCoral;
}

std::string BirchCoralItem::ToString() const {
  auto root = base::Value::Dict().Set(
      "Coral item",
      base::Value::Dict().Set("Title", title()).Set("Subtitle", subtitle()));
  return base::WriteJson(root).value_or(std::string());
}

void BirchCoralItem::PerformAction() {
  // TODO(yulunwu) restore all applicable items in group to active desk.
  // Open all related tabs in the same window with the default window bounds.
  // Open related app(s) in its last used window state.
}

void BirchCoralItem::LoadIcon(LoadIconCallback callback) const {
  // TODO(yulunwu) load icons for first four birch restore items.
}

void BirchCoralItem::PerformAddonAction() {
  auto* overview_session = OverviewController::Get()->overview_session();
  CHECK(overview_session);
  overview_session->ToggleTabAppSelectionMenu();
}

BirchAddonType BirchCoralItem::GetAddonType() const {
  return BirchAddonType::kButton;
}

std::u16string BirchCoralItem::GetAddonAccessibleName() const {
  return u"Show";
}

////////////////////////////////////////////////////////////////////////////////

BirchReleaseNotesItem::BirchReleaseNotesItem(
    const std::u16string& release_notes_title,
    const std::u16string& release_notes_text,
    const GURL& url,
    const base::Time first_seen)
    : BirchItem(release_notes_title, release_notes_text),
      url_(url),
      first_seen_(first_seen) {}

BirchReleaseNotesItem::~BirchReleaseNotesItem() = default;

BirchItemType BirchReleaseNotesItem::GetType() const {
  return BirchItemType::kReleaseNotes;
}

std::string BirchReleaseNotesItem::ToString() const {
  std::stringstream ss;
  ss << "release_notes_title: " << base::UTF16ToUTF8(title())
     << ", release_notes_text:" << base::UTF16ToUTF8(subtitle())
     << ", url:" << url_ << ", ranking: " << ranking()
     << ", first seen: " << first_seen_;
  return ss.str();
}

void BirchReleaseNotesItem::PerformAction() {
  if (!url_.is_valid()) {
    LOG(ERROR) << "No valid URL for release notes item";
    return;
  }
  RecordActionMetrics();
  NewWindowDelegate::GetPrimary()->OpenUrl(
      url_, NewWindowDelegate::OpenUrlFrom::kUserInteraction,
      NewWindowDelegate::Disposition::kNewForegroundTab);
}

void BirchReleaseNotesItem::LoadIcon(LoadIconCallback callback) const {
  std::move(callback).Run(
      ui::ResourceBundle::GetSharedInstance().GetThemedLottieImageNamed(
          IDR_BIRCH_RELEASE_NOTES_ICON),
      SecondaryIconType::kNoIcon);
}

}  // namespace ash