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

#include <algorithm>

#include "ash/birch/birch_item.h"
#include "ash/constants/ash_switches.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/time/time.h"
#include "base/types/cxx23_to_underlying.h"

namespace {
// How long release notes remain top ranked.
constexpr base::TimeDelta kMinutesWhereReleaseNotesIsTopRanked =
    base::Minutes(10);
}  // namespace

namespace ash {

BirchRanker::BirchRanker(base::Time now) : now_(now) {}

BirchRanker::~BirchRanker() = default;

void BirchRanker::RankCalendarItems(std::vector<BirchCalendarItem>* items) {
  CHECK(items);
  // Sort the events by start time so that we can search forward in the vector
  // to find upcoming events. Events with the same start time will be ranked
  // according so the response status, with the `kAccepted` response being the
  // highest priority.
  std::sort(items->begin(), items->end(),
            [](const BirchCalendarItem& a, const BirchCalendarItem& b) {
              if (a.start_time() == b.start_time()) {
                return base::to_underlying(a.response_status()) <
                       base::to_underlying(b.response_status());
              }
              return a.start_time() < b.start_time();
            });

  const bool is_morning = IsMorning();
  const bool is_evening = IsEvening();
  bool found_upcoming_event = false;
  bool found_tomorrow_event = false;

  for (BirchCalendarItem& item : *items) {
    // Declined events should not be ranked.
    if (item.response_status() ==
        BirchCalendarItem::ResponseStatus::kDeclined) {
      continue;
    }

    // All-day events have low priority. We only show all-day events from today
    // (e.g. ongoing all-day events).
    if (item.all_day_event() && IsOngoingEvent(item)) {
      item.set_ranking(39.f);
      continue;
    }

    // Non-all-day ongoing events have priority in the morning.
    if (is_morning && IsOngoingEvent(item)) {
      item.set_ranking(6.f);
      continue;
    }

    // The first upcoming event has the same priority in the morning.
    if (is_morning && now_ < item.start_time() && !found_upcoming_event) {
      found_upcoming_event = true;
      item.set_ranking(6.f);
      continue;
    }

    // Non-all-day ongoing events have medium priority all day.
    if (IsOngoingEvent(item)) {
      item.set_ranking(12.f);
      continue;
    }

    // Events starting in the next 30 minutes has medium priority all day.
    if (now_ <= item.start_time() &&
        item.start_time() < now_ + base::Minutes(30)) {
      item.set_ranking(15.f);
      continue;
    }

    // In the evening, the first event from tomorrow has low priority.
    if (is_evening && IsTomorrowEvent(item) && !found_tomorrow_event) {
      found_tomorrow_event = true;
      item.set_ranking(28.f);
      continue;
    }
  }
}

void BirchRanker::RankAttachmentItems(std::vector<BirchAttachmentItem>* items) {
  CHECK(items);

  // Sort the attachments by their event start time.
  std::sort(items->begin(), items->end(),
            [](const BirchAttachmentItem& a, const BirchAttachmentItem& b) {
              return a.start_time() < b.start_time();
            });

  const bool is_morning = IsMorning();

  for (BirchAttachmentItem& item : *items) {
    // Attachments for ongoing events have high priority in the morning and
    // medium priority the rest of the day.
    const bool is_ongoing = item.start_time() <= now_ && now_ < item.end_time();
    if (is_ongoing) {
      item.set_ranking(is_morning ? 7.f : 13.f);
      continue;
    }

    // Attachments for events starting in the next 30 minutes have medium
    // priority.
    if (now_ <= item.start_time() &&
        item.start_time() < now_ + base::Minutes(30)) {
      item.set_ranking(16.f);
      continue;
    }
  }
}

void BirchRanker::RankFileSuggestItems(std::vector<BirchFileItem>* items) {
  CHECK(items);

  // Sort the file suggestions by their timestamp, descending.
  std::sort(items->begin(), items->end(),
            [](const BirchFileItem& a, const BirchFileItem& b) {
              return b.timestamp() < a.timestamp();
            });

  // TODO(b/305094126): Differentiate between modify time and share time.
  // Currently the single timestamp represents both.
  for (BirchFileItem& item : *items) {
    // Items modified/shared recently have high priority.
    if (now_ - base::Hours(1) < item.timestamp()) {
      item.set_ranking(22.f);
      continue;
    }
    // Items modified/shared today have medium priority.
    if (now_ - base::Days(1) < item.timestamp()) {
      item.set_ranking(35.f);
      continue;
    }
    // Items modified/shared this week have low priority.
    if (now_ - base::Days(7) < item.timestamp()) {
      item.set_ranking(43.f);
      continue;
    }
  }
}

void BirchRanker::RankRecentTabItems(std::vector<BirchTabItem>* items) {
  CHECK(items);

  // Sort the recent tabs by their timestamp, descending.
  std::sort(items->begin(), items->end(),
            [](const BirchTabItem& a, const BirchTabItem& b) {
              return b.timestamp() < a.timestamp();
            });

  for (BirchTabItem& item : *items) {
    const bool is_mobile =
        item.form_factor() == BirchTabItem::DeviceFormFactor::kPhone ||
        item.form_factor() == BirchTabItem::DeviceFormFactor::kTablet;
    // Very recent mobile items have high priority.
    if (is_mobile && now_ - base::Minutes(5) < item.timestamp()) {
      item.set_ranking(17.f);
      continue;
    }
    const bool is_desktop =
        item.form_factor() == BirchTabItem::DeviceFormFactor::kDesktop;
    // Desktop items from the last hour have medium priority.
    if (is_desktop && now_ - base::Hours(1) < item.timestamp()) {
      item.set_ranking(20.f);
      continue;
    }
    // Desktop items from the last day have low priority.
    if (is_desktop && now_ - base::Days(1) < item.timestamp()) {
      item.set_ranking(33.f);
      continue;
    }
  }
}

void BirchRanker::RankLastActiveItems(std::vector<BirchLastActiveItem>* items) {
  CHECK(items);

  for (BirchLastActiveItem& item : *items) {
    if (IsMorning()) {
      item.set_ranking(8.f);
    }
  }
}

void BirchRanker::RankMostVisitedItems(
    std::vector<BirchMostVisitedItem>* items) {
  CHECK(items);

  for (BirchMostVisitedItem& item : *items) {
    if (IsMorning()) {
      item.set_ranking(9.f);
      continue;
    }
  }
}

void BirchRanker::RankSelfShareItems(std::vector<BirchSelfShareItem>* items) {
  CHECK(items);

  // Sort the self share items by their shared time, descending.
  std::sort(items->begin(), items->end(),
            [](const BirchSelfShareItem& a, const BirchSelfShareItem& b) {
              return b.shared_time() < a.shared_time();
            });

  for (BirchSelfShareItem& item : *items) {
    if (now_ - base::Hours(1) < item.shared_time()) {
      item.set_ranking(14.f);
      continue;
    }
    if (now_ - base::Days(1) < item.shared_time()) {
      item.set_ranking(30.f);
      continue;
    }
    if (now_ - base::Days(2) < item.shared_time()) {
      item.set_ranking(40.f);
      continue;
    }
  }
}

void BirchRanker::RankLostMediaItems(std::vector<BirchLostMediaItem>* items) {
  CHECK(items);
  for (BirchLostMediaItem& item : *items) {
    item.set_ranking(11.0f);
  }
}

void BirchRanker::RankWeatherItems(std::vector<BirchWeatherItem>* items) {
  if (!items->empty() && IsMorning()) {
    (*items)[0].set_ranking(5.f);
  }

  // TODO(b/305094126): Figure out how to query the next day's weather and show
  // it in the evenings (8pm to midnight).
}

void BirchRanker::RankReleaseNotesItems(
    std::vector<BirchReleaseNotesItem>* items) {
  for (BirchReleaseNotesItem& item : *items) {
    item.set_ranking(GetReleaseNotesItemRanking(item));
  }
}

void BirchRanker::RankCoralItems(std::vector<BirchCoralItem>* items) {
  CHECK(items);
  for (BirchCoralItem& item : *items) {
    // TODO(yulunwu) Set ranking for coral items
    item.set_ranking(100.0f);
  }
}

float BirchRanker::GetReleaseNotesItemRanking(
    const BirchReleaseNotesItem& item) const {
  const base::TimeDelta elapsed_time = now_ - item.first_seen();
  if (elapsed_time <= kMinutesWhereReleaseNotesIsTopRanked) {
    return 3.0f;
  }
  if (elapsed_time <= base::Hours(1)) {
    return 18.0f;
  }
  if (elapsed_time <= base::Hours(24)) {
    return 31.0f;
  }
  return 47.0f;
}

bool BirchRanker::IsMorning() const {
  auto* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(switches::kBirchIsMorning)) {
    return true;
  }
  if (command_line->HasSwitch(switches::kBirchIsEvening)) {
    return false;
  }
  base::Time last_midnight = now_.LocalMidnight();
  base::Time five_am_today = last_midnight + base::Hours(5);
  base::Time noon_today = last_midnight + base::Hours(12);
  return five_am_today <= now_ && now_ < noon_today;
}

bool BirchRanker::IsEvening() const {
  auto* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(switches::kBirchIsEvening)) {
    return true;
  }
  if (command_line->HasSwitch(switches::kBirchIsMorning)) {
    return false;
  }
  base::Time last_midnight = now_.LocalMidnight();
  base::Time five_pm_today = last_midnight + base::Hours(17);
  return five_pm_today <= now_;
}

bool BirchRanker::IsOngoingEvent(const BirchCalendarItem& item) const {
  return item.start_time() <= now_ && now_ < item.end_time();
}

bool BirchRanker::IsTomorrowEvent(const BirchCalendarItem& item) const {
  return now_.LocalMidnight() + base::Days(1) < item.start_time();
}

}  // namespace ash