#include "ash/assistant/assistant_alarm_timer_controller_impl.h"
#include <cmath>
#include <utility>
#include "ash/assistant/assistant_controller_impl.h"
#include "ash/assistant/assistant_notification_controller_impl.h"
#include "ash/assistant/util/deep_link_util.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/i18n/message_formatter.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chromeos/ash/services/assistant/public/cpp/assistant_service.h"
#include "chromeos/ash/services/assistant/public/cpp/features.h"
#include "chromeos/ash/services/libassistant/public/cpp/assistant_notification.h"
#include "chromeos/ash/services/libassistant/public/cpp/assistant_timer.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "third_party/icu/source/common/unicode/utypes.h"
#include "third_party/icu/source/i18n/unicode/measfmt.h"
#include "third_party/icu/source/i18n/unicode/measunit.h"
#include "third_party/icu/source/i18n/unicode/measure.h"
#include "ui/base/l10n/l10n_util.h"
namespace ash {
namespace {
using assistant::AssistantNotification;
using assistant::AssistantNotificationButton;
using assistant::AssistantNotificationPriority;
using assistant::AssistantTimer;
using assistant::AssistantTimerState;
using assistant::util::AlarmTimerAction;
constexpr char kTimerNotificationGroupingKey[] = "assistant/timer";
constexpr char kTimerNotificationIdPrefix[] = "assistant/timer";
std::string ToFormattedTimeString(base::TimeDelta time,
UMeasureFormatWidth width) {
DCHECK(width == UMEASFMT_WIDTH_NARROW || width == UMEASFMT_WIDTH_NUMERIC);
const auto createHour = icu::MeasureUnit::createHour;
const auto createMinute = icu::MeasureUnit::createMinute;
const auto createSecond = icu::MeasureUnit::createSecond;
const int64_t total_seconds = std::abs(std::round(time.InSecondsF()));
const int32_t hours = total_seconds / 3600;
const int32_t minutes = (total_seconds - hours * 3600) / 60;
const int32_t seconds = total_seconds % 60;
UErrorCode status = U_ZERO_ERROR;
std::vector<icu::Measure> measures;
if (hours)
measures.emplace_back(hours, createHour(status), status);
if (minutes || width == UMEASFMT_WIDTH_NUMERIC)
measures.emplace_back(minutes, createMinute(status), status);
if (seconds || width == UMEASFMT_WIDTH_NUMERIC)
measures.emplace_back(seconds, createSecond(status), status);
icu::UnicodeString unicode_message;
icu::FieldPosition field_position = icu::FieldPosition::DONT_CARE;
icu::MeasureFormat measure_format(icu::Locale::getDefault(), width, status);
measure_format.formatMeasures(measures.data(), measures.size(),
unicode_message, field_position, status);
std::string formatted_time;
if (U_SUCCESS(status)) {
unicode_message.toUTF8String(formatted_time);
} else {
LOG(ERROR) << "Error formatting time string: " << status;
formatted_time =
base::UTF16ToUTF8(base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(
width == UMEASFMT_WIDTH_NARROW
? IDS_ASSISTANT_TIMER_NOTIFICATION_FORMATTED_TIME_NARROW_FALLBACK
: IDS_ASSISTANT_TIMER_NOTIFICATION_FORMATTED_TIME_NUMERIC_FALLBACK),
hours, minutes, seconds));
}
if (time.InSeconds() < 0) {
formatted_time =
base::UTF16ToUTF8(base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(
IDS_ASSISTANT_TIMER_NOTIFICATION_FORMATTED_TIME_NEGATE),
formatted_time));
}
return formatted_time;
}
std::string ToOriginalDurationString(const AssistantTimer& timer) {
return ToFormattedTimeString(timer.original_duration, UMEASFMT_WIDTH_NARROW);
}
std::string ToRemainingTimeString(const AssistantTimer& timer) {
return ToFormattedTimeString(timer.remaining_time, UMEASFMT_WIDTH_NUMERIC);
}
std::string CreateTimerNotificationId(const AssistantTimer& timer) {
return std::string(kTimerNotificationIdPrefix) + timer.id;
}
std::string CreateTimerNotificationTitle(const AssistantTimer& timer) {
return ToRemainingTimeString(timer);
}
std::string CreateTimerNotificationMessage(const AssistantTimer& timer) {
if (timer.label.empty()) {
return base::UTF16ToUTF8(
base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(
timer.state == AssistantTimerState::kFired
? IDS_ASSISTANT_TIMER_NOTIFICATION_MESSAGE_WHEN_FIRED
: IDS_ASSISTANT_TIMER_NOTIFICATION_MESSAGE),
ToOriginalDurationString(timer)));
}
return base::UTF16ToUTF8(base::i18n::MessageFormatter::FormatWithNumberedArgs(
l10n_util::GetStringUTF16(
timer.state == AssistantTimerState::kFired
? IDS_ASSISTANT_TIMER_NOTIFICATION_MESSAGE_WHEN_FIRED_WITH_LABEL
: IDS_ASSISTANT_TIMER_NOTIFICATION_MESSAGE_WITH_LABEL),
ToOriginalDurationString(timer), timer.label));
}
std::vector<AssistantNotificationButton> CreateTimerNotificationButtons(
const AssistantTimer& timer) {
std::vector<AssistantNotificationButton> buttons;
if (timer.state != AssistantTimerState::kFired) {
if (timer.state == AssistantTimerState::kPaused) {
buttons.push_back({l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_RESUME_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kResumeTimer, timer.id)
.value(),
false});
} else {
buttons.push_back({l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_PAUSE_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kPauseTimer, timer.id)
.value(),
false});
}
}
if (timer.state == AssistantTimerState::kFired) {
buttons.push_back(
{l10n_util::GetStringUTF8(IDS_ASSISTANT_TIMER_NOTIFICATION_STOP_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
.value(),
true});
buttons.push_back(
{l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_ADD_1_MIN_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kAddTimeToTimer, timer.id, base::Minutes(1))
.value(),
false});
} else {
buttons.push_back({l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_CANCEL_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
.value(),
true});
}
return buttons;
}
AssistantNotificationPriority CreateTimerNotificationPriority(
const AssistantTimer& timer) {
if (timer.state == AssistantTimerState::kFired)
return AssistantNotificationPriority::kHigh;
constexpr base::TimeDelta kPopupThreshold = base::Seconds(6);
const base::TimeDelta lifetime =
base::Time::Now() - timer.creation_time.value_or(base::Time::Now());
if (lifetime >= kPopupThreshold)
return AssistantNotificationPriority::kLow;
return AssistantNotificationPriority::kDefault;
}
AssistantNotification CreateTimerNotification(
const AssistantTimer& timer,
const AssistantNotification* existing_notification = nullptr) {
AssistantNotification notification;
notification.title = CreateTimerNotificationTitle(timer);
notification.message = CreateTimerNotificationMessage(timer);
notification.buttons = CreateTimerNotificationButtons(timer);
notification.client_id = CreateTimerNotificationId(timer);
notification.grouping_key = kTimerNotificationGroupingKey;
notification.priority = CreateTimerNotificationPriority(timer);
notification.remove_on_click = false;
notification.is_pinned = true;
if (existing_notification &&
notification.priority > existing_notification->priority) {
notification.renotify = true;
}
return notification;
}
bool ShouldAllowUpdateFromLibAssistant(const AssistantTimer& original,
const AssistantTimer& update) {
DCHECK_EQ(original.id, update.id);
return !original.IsEqualInLibAssistantTo(update);
}
}
AssistantAlarmTimerControllerImpl::AssistantAlarmTimerControllerImpl(
AssistantControllerImpl* assistant_controller)
: assistant_controller_(assistant_controller) {
model_.AddObserver(this);
assistant_controller_observation_.Observe(AssistantController::Get());
}
AssistantAlarmTimerControllerImpl::~AssistantAlarmTimerControllerImpl() {
model_.RemoveObserver(this);
}
void AssistantAlarmTimerControllerImpl::SetAssistant(
assistant::Assistant* assistant) {
assistant_ = assistant;
}
const AssistantAlarmTimerModel* AssistantAlarmTimerControllerImpl::GetModel()
const {
return &model_;
}
void AssistantAlarmTimerControllerImpl::OnTimerStateChanged(
const std::vector<AssistantTimer>& new_or_updated_timers) {
for (const auto* old_timer : model_.GetAllTimers()) {
if (!base::Contains(new_or_updated_timers, old_timer->id,
&AssistantTimer::id)) {
model_.RemoveTimer(old_timer->id);
}
}
for (const auto& new_or_updated_timer : new_or_updated_timers) {
const auto* original_timer = model_.GetTimerById(new_or_updated_timer.id);
const bool is_new_timer = original_timer == nullptr;
if (is_new_timer || ShouldAllowUpdateFromLibAssistant(
*original_timer, new_or_updated_timer)) {
model_.AddOrUpdateTimer(std::move(new_or_updated_timer));
}
}
}
void AssistantAlarmTimerControllerImpl::OnAssistantControllerConstructed() {
AssistantState::Get()->AddObserver(this);
}
void AssistantAlarmTimerControllerImpl::OnAssistantControllerDestroying() {
AssistantState::Get()->RemoveObserver(this);
}
void AssistantAlarmTimerControllerImpl::OnDeepLinkReceived(
assistant::util::DeepLinkType type,
const std::map<std::string, std::string>& params) {
using assistant::util::DeepLinkParam;
using assistant::util::DeepLinkType;
if (type != DeepLinkType::kAlarmTimer)
return;
const absl::optional<AlarmTimerAction>& action =
assistant::util::GetDeepLinkParamAsAlarmTimerAction(params);
if (!action.has_value())
return;
const absl::optional<std::string>& alarm_timer_id =
assistant::util::GetDeepLinkParam(params, DeepLinkParam::kId);
if (!alarm_timer_id.has_value())
return;
const absl::optional<base::TimeDelta>& duration =
assistant::util::GetDeepLinkParamAsTimeDelta(params,
DeepLinkParam::kDurationMs);
PerformAlarmTimerAction(action.value(), alarm_timer_id.value(), duration);
}
void AssistantAlarmTimerControllerImpl::OnAssistantStatusChanged(
assistant::AssistantStatus status) {
if (status == assistant::AssistantStatus::NOT_READY)
model_.RemoveAllTimers();
}
void AssistantAlarmTimerControllerImpl::OnTimerAdded(
const AssistantTimer& timer) {
ScheduleNextTick(timer);
assistant_controller_->notification_controller()->AddOrUpdateNotification(
CreateTimerNotification(timer));
}
void AssistantAlarmTimerControllerImpl::OnTimerUpdated(
const AssistantTimer& timer) {
ScheduleNextTick(timer);
auto* notification_controller =
assistant_controller_->notification_controller();
const auto* existing_notification =
notification_controller->model()->GetNotificationById(
CreateTimerNotificationId(timer));
if (existing_notification) {
notification_controller->AddOrUpdateNotification(
CreateTimerNotification(timer, existing_notification));
}
}
void AssistantAlarmTimerControllerImpl::OnTimerRemoved(
const AssistantTimer& timer) {
tickers_.erase(timer.id);
assistant_controller_->notification_controller()->RemoveNotificationById(
CreateTimerNotificationId(timer), false);
}
void AssistantAlarmTimerControllerImpl::PerformAlarmTimerAction(
const AlarmTimerAction& action,
const std::string& alarm_timer_id,
const absl::optional<base::TimeDelta>& duration) {
DCHECK(assistant_);
switch (action) {
case AlarmTimerAction::kAddTimeToTimer:
if (!duration.has_value()) {
LOG(ERROR) << "Ignoring add time to timer action duration.";
return;
}
assistant_->AddTimeToTimer(alarm_timer_id, duration.value());
break;
case AlarmTimerAction::kPauseTimer:
DCHECK(!duration.has_value());
assistant_->PauseTimer(alarm_timer_id);
break;
case AlarmTimerAction::kRemoveAlarmOrTimer:
DCHECK(!duration.has_value());
assistant_->RemoveAlarmOrTimer(alarm_timer_id);
break;
case AlarmTimerAction::kResumeTimer:
DCHECK(!duration.has_value());
assistant_->ResumeTimer(alarm_timer_id);
break;
}
}
void AssistantAlarmTimerControllerImpl::ScheduleNextTick(
const AssistantTimer& timer) {
auto& ticker = tickers_[timer.id];
if (ticker.IsRunning())
return;
int millis_to_next_full_sec = timer.remaining_time.InMilliseconds() % 1000;
if (millis_to_next_full_sec < 0)
millis_to_next_full_sec = 1000 + millis_to_next_full_sec;
if (millis_to_next_full_sec == 0)
millis_to_next_full_sec = 1000;
ticker.Start(FROM_HERE, base::Milliseconds(millis_to_next_full_sec),
base::BindOnce(&AssistantAlarmTimerControllerImpl::Tick,
base::Unretained(this), timer.id));
}
void AssistantAlarmTimerControllerImpl::Tick(const std::string& timer_id) {
const auto* timer = model_.GetTimerById(timer_id);
DCHECK(timer);
if (timer->state == AssistantTimerState::kPaused)
return;
AssistantTimer updated_timer(*timer);
updated_timer.remaining_time = updated_timer.fire_time - base::Time::Now();
if (std::round(updated_timer.remaining_time.InSecondsF()) <= 0.f)
updated_timer.state = AssistantTimerState::kFired;
model_.AddOrUpdateTimer(std::move(updated_timer));
}
}