blob: c1bb4390d3dc7ca92e2bf30c2cd6c70f3b974f91 [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.h"
#include <map>
#include <string>
#include <utility>
#include "ash/assistant/assistant_controller.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/utf_string_conversions.h"
#include "chromeos/services/assistant/public/features.h"
#include "chromeos/services/assistant/public/mojom/assistant.mojom.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 {
// Grouping key and ID prefix for timer notifications.
constexpr char kTimerNotificationGroupingKey[] = "assistant/timer";
constexpr char kTimerNotificationIdPrefix[] = "assistant/timer";
constexpr base::TimeDelta kOneMin = base::TimeDelta::FromMinutes(1);
// Interval at which alarms/timers are ticked.
constexpr base::TimeDelta kTickInterval = base::TimeDelta::FromSeconds(1);
// Helpers ---------------------------------------------------------------------
// Creates a notification ID for the given |alarm_timer_id|. It is guaranteed
// that this method will always return the same notification ID given the same
// alarm/timer ID.
std::string CreateTimerNotificationId(const std::string& alarm_timer_id) {
return std::string(kTimerNotificationIdPrefix) + alarm_timer_id;
}
// Creates a notification message for the given |alarm_timer| which has the
// specified amount of |time_remaining|. Note that if the alarm/timer is expired
// the amount of time remaining will be negated.
std::string CreateTimerNotificationMessage(const AlarmTimer& alarm_timer,
base::TimeDelta time_remaining) {
// 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 hours/minutes/seconds remaining.
const int64_t total_seconds = time_remaining.InSeconds();
const int64_t hours = total_seconds / 3600;
const int64_t minutes = (total_seconds - hours * 3600) / 60;
const int64_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. We only show |hours| if
// necessary, otherwise they are omitted.
std::vector<icu::Measure> measures;
if (hours)
measures.push_back(icu::Measure(hours, createHour(status), status));
measures.push_back(icu::Measure(minutes, createMinute(status), status));
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;
UMeasureFormatWidth width = UMEASFMT_WIDTH_NUMERIC;
icu::MeasureFormat measure_format(icu::Locale::getDefault(), width, status);
measure_format.formatMeasures(measures.data(), measures.size(),
unicode_message, field_position, status);
std::string message;
if (U_SUCCESS(status)) {
// If formatting was successful, convert our |unicode_message| into UTF-8.
unicode_message.toUTF8String(message);
} else {
// If something went wrong, we'll fall back to using "hh:mm:ss" instead.
LOG(ERROR) << "Error formatting timer notification message: " << status;
message = base::StringPrintf("%02ld:%02ld:%02ld", hours, minutes, seconds);
}
// If time has elapsed since the |alarm_timer| has expired, we'll need to
// negate the amount of time remaining.
if (total_seconds && alarm_timer.expired()) {
const auto format = l10n_util::GetStringUTF16(
IDS_ASSISTANT_TIMER_NOTIFICATION_MESSAGE_EXPIRED);
return base::UTF16ToUTF8(
base::i18n::MessageFormatter::FormatWithNumberedArgs(format, message));
}
// Otherwise, all necessary formatting has been performed.
return message;
}
// TODO(llin): Migrate to use the AlarmManager API to better support multiple
// timers when the API is available.
chromeos::assistant::mojom::AssistantNotificationPtr CreateTimerNotification(
const AlarmTimer& alarm_timer,
base::TimeDelta time_remaining) {
using chromeos::assistant::mojom::AssistantNotification;
using chromeos::assistant::mojom::AssistantNotificationButton;
using chromeos::assistant::mojom::AssistantNotificationPtr;
using chromeos::assistant::mojom::AssistantNotificationType;
const std::string title =
l10n_util::GetStringUTF8(IDS_ASSISTANT_TIMER_NOTIFICATION_TITLE);
const std::string message =
CreateTimerNotificationMessage(alarm_timer, time_remaining);
base::Optional<GURL> stop_alarm_timer_action_url =
assistant::util::CreateAlarmTimerDeepLink(
assistant::util::AlarmTimerAction::kStopRinging,
/*alarm_timer_id=*/base::nullopt,
/*duration=*/base::nullopt);
base::Optional<GURL> add_time_to_timer_action_url =
assistant::util::CreateAlarmTimerDeepLink(
assistant::util::AlarmTimerAction::kAddTimeToTimer, alarm_timer.id,
kOneMin);
AssistantNotificationPtr notification = AssistantNotification::New();
// If in-Assistant notifications are supported, we'll allow alarm/timer
// notifications to show in either Assistant UI or the Message Center.
// Otherwise, we'll only allow the notification to show in the Message Center.
notification->type =
chromeos::assistant::features::IsInAssistantNotificationsEnabled()
? AssistantNotificationType::kPreferInAssistant
: AssistantNotificationType::kSystem;
notification->title = title;
notification->message = message;
notification->client_id = CreateTimerNotificationId(alarm_timer.id);
notification->grouping_key = kTimerNotificationGroupingKey;
// This notification should be able to wake up the display if it was off.
notification->is_high_priority = true;
if (!stop_alarm_timer_action_url.has_value()) {
LOG(ERROR) << "Can't create stop alarm timer action URL";
return notification;
}
notification->action_url = stop_alarm_timer_action_url.value();
// "STOP" button.
notification->buttons.push_back(AssistantNotificationButton::New(
l10n_util::GetStringUTF8(IDS_ASSISTANT_TIMER_NOTIFICATION_STOP_BUTTON),
stop_alarm_timer_action_url.value()));
if (!add_time_to_timer_action_url.has_value()) {
LOG(ERROR) << "Can't create add time to timer action URL";
return notification;
}
// "ADD 1 MIN" button.
notification->buttons.push_back(
chromeos::assistant::mojom::AssistantNotificationButton::New(
l10n_util::GetStringUTF8(
IDS_ASSISTANT_TIMER_NOTIFICATION_ADD_1_MIN_BUTTON),
add_time_to_timer_action_url.value()));
return notification;
}
} // namespace
// AssistantAlarmTimerController -----------------------------------------------
AssistantAlarmTimerController::AssistantAlarmTimerController(
AssistantController* assistant_controller)
: assistant_controller_(assistant_controller) {
AddModelObserver(this);
assistant_controller_->AddObserver(this);
}
AssistantAlarmTimerController::~AssistantAlarmTimerController() {
assistant_controller_->RemoveObserver(this);
RemoveModelObserver(this);
}
void AssistantAlarmTimerController::BindReceiver(
mojo::PendingReceiver<mojom::AssistantAlarmTimerController> receiver) {
receiver_.Bind(std::move(receiver));
}
void AssistantAlarmTimerController::AddModelObserver(
AssistantAlarmTimerModelObserver* observer) {
model_.AddObserver(observer);
}
void AssistantAlarmTimerController::RemoveModelObserver(
AssistantAlarmTimerModelObserver* observer) {
model_.RemoveObserver(observer);
}
void AssistantAlarmTimerController::OnAlarmTimerStateChanged(
mojom::AssistantAlarmTimerEventPtr event) {
if (!event) {
// Nothing is ringing. Remove all alarms and timers.
model_.RemoveAllAlarmsTimers();
return;
}
switch (event->type) {
case mojom::AssistantAlarmTimerEventType::kTimer:
if (event->data->get_timer_data()->state ==
mojom::AssistantTimerState::kFired) {
// Remove all timers/alarms since there will be only one timer/alarm
// firing.
// TODO(llin): Handle multiple timers firing when the API is supported.
model_.RemoveAllAlarmsTimers();
AlarmTimer timer;
timer.id = event->data->get_timer_data()->timer_id;
timer.type = AlarmTimerType::kTimer;
timer.end_time = base::TimeTicks::Now();
model_.AddAlarmTimer(timer);
}
break;
// TODO(llin): Handle alarm event.
}
}
void AssistantAlarmTimerController::OnAlarmTimerAdded(
const AlarmTimer& alarm_timer,
const base::TimeDelta& time_remaining) {
// Schedule a repeating timer to tick the tracked alarms/timers.
if (!timer_.IsRunning()) {
timer_.Start(FROM_HERE, kTickInterval, &model_,
&AssistantAlarmTimerModel::Tick);
}
// Create a notification for the added alarm/timer.
assistant_controller_->notification_controller()->AddOrUpdateNotification(
CreateTimerNotification(alarm_timer, time_remaining));
}
void AssistantAlarmTimerController::OnAlarmsTimersTicked(
const std::map<std::string, base::TimeDelta>& times_remaining) {
// Update any existing notifications associated w/ our alarms/timers.
for (auto& pair : times_remaining) {
auto* notification_controller =
assistant_controller_->notification_controller();
if (notification_controller->model()->HasNotificationForId(
CreateTimerNotificationId(/*alarm_timer_id=*/pair.first))) {
notification_controller->AddOrUpdateNotification(CreateTimerNotification(
*model_.GetAlarmTimerById(pair.first), pair.second));
}
}
}
void AssistantAlarmTimerController::OnAllAlarmsTimersRemoved() {
// We can stop our timer from ticking when all alarms/timers are removed.
timer_.Stop();
// Remove any notifications associated w/ alarms/timers.
assistant_controller_->notification_controller()
->RemoveNotificationByGroupingKey(kTimerNotificationGroupingKey,
/*from_server=*/false);
}
void AssistantAlarmTimerController::SetAssistant(
chromeos::assistant::mojom::Assistant* assistant) {
assistant_ = assistant;
}
void AssistantAlarmTimerController::OnAssistantControllerConstructed() {
assistant_controller_->ui_controller()->AddModelObserver(this);
}
void AssistantAlarmTimerController::OnAssistantControllerDestroying() {
assistant_controller_->ui_controller()->RemoveModelObserver(this);
}
void AssistantAlarmTimerController::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<assistant::util::AlarmTimerAction>& action =
assistant::util::GetDeepLinkParamAsAlarmTimerAction(params);
if (!action.has_value())
return;
// Timer ID is optional. Only used for adding time to timer.
const base::Optional<std::string>& alarm_timer_id =
assistant::util::GetDeepLinkParam(params, DeepLinkParam::kId);
// 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, duration);
}
void AssistantAlarmTimerController::OnUiVisibilityChanged(
AssistantVisibility new_visibility,
AssistantVisibility old_visibility,
base::Optional<AssistantEntryPoint> entry_point,
base::Optional<AssistantExitPoint> exit_point) {
// When the Assistant UI transitions from a visible state, we'll dismiss any
// ringing alarms or timers (assuming certain conditions have been met).
if (old_visibility != AssistantVisibility::kVisible)
return;
// We only do this if in-Assistant notifications are enabled, as in-Assistant
// alarm/timer notifications only live as long the Assistant UI. Per UX
// requirement, when Assistant UI dismisses with an in-Assistant timer
// notification showing, any ringing alarms/timers should be stopped.
if (!chromeos::assistant::features::IsInAssistantNotificationsEnabled())
return;
assistant_->StopAlarmTimerRinging();
}
void AssistantAlarmTimerController::PerformAlarmTimerAction(
const assistant::util::AlarmTimerAction& action,
const base::Optional<std::string>& alarm_timer_id,
const base::Optional<base::TimeDelta>& duration) {
DCHECK(assistant_);
switch (action) {
case assistant::util::AlarmTimerAction::kAddTimeToTimer:
if (!alarm_timer_id.has_value() || !duration.has_value()) {
LOG(ERROR) << "Ignore add time to timer action without timer ID or "
<< "duration.";
return;
}
// Verify the timer is ringing.
DCHECK(model_.GetAlarmTimerById(alarm_timer_id.value()));
// LibAssistant doesn't currently support adding time to an ringing timer.
// We'll create a new one with the duration specified. Note that we
// currently only support this deep link for an alarm/timer that is
// ringing.
assistant_->StopAlarmTimerRinging();
assistant_->CreateTimer(duration.value());
break;
case assistant::util::AlarmTimerAction::kStopRinging:
assistant_->StopAlarmTimerRinging();
break;
}
}
} // namespace ash