blob: 7ebb6f905fb5423c42f2992dd93fc2e62c2d32ae [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 <map>
#include <string>
#include <utility>
#include "ash/assistant/assistant_controller_impl.h"
#include "ash/assistant/assistant_notification_controller.h"
#include "ash/assistant/util/deep_link_util.h"
#include "ash/public/mojom/assistant_controller.mojom.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/i18n/message_formatter.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_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::features::IsTimersV2Enabled;
using chromeos::assistant::mojom::AssistantNotification;
using chromeos::assistant::mojom::AssistantNotificationButton;
using chromeos::assistant::mojom::AssistantNotificationButtonPtr;
using chromeos::assistant::mojom::AssistantNotificationPriority;
using chromeos::assistant::mojom::AssistantNotificationPtr;
// Grouping key and ID prefix for timer notifications.
constexpr char kTimerNotificationGroupingKey[] = "assistant/timer";
constexpr char kTimerNotificationIdPrefix[] = "assistant/timer";
// Interval at which alarms/timers are ticked.
constexpr base::TimeDelta kTickInterval = base::TimeDelta::FromSeconds(1);
// 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;
// Calculate time in hours/minutes/seconds.
const int64_t total_seconds = std::abs(time.InSeconds());
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<AssistantNotificationButtonPtr> CreateTimerNotificationButtons(
const AssistantTimer& timer) {
std::vector<AssistantNotificationButtonPtr> buttons;
if (!IsTimersV2Enabled()) {
// "STOP" button.
buttons.push_back(AssistantNotificationButton::New(
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(AssistantNotificationButton::New(
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(AssistantNotificationButton::New(
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(AssistantNotificationButton::New(
l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_PAUSE_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kPauseTimer, timer.id)
.value(),
/*remove_notification_on_click=*/false));
}
}
// "CANCEL" button.
buttons.push_back(AssistantNotificationButton::New(
l10n_util::GetStringUTF8(IDS_ASSISTANT_TIMER_NOTIFICATION_CANCEL_BUTTON),
assistant::util::CreateAlarmTimerDeepLink(
AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
.value(),
/*remove_notification_on_click=*/true));
if (timer.state == AssistantTimerState::kFired) {
// "ADD 1 MIN" button.
buttons.push_back(AssistantNotificationButton::New(
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));
}
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|.
AssistantNotificationPtr CreateTimerNotification(const AssistantTimer& timer) {
AssistantNotificationPtr notification = AssistantNotification::New();
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();
return notification;
}
} // 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.
for (auto& new_or_updated_timer : new_or_updated_timers)
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 a repeating timer to tick the tracked timers.
if (!ticker_.IsRunning()) {
ticker_.Start(FROM_HERE, kTickInterval, &model_,
&AssistantAlarmTimerModel::Tick);
}
// Create a notification for the added alarm/timer.
assistant_controller_->notification_controller()->AddOrUpdateNotification(
CreateTimerNotification(timer));
}
void AssistantAlarmTimerControllerImpl::OnTimerUpdated(
const AssistantTimer& timer) {
// When a |timer| is updated we need to update the corresponding notification
// unless it has already been dismissed by the user.
auto* notification_controller =
assistant_controller_->notification_controller();
if (notification_controller->model()->HasNotificationForId(
CreateTimerNotificationId(timer))) {
notification_controller->AddOrUpdateNotification(
CreateTimerNotification(timer));
}
}
void AssistantAlarmTimerControllerImpl::OnTimerRemoved(
const AssistantTimer& timer) {
// If our model is empty, we no longer need tick updates.
if (model_.empty())
ticker_.Stop();
// 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;
}
}
} // namespace ash