blob: 7bb52eb49e196770d10d7a4449f83c4996070b95 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#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/i18n/message_formatter.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chromeos/services/assistant/public/cpp/assistant_notification.h"
#include "chromeos/services/assistant/public/cpp/assistant_service.h"
#include "chromeos/services/assistant/public/cpp/features.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::util::AlarmTimerAction;
using chromeos::assistant::AssistantNotification;
using chromeos::assistant::AssistantNotificationButton;
using chromeos::assistant::AssistantNotificationPriority;
using chromeos::assistant::features::IsTimersV2Enabled;
// Grouping key and ID prefix for timer notifications.
constexpr char kTimerNotificationGroupingKey[] = "assistant/timer";
constexpr char kTimerNotificationIdPrefix[] = "assistant/timer";
// Helpers ---------------------------------------------------------------------
std::string ToFormattedTimeString(base::TimeDelta time,
UMeasureFormatWidth width) {
DCHECK(width == UMEASFMT_WIDTH_NARROW || width == UMEASFMT_WIDTH_NUMERIC);
// Method aliases to prevent line-wrapping below.
const auto createHour = icu::MeasureUnit::createHour;
const auto createMinute = icu::MeasureUnit::createMinute;
const auto createSecond = icu::MeasureUnit::createSecond;
// We round |total_seconds| to the nearest full second since we don't display
// our time string w/ millisecond granularity and because this method is
// called very near to full second boundaries. Otherwise, values like 4.99 sec
// would be displayed to the user as "0:04" instead of the expected "0:05".
const int64_t total_seconds = std::abs(std::round(time.InSecondsF()));
// Calculate time in hours/minutes/seconds.
const int32_t hours = total_seconds / 3600;
const int32_t minutes = (total_seconds - hours * 3600) / 60;
const int32_t seconds = total_seconds % 60;
// Success of the ICU APIs is tracked by |status|.
UErrorCode status = U_ZERO_ERROR;
// Create our distinct |measures| to be formatted.
std::vector<icu::Measure> measures;
// We only show |hours| if necessary.
if (hours)
measures.push_back(icu::Measure(hours, createHour(status), status));
// We only show |minutes| if necessary or if using numeric format |width|.
if (minutes || width == UMEASFMT_WIDTH_NUMERIC)
measures.push_back(icu::Measure(minutes, createMinute(status), status));
// We only show |seconds| if necessary or if using numeric format |width|.
if (seconds || width == UMEASFMT_WIDTH_NUMERIC)
measures.push_back(icu::Measure(seconds, createSecond(status), status));
// Format our |measures| into a |unicode_message|.
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)) {
// If formatting was successful, convert our |unicode_message| into UTF-8.
unicode_message.toUTF8String(formatted_time);
} else {
// If something went wrong formatting w/ ICU, fall back to I18N messages.
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 necessary, negate the amount of time remaining.
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;
}
// Returns a string representation of the original duration for a given |timer|.
std::string ToOriginalDurationString(const AssistantTimer& timer) {
return ToFormattedTimeString(timer.original_duration, UMEASFMT_WIDTH_NARROW);
}
// Returns a string representation of the remaining time for the given |timer|.
std::string ToRemainingTimeString(const AssistantTimer& timer) {
return ToFormattedTimeString(timer.remaining_time, UMEASFMT_WIDTH_NUMERIC);
}
// Creates a notification ID for the given |timer|. It is guaranteed that this
// method will always return the same notification ID given the same timer.
std::string CreateTimerNotificationId(const AssistantTimer& timer) {
return std::string(kTimerNotificationIdPrefix) + timer.id;
}
// Creates a notification title for the given |timer|.
std::string CreateTimerNotificationTitle(const AssistantTimer& timer) {
if (IsTimersV2Enabled())
return ToRemainingTimeString(timer);
return l10n_util::GetStringUTF8(IDS_ASSISTANT_TIMER_NOTIFICATION_TITLE);
}
// Creates a notification message for the given |timer|.
std::string CreateTimerNotificationMessage(const AssistantTimer& timer) {
if (IsTimersV2Enabled()) {
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));
}
return ToRemainingTimeString(timer);
}
// Creates notification action URL for the given |timer|.
GURL CreateTimerNotificationActionUrl(const AssistantTimer& timer) {
// In timers v2, clicking the notification does nothing.
if (IsTimersV2Enabled())
return GURL();
// In timers v1, clicking the notification removes the |timer|.
return assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
.value();
}
// Creates notification buttons for the given |timer|.
std::vector<AssistantNotificationButton> CreateTimerNotificationButtons(
const AssistantTimer& timer) {
std::vector<AssistantNotificationButton> buttons;
if (!IsTimersV2Enabled()) {
// "STOP" button.
buttons.push_back(
{l10n_util::GetStringUTF8(IDS_ASSISTANT_TIMER_NOTIFICATION_STOP_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
.value(),
/*remove_notification_on_click=*/true});
// "ADD 1 MIN" button.
buttons.push_back({l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_ADD_1_MIN_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kAddTimeToTimer, timer.id,
base::TimeDelta::FromMinutes(1))
.value(),
/*remove_notification_on_click=*/true});
return buttons;
}
DCHECK(IsTimersV2Enabled());
if (timer.state != AssistantTimerState::kFired) {
if (timer.state == AssistantTimerState::kPaused) {
// "RESUME" button.
buttons.push_back({l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_RESUME_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kResumeTimer, timer.id)
.value(),
/*remove_notification_on_click=*/false});
} else {
// "PAUSE" button.
buttons.push_back({l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_PAUSE_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kPauseTimer, timer.id)
.value(),
/*remove_notification_on_click=*/false});
}
}
if (timer.state == AssistantTimerState::kFired) {
// "STOP" button.
buttons.push_back(
{l10n_util::GetStringUTF8(IDS_ASSISTANT_TIMER_NOTIFICATION_STOP_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
.value(),
/*remove_notification_on_click=*/true});
// "ADD 1 MIN" button.
buttons.push_back({l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_ADD_1_MIN_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kAddTimeToTimer, timer.id,
base::TimeDelta::FromMinutes(1))
.value(),
/*remove_notification_on_click=*/false});
} else {
// "CANCEL" button.
buttons.push_back({l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_CANCEL_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
.value(),
/*remove_notification_on_click=*/true});
}
return buttons;
}
// Creates a timer notification priority for the given |timer|.
AssistantNotificationPriority CreateTimerNotificationPriority(
const AssistantTimer& timer) {
// In timers v1, all notifications are |kHigh| priority.
if (!IsTimersV2Enabled())
return AssistantNotificationPriority::kHigh;
// In timers v2, a notification for a |kFired| timer is |kHigh| priority.
// This will cause the notification to pop up to the user.
if (timer.state == AssistantTimerState::kFired)
return AssistantNotificationPriority::kHigh;
// If the notification has lived for at least |kPopupThreshold|, drop the
// priority to |kLow| so that the notification will not pop up to the user.
constexpr base::TimeDelta kPopupThreshold = base::TimeDelta::FromSeconds(6);
const base::TimeDelta lifetime =
base::Time::Now() - timer.creation_time.value_or(base::Time::Now());
if (lifetime >= kPopupThreshold)
return AssistantNotificationPriority::kLow;
// Otherwise, the notification is |kDefault| priority. This means that it
// may or may not pop up to the user, depending on the presence of other
// notifications.
return AssistantNotificationPriority::kDefault;
}
// Creates a notification for the given |timer|.
AssistantNotification CreateTimerNotification(
const AssistantTimer& timer,
const AssistantNotification* existing_notification = nullptr) {
AssistantNotification notification;
notification.title = CreateTimerNotificationTitle(timer);
notification.message = CreateTimerNotificationMessage(timer);
notification.action_url = CreateTimerNotificationActionUrl(timer);
notification.buttons = CreateTimerNotificationButtons(timer);
notification.client_id = CreateTimerNotificationId(timer);
notification.grouping_key = kTimerNotificationGroupingKey;
notification.priority = CreateTimerNotificationPriority(timer);
notification.remove_on_click = !IsTimersV2Enabled();
notification.is_pinned = IsTimersV2Enabled();
// If we are creating a notification to replace an |existing_notification| and
// our new notification has higher priority, we want the system to "renotify"
// the user of the notification change. This will cause the new notification
// to popup to the user even if it was previously marked as read.
if (existing_notification &&
notification.priority > existing_notification->priority) {
notification.renotify = true;
}
return notification;
}
// Returns whether an |update| from LibAssistant to the specified |original|
// timer is allowed. Updates are always allowed in v1, only conditionally in v2.
bool ShouldAllowUpdateFromLibAssistant(const AssistantTimer& original,
const AssistantTimer& update) {
// If |id| is not equal, then |update| does refer to the |original| timer.
DCHECK_EQ(original.id, update.id);
// In v1, LibAssistant updates are always allowed since we only ever manage a
// single timer at a time and only as it transitions from firing to removal.
if (!IsTimersV2Enabled())
return true;
// In v2, updates are only allowed from LibAssistant if they are significant.
// We may receive an update due to a state change in another timer, and we'd
// want to discard the update to this timer to avoid introducing UI jank by
// updating its notification outside of its regular tick interval. In v2, we
// also update timer state from |kScheduled| to |kFired| ourselves to work
// around latency in receiving the event from LibAssistant. When we do so, we
// expect to later receive the state change from LibAssistant but discard it.
return !original.IsEqualInLibAssistantTo(update);
}
} // namespace
// AssistantAlarmTimerControllerImpl ------------------------------------------
AssistantAlarmTimerControllerImpl::AssistantAlarmTimerControllerImpl(
AssistantControllerImpl* assistant_controller)
: assistant_controller_(assistant_controller) {
model_.AddObserver(this);
assistant_controller_observer_.Add(AssistantController::Get());
}
AssistantAlarmTimerControllerImpl::~AssistantAlarmTimerControllerImpl() {
model_.RemoveObserver(this);
}
void AssistantAlarmTimerControllerImpl::SetAssistant(
chromeos::assistant::Assistant* assistant) {
assistant_ = assistant;
}
const AssistantAlarmTimerModel* AssistantAlarmTimerControllerImpl::GetModel()
const {
return &model_;
}
void AssistantAlarmTimerControllerImpl::OnTimerStateChanged(
std::vector<AssistantTimerPtr> new_or_updated_timers) {
// First we remove all old timers that no longer exist.
for (const auto* old_timer : model_.GetAllTimers()) {
if (std::none_of(new_or_updated_timers.begin(), new_or_updated_timers.end(),
[&old_timer](const auto& new_or_updated_timer) {
return old_timer->id == new_or_updated_timer->id;
})) {
model_.RemoveTimer(old_timer->id);
}
}
// Then we add any new timers and update existing ones (if allowed).
for (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 base::Optional<AlarmTimerAction>& action =
assistant::util::GetDeepLinkParamAsAlarmTimerAction(params);
if (!action.has_value())
return;
const base::Optional<std::string>& alarm_timer_id =
assistant::util::GetDeepLinkParam(params, DeepLinkParam::kId);
if (!alarm_timer_id.has_value())
return;
// Duration is optional. Only used for adding time to timer.
const base::Optional<base::TimeDelta>& duration =
assistant::util::GetDeepLinkParamAsTimeDelta(params,
DeepLinkParam::kDurationMs);
PerformAlarmTimerAction(action.value(), alarm_timer_id.value(), duration);
}
void AssistantAlarmTimerControllerImpl::OnAssistantStatusChanged(
chromeos::assistant::AssistantStatus status) {
// If LibAssistant is no longer running we need to clear our cache to
// accurately reflect LibAssistant alarm/timer state.
if (status == chromeos::assistant::AssistantStatus::NOT_READY)
model_.RemoveAllTimers();
}
void AssistantAlarmTimerControllerImpl::OnTimerAdded(
const AssistantTimer& timer) {
// Schedule the next tick of |timer|.
ScheduleNextTick(timer);
// Create a notification for the added alarm/timer.
assistant_controller_->notification_controller()->AddOrUpdateNotification(
CreateTimerNotification(timer));
}
void AssistantAlarmTimerControllerImpl::OnTimerUpdated(
const AssistantTimer& timer) {
// Schedule the next tick of |timer|.
ScheduleNextTick(timer);
auto* notification_controller =
assistant_controller_->notification_controller();
const auto* existing_notification =
notification_controller->model()->GetNotificationById(
CreateTimerNotificationId(timer));
// When a |timer| is updated we need to update the corresponding notification
// unless it has already been dismissed by the user.
if (existing_notification) {
notification_controller->AddOrUpdateNotification(
CreateTimerNotification(timer, existing_notification));
}
}
void AssistantAlarmTimerControllerImpl::OnTimerRemoved(
const AssistantTimer& timer) {
// Clean up the ticker for |timer|, if one exists.
tickers_.erase(timer.id);
// Remove any notification associated w/ |timer|.
assistant_controller_->notification_controller()->RemoveNotificationById(
CreateTimerNotificationId(timer), /*from_server=*/false);
}
void AssistantAlarmTimerControllerImpl::PerformAlarmTimerAction(
const AlarmTimerAction& action,
const std::string& alarm_timer_id,
const base::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;
// The next tick of |timer| should occur at its next full second of remaining
// time. Here we are calculating the number of milliseconds to that next full
// second.
int millis_to_next_full_sec = timer.remaining_time.InMilliseconds() % 1000;
// If |timer| has already fired, |millis_to_next_full_sec| will be negative.
// In this case, we take the inverse of the value to get the correct number of
// milliseconds to the next full second of remaining time.
if (millis_to_next_full_sec < 0)
millis_to_next_full_sec = 1000 + millis_to_next_full_sec;
// If we are exactly at the boundary of a full second, we want to make sure
// we wait until the next second to perform the next tick. Otherwise we'll end
// up w/ a superfluous tick that is unnecessary.
if (millis_to_next_full_sec == 0)
millis_to_next_full_sec = 1000;
// NOTE: We pass a copy of |timer.id| here as |timer| may no longer exist
// when Tick() is called due to the possibility of the |model_| being updated
// via a call to OnTimerStateChanged(), such as might happen if a timer is
// created, paused, resumed, or removed by LibAssistant.
ticker.Start(FROM_HERE,
base::TimeDelta::FromMilliseconds(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);
// We don't tick paused timers. Once the |timer| resumes, ticking will resume.
if (timer->state == AssistantTimerState::kPaused)
return;
// Update |timer| to reflect the new amount of |remaining_time|.
AssistantTimerPtr updated_timer = std::make_unique<AssistantTimer>(*timer);
updated_timer->remaining_time = updated_timer->fire_time - base::Time::Now();
// If there is no remaining time left on the timer, we ensure that our timer
// is marked as |kFired|. Since LibAssistant may be a bit slow to notify us of
// the change in state, we set the value ourselves to eliminate UI jank.
// NOTE: We use the rounded value of |remaining_time| since that's what we are
// displaying to the user and otherwise would be out of sync for ticks
// occurring at full second boundary values.
if (std::round(updated_timer->remaining_time.InSecondsF()) <= 0.f)
updated_timer->state = AssistantTimerState::kFired;
model_.AddOrUpdateTimer(std::move(updated_timer));
}
} // namespace ash