|  | // 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/bind.h" | 
|  | #include "base/i18n/message_formatter.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 "chromeos/services/libassistant/public/cpp/assistant_notification.h" | 
|  | #include "chromeos/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::util::AlarmTimerAction; | 
|  | using chromeos::assistant::AssistantNotification; | 
|  | using chromeos::assistant::AssistantNotificationButton; | 
|  | using chromeos::assistant::AssistantNotificationPriority; | 
|  | using chromeos::assistant::AssistantTimer; | 
|  | using chromeos::assistant::AssistantTimerState; | 
|  |  | 
|  | // 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.emplace_back(hours, createHour(status), status); | 
|  |  | 
|  | // We only show |minutes| if necessary or if using numeric format |width|. | 
|  | if (minutes || width == UMEASFMT_WIDTH_NUMERIC) | 
|  | measures.emplace_back(minutes, createMinute(status), status); | 
|  |  | 
|  | // We only show |seconds| if necessary or if using numeric format |width|. | 
|  | if (seconds || width == UMEASFMT_WIDTH_NUMERIC) | 
|  | measures.emplace_back(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) { | 
|  | return ToRemainingTimeString(timer); | 
|  | } | 
|  |  | 
|  | // Creates a notification message for the given |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)); | 
|  | } | 
|  |  | 
|  | // Creates notification buttons for the given |timer|. | 
|  | std::vector<AssistantNotificationButton> CreateTimerNotificationButtons( | 
|  | const AssistantTimer& timer) { | 
|  | std::vector<AssistantNotificationButton> buttons; | 
|  |  | 
|  | 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::Minutes(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 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::Seconds(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.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 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 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_observation_.Observe(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( | 
|  | const std::vector<AssistantTimer>& 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 (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; | 
|  |  | 
|  | // Duration is optional. Only used for adding time to timer. | 
|  | const absl::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 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; | 
|  |  | 
|  | // 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::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); | 
|  |  | 
|  | // 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|. | 
|  | AssistantTimer updated_timer(*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 |