1 // Copyright 2018 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ash/assistant/assistant_alarm_timer_controller_impl.h"
6
7 #include <cmath>
8 #include <utility>
9
10 #include "ash/assistant/assistant_controller_impl.h"
11 #include "ash/assistant/assistant_notification_controller_impl.h"
12 #include "ash/assistant/util/deep_link_util.h"
13 #include "ash/strings/grit/ash_strings.h"
14 #include "base/i18n/message_formatter.h"
15 #include "base/stl_util.h"
16 #include "base/strings/stringprintf.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "base/time/time.h"
19 #include "chromeos/services/assistant/public/cpp/assistant_notification.h"
20 #include "chromeos/services/assistant/public/cpp/assistant_service.h"
21 #include "chromeos/services/assistant/public/cpp/features.h"
22 #include "mojo/public/cpp/bindings/pending_receiver.h"
23 #include "mojo/public/cpp/bindings/receiver.h"
24 #include "third_party/icu/source/common/unicode/utypes.h"
25 #include "third_party/icu/source/i18n/unicode/measfmt.h"
26 #include "third_party/icu/source/i18n/unicode/measunit.h"
27 #include "third_party/icu/source/i18n/unicode/measure.h"
28 #include "ui/base/l10n/l10n_util.h"
29
30 namespace ash {
31
32 namespace {
33
34 using assistant::util::AlarmTimerAction;
35 using chromeos::assistant::AssistantNotification;
36 using chromeos::assistant::AssistantNotificationButton;
37 using chromeos::assistant::AssistantNotificationPriority;
38 using chromeos::assistant::features::IsTimersV2Enabled;
39
40 // Grouping key and ID prefix for timer notifications.
41 constexpr char kTimerNotificationGroupingKey[] = "assistant/timer";
42 constexpr char kTimerNotificationIdPrefix[] = "assistant/timer";
43
44 // Helpers ---------------------------------------------------------------------
45
ToFormattedTimeString(base::TimeDelta time,UMeasureFormatWidth width)46 std::string ToFormattedTimeString(base::TimeDelta time,
47 UMeasureFormatWidth width) {
48 DCHECK(width == UMEASFMT_WIDTH_NARROW || width == UMEASFMT_WIDTH_NUMERIC);
49
50 // Method aliases to prevent line-wrapping below.
51 const auto createHour = icu::MeasureUnit::createHour;
52 const auto createMinute = icu::MeasureUnit::createMinute;
53 const auto createSecond = icu::MeasureUnit::createSecond;
54
55 // We round |total_seconds| to the nearest full second since we don't display
56 // our time string w/ millisecond granularity and because this method is
57 // called very near to full second boundaries. Otherwise, values like 4.99 sec
58 // would be displayed to the user as "0:04" instead of the expected "0:05".
59 const int64_t total_seconds = std::abs(std::round(time.InSecondsF()));
60
61 // Calculate time in hours/minutes/seconds.
62 const int32_t hours = total_seconds / 3600;
63 const int32_t minutes = (total_seconds - hours * 3600) / 60;
64 const int32_t seconds = total_seconds % 60;
65
66 // Success of the ICU APIs is tracked by |status|.
67 UErrorCode status = U_ZERO_ERROR;
68
69 // Create our distinct |measures| to be formatted.
70 std::vector<icu::Measure> measures;
71
72 // We only show |hours| if necessary.
73 if (hours)
74 measures.push_back(icu::Measure(hours, createHour(status), status));
75
76 // We only show |minutes| if necessary or if using numeric format |width|.
77 if (minutes || width == UMEASFMT_WIDTH_NUMERIC)
78 measures.push_back(icu::Measure(minutes, createMinute(status), status));
79
80 // We only show |seconds| if necessary or if using numeric format |width|.
81 if (seconds || width == UMEASFMT_WIDTH_NUMERIC)
82 measures.push_back(icu::Measure(seconds, createSecond(status), status));
83
84 // Format our |measures| into a |unicode_message|.
85 icu::UnicodeString unicode_message;
86 icu::FieldPosition field_position = icu::FieldPosition::DONT_CARE;
87 icu::MeasureFormat measure_format(icu::Locale::getDefault(), width, status);
88 measure_format.formatMeasures(measures.data(), measures.size(),
89 unicode_message, field_position, status);
90
91 std::string formatted_time;
92 if (U_SUCCESS(status)) {
93 // If formatting was successful, convert our |unicode_message| into UTF-8.
94 unicode_message.toUTF8String(formatted_time);
95 } else {
96 // If something went wrong formatting w/ ICU, fall back to I18N messages.
97 LOG(ERROR) << "Error formatting time string: " << status;
98 formatted_time =
99 base::UTF16ToUTF8(base::i18n::MessageFormatter::FormatWithNumberedArgs(
100 l10n_util::GetStringUTF16(
101 width == UMEASFMT_WIDTH_NARROW
102 ? IDS_ASSISTANT_TIMER_NOTIFICATION_FORMATTED_TIME_NARROW_FALLBACK
103 : IDS_ASSISTANT_TIMER_NOTIFICATION_FORMATTED_TIME_NUMERIC_FALLBACK),
104 hours, minutes, seconds));
105 }
106
107 // If necessary, negate the amount of time remaining.
108 if (time.InSeconds() < 0) {
109 formatted_time =
110 base::UTF16ToUTF8(base::i18n::MessageFormatter::FormatWithNumberedArgs(
111 l10n_util::GetStringUTF16(
112 IDS_ASSISTANT_TIMER_NOTIFICATION_FORMATTED_TIME_NEGATE),
113 formatted_time));
114 }
115
116 return formatted_time;
117 }
118
119 // Returns a string representation of the original duration for a given |timer|.
ToOriginalDurationString(const AssistantTimer & timer)120 std::string ToOriginalDurationString(const AssistantTimer& timer) {
121 return ToFormattedTimeString(timer.original_duration, UMEASFMT_WIDTH_NARROW);
122 }
123
124 // Returns a string representation of the remaining time for the given |timer|.
ToRemainingTimeString(const AssistantTimer & timer)125 std::string ToRemainingTimeString(const AssistantTimer& timer) {
126 return ToFormattedTimeString(timer.remaining_time, UMEASFMT_WIDTH_NUMERIC);
127 }
128
129 // Creates a notification ID for the given |timer|. It is guaranteed that this
130 // method will always return the same notification ID given the same timer.
CreateTimerNotificationId(const AssistantTimer & timer)131 std::string CreateTimerNotificationId(const AssistantTimer& timer) {
132 return std::string(kTimerNotificationIdPrefix) + timer.id;
133 }
134
135 // Creates a notification title for the given |timer|.
CreateTimerNotificationTitle(const AssistantTimer & timer)136 std::string CreateTimerNotificationTitle(const AssistantTimer& timer) {
137 if (IsTimersV2Enabled())
138 return ToRemainingTimeString(timer);
139 return l10n_util::GetStringUTF8(IDS_ASSISTANT_TIMER_NOTIFICATION_TITLE);
140 }
141
142 // Creates a notification message for the given |timer|.
CreateTimerNotificationMessage(const AssistantTimer & timer)143 std::string CreateTimerNotificationMessage(const AssistantTimer& timer) {
144 if (IsTimersV2Enabled()) {
145 if (timer.label.empty()) {
146 return base::UTF16ToUTF8(
147 base::i18n::MessageFormatter::FormatWithNumberedArgs(
148 l10n_util::GetStringUTF16(
149 timer.state == AssistantTimerState::kFired
150 ? IDS_ASSISTANT_TIMER_NOTIFICATION_MESSAGE_WHEN_FIRED
151 : IDS_ASSISTANT_TIMER_NOTIFICATION_MESSAGE),
152 ToOriginalDurationString(timer)));
153 }
154 return base::UTF16ToUTF8(base::i18n::MessageFormatter::FormatWithNumberedArgs(
155 l10n_util::GetStringUTF16(
156 timer.state == AssistantTimerState::kFired
157 ? IDS_ASSISTANT_TIMER_NOTIFICATION_MESSAGE_WHEN_FIRED_WITH_LABEL
158 : IDS_ASSISTANT_TIMER_NOTIFICATION_MESSAGE_WITH_LABEL),
159 ToOriginalDurationString(timer), timer.label));
160 }
161 return ToRemainingTimeString(timer);
162 }
163
164 // Creates notification action URL for the given |timer|.
CreateTimerNotificationActionUrl(const AssistantTimer & timer)165 GURL CreateTimerNotificationActionUrl(const AssistantTimer& timer) {
166 // In timers v2, clicking the notification does nothing.
167 if (IsTimersV2Enabled())
168 return GURL();
169 // In timers v1, clicking the notification removes the |timer|.
170 return assistant::util::CreateAlarmTimerDeepLink(
171 AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
172 .value();
173 }
174
175 // Creates notification buttons for the given |timer|.
CreateTimerNotificationButtons(const AssistantTimer & timer)176 std::vector<AssistantNotificationButton> CreateTimerNotificationButtons(
177 const AssistantTimer& timer) {
178 std::vector<AssistantNotificationButton> buttons;
179
180 if (!IsTimersV2Enabled()) {
181 // "STOP" button.
182 buttons.push_back(
183 {l10n_util::GetStringUTF8(IDS_ASSISTANT_TIMER_NOTIFICATION_STOP_BUTTON),
184 assistant::util::CreateAlarmTimerDeepLink(
185 AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
186 .value(),
187 /*remove_notification_on_click=*/true});
188
189 // "ADD 1 MIN" button.
190 buttons.push_back({l10n_util::GetStringUTF8(
191 IDS_ASSISTANT_TIMER_NOTIFICATION_ADD_1_MIN_BUTTON),
192 assistant::util::CreateAlarmTimerDeepLink(
193 AlarmTimerAction::kAddTimeToTimer, timer.id,
194 base::TimeDelta::FromMinutes(1))
195 .value(),
196 /*remove_notification_on_click=*/true});
197
198 return buttons;
199 }
200
201 DCHECK(IsTimersV2Enabled());
202
203 if (timer.state != AssistantTimerState::kFired) {
204 if (timer.state == AssistantTimerState::kPaused) {
205 // "RESUME" button.
206 buttons.push_back({l10n_util::GetStringUTF8(
207 IDS_ASSISTANT_TIMER_NOTIFICATION_RESUME_BUTTON),
208 assistant::util::CreateAlarmTimerDeepLink(
209 AlarmTimerAction::kResumeTimer, timer.id)
210 .value(),
211 /*remove_notification_on_click=*/false});
212 } else {
213 // "PAUSE" button.
214 buttons.push_back({l10n_util::GetStringUTF8(
215 IDS_ASSISTANT_TIMER_NOTIFICATION_PAUSE_BUTTON),
216 assistant::util::CreateAlarmTimerDeepLink(
217 AlarmTimerAction::kPauseTimer, timer.id)
218 .value(),
219 /*remove_notification_on_click=*/false});
220 }
221 }
222
223 if (timer.state == AssistantTimerState::kFired) {
224 // "STOP" button.
225 buttons.push_back(
226 {l10n_util::GetStringUTF8(IDS_ASSISTANT_TIMER_NOTIFICATION_STOP_BUTTON),
227 assistant::util::CreateAlarmTimerDeepLink(
228 AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
229 .value(),
230 /*remove_notification_on_click=*/true});
231
232 // "ADD 1 MIN" button.
233 buttons.push_back({l10n_util::GetStringUTF8(
234 IDS_ASSISTANT_TIMER_NOTIFICATION_ADD_1_MIN_BUTTON),
235 assistant::util::CreateAlarmTimerDeepLink(
236 AlarmTimerAction::kAddTimeToTimer, timer.id,
237 base::TimeDelta::FromMinutes(1))
238 .value(),
239 /*remove_notification_on_click=*/false});
240 } else {
241 // "CANCEL" button.
242 buttons.push_back({l10n_util::GetStringUTF8(
243 IDS_ASSISTANT_TIMER_NOTIFICATION_CANCEL_BUTTON),
244 assistant::util::CreateAlarmTimerDeepLink(
245 AlarmTimerAction::kRemoveAlarmOrTimer, timer.id)
246 .value(),
247 /*remove_notification_on_click=*/true});
248 }
249
250 return buttons;
251 }
252
253 // Creates a timer notification priority for the given |timer|.
CreateTimerNotificationPriority(const AssistantTimer & timer)254 AssistantNotificationPriority CreateTimerNotificationPriority(
255 const AssistantTimer& timer) {
256 // In timers v1, all notifications are |kHigh| priority.
257 if (!IsTimersV2Enabled())
258 return AssistantNotificationPriority::kHigh;
259
260 // In timers v2, a notification for a |kFired| timer is |kHigh| priority.
261 // This will cause the notification to pop up to the user.
262 if (timer.state == AssistantTimerState::kFired)
263 return AssistantNotificationPriority::kHigh;
264
265 // If the notification has lived for at least |kPopupThreshold|, drop the
266 // priority to |kLow| so that the notification will not pop up to the user.
267 constexpr base::TimeDelta kPopupThreshold = base::TimeDelta::FromSeconds(6);
268 const base::TimeDelta lifetime =
269 base::Time::Now() - timer.creation_time.value_or(base::Time::Now());
270 if (lifetime >= kPopupThreshold)
271 return AssistantNotificationPriority::kLow;
272
273 // Otherwise, the notification is |kDefault| priority. This means that it
274 // may or may not pop up to the user, depending on the presence of other
275 // notifications.
276 return AssistantNotificationPriority::kDefault;
277 }
278
279 // Creates a notification for the given |timer|.
CreateTimerNotification(const AssistantTimer & timer,const AssistantNotification * existing_notification=nullptr)280 AssistantNotification CreateTimerNotification(
281 const AssistantTimer& timer,
282 const AssistantNotification* existing_notification = nullptr) {
283 AssistantNotification notification;
284 notification.title = CreateTimerNotificationTitle(timer);
285 notification.message = CreateTimerNotificationMessage(timer);
286 notification.action_url = CreateTimerNotificationActionUrl(timer);
287 notification.buttons = CreateTimerNotificationButtons(timer);
288 notification.client_id = CreateTimerNotificationId(timer);
289 notification.grouping_key = kTimerNotificationGroupingKey;
290 notification.priority = CreateTimerNotificationPriority(timer);
291 notification.remove_on_click = !IsTimersV2Enabled();
292 notification.is_pinned = IsTimersV2Enabled();
293
294 // If we are creating a notification to replace an |existing_notification| and
295 // our new notification has higher priority, we want the system to "renotify"
296 // the user of the notification change. This will cause the new notification
297 // to popup to the user even if it was previously marked as read.
298 if (existing_notification &&
299 notification.priority > existing_notification->priority) {
300 notification.renotify = true;
301 }
302
303 return notification;
304 }
305
306 // Returns whether an |update| from LibAssistant to the specified |original|
307 // timer is allowed. Updates are always allowed in v1, only conditionally in v2.
ShouldAllowUpdateFromLibAssistant(const AssistantTimer & original,const AssistantTimer & update)308 bool ShouldAllowUpdateFromLibAssistant(const AssistantTimer& original,
309 const AssistantTimer& update) {
310 // If |id| is not equal, then |update| does refer to the |original| timer.
311 DCHECK_EQ(original.id, update.id);
312
313 // In v1, LibAssistant updates are always allowed since we only ever manage a
314 // single timer at a time and only as it transitions from firing to removal.
315 if (!IsTimersV2Enabled())
316 return true;
317
318 // In v2, updates are only allowed from LibAssistant if they are significant.
319 // We may receive an update due to a state change in another timer, and we'd
320 // want to discard the update to this timer to avoid introducing UI jank by
321 // updating its notification outside of its regular tick interval. In v2, we
322 // also update timer state from |kScheduled| to |kFired| ourselves to work
323 // around latency in receiving the event from LibAssistant. When we do so, we
324 // expect to later receive the state change from LibAssistant but discard it.
325 return !original.IsEqualInLibAssistantTo(update);
326 }
327
328 } // namespace
329
330 // AssistantAlarmTimerControllerImpl ------------------------------------------
331
AssistantAlarmTimerControllerImpl(AssistantControllerImpl * assistant_controller)332 AssistantAlarmTimerControllerImpl::AssistantAlarmTimerControllerImpl(
333 AssistantControllerImpl* assistant_controller)
334 : assistant_controller_(assistant_controller) {
335 model_.AddObserver(this);
336 assistant_controller_observer_.Add(AssistantController::Get());
337 }
338
~AssistantAlarmTimerControllerImpl()339 AssistantAlarmTimerControllerImpl::~AssistantAlarmTimerControllerImpl() {
340 model_.RemoveObserver(this);
341 }
342
SetAssistant(chromeos::assistant::Assistant * assistant)343 void AssistantAlarmTimerControllerImpl::SetAssistant(
344 chromeos::assistant::Assistant* assistant) {
345 assistant_ = assistant;
346 }
347
GetModel() const348 const AssistantAlarmTimerModel* AssistantAlarmTimerControllerImpl::GetModel()
349 const {
350 return &model_;
351 }
352
OnTimerStateChanged(std::vector<AssistantTimerPtr> new_or_updated_timers)353 void AssistantAlarmTimerControllerImpl::OnTimerStateChanged(
354 std::vector<AssistantTimerPtr> new_or_updated_timers) {
355 // First we remove all old timers that no longer exist.
356 for (const auto* old_timer : model_.GetAllTimers()) {
357 if (std::none_of(new_or_updated_timers.begin(), new_or_updated_timers.end(),
358 [&old_timer](const auto& new_or_updated_timer) {
359 return old_timer->id == new_or_updated_timer->id;
360 })) {
361 model_.RemoveTimer(old_timer->id);
362 }
363 }
364
365 // Then we add any new timers and update existing ones (if allowed).
366 for (auto& new_or_updated_timer : new_or_updated_timers) {
367 const auto* original_timer = model_.GetTimerById(new_or_updated_timer->id);
368 const bool is_new_timer = original_timer == nullptr;
369 if (is_new_timer || ShouldAllowUpdateFromLibAssistant(
370 *original_timer, *new_or_updated_timer)) {
371 model_.AddOrUpdateTimer(std::move(new_or_updated_timer));
372 }
373 }
374 }
375
OnAssistantControllerConstructed()376 void AssistantAlarmTimerControllerImpl::OnAssistantControllerConstructed() {
377 AssistantState::Get()->AddObserver(this);
378 }
379
OnAssistantControllerDestroying()380 void AssistantAlarmTimerControllerImpl::OnAssistantControllerDestroying() {
381 AssistantState::Get()->RemoveObserver(this);
382 }
383
OnDeepLinkReceived(assistant::util::DeepLinkType type,const std::map<std::string,std::string> & params)384 void AssistantAlarmTimerControllerImpl::OnDeepLinkReceived(
385 assistant::util::DeepLinkType type,
386 const std::map<std::string, std::string>& params) {
387 using assistant::util::DeepLinkParam;
388 using assistant::util::DeepLinkType;
389
390 if (type != DeepLinkType::kAlarmTimer)
391 return;
392
393 const base::Optional<AlarmTimerAction>& action =
394 assistant::util::GetDeepLinkParamAsAlarmTimerAction(params);
395 if (!action.has_value())
396 return;
397
398 const base::Optional<std::string>& alarm_timer_id =
399 assistant::util::GetDeepLinkParam(params, DeepLinkParam::kId);
400 if (!alarm_timer_id.has_value())
401 return;
402
403 // Duration is optional. Only used for adding time to timer.
404 const base::Optional<base::TimeDelta>& duration =
405 assistant::util::GetDeepLinkParamAsTimeDelta(params,
406 DeepLinkParam::kDurationMs);
407
408 PerformAlarmTimerAction(action.value(), alarm_timer_id.value(), duration);
409 }
410
OnAssistantStatusChanged(chromeos::assistant::AssistantStatus status)411 void AssistantAlarmTimerControllerImpl::OnAssistantStatusChanged(
412 chromeos::assistant::AssistantStatus status) {
413 // If LibAssistant is no longer running we need to clear our cache to
414 // accurately reflect LibAssistant alarm/timer state.
415 if (status == chromeos::assistant::AssistantStatus::NOT_READY)
416 model_.RemoveAllTimers();
417 }
418
OnTimerAdded(const AssistantTimer & timer)419 void AssistantAlarmTimerControllerImpl::OnTimerAdded(
420 const AssistantTimer& timer) {
421 // Schedule the next tick of |timer|.
422 ScheduleNextTick(timer);
423
424 // Create a notification for the added alarm/timer.
425 assistant_controller_->notification_controller()->AddOrUpdateNotification(
426 CreateTimerNotification(timer));
427 }
428
OnTimerUpdated(const AssistantTimer & timer)429 void AssistantAlarmTimerControllerImpl::OnTimerUpdated(
430 const AssistantTimer& timer) {
431 // Schedule the next tick of |timer|.
432 ScheduleNextTick(timer);
433
434 auto* notification_controller =
435 assistant_controller_->notification_controller();
436 const auto* existing_notification =
437 notification_controller->model()->GetNotificationById(
438 CreateTimerNotificationId(timer));
439
440 // When a |timer| is updated we need to update the corresponding notification
441 // unless it has already been dismissed by the user.
442 if (existing_notification) {
443 notification_controller->AddOrUpdateNotification(
444 CreateTimerNotification(timer, existing_notification));
445 }
446 }
447
OnTimerRemoved(const AssistantTimer & timer)448 void AssistantAlarmTimerControllerImpl::OnTimerRemoved(
449 const AssistantTimer& timer) {
450 // Clean up the ticker for |timer|, if one exists.
451 tickers_.erase(timer.id);
452
453 // Remove any notification associated w/ |timer|.
454 assistant_controller_->notification_controller()->RemoveNotificationById(
455 CreateTimerNotificationId(timer), /*from_server=*/false);
456 }
457
PerformAlarmTimerAction(const AlarmTimerAction & action,const std::string & alarm_timer_id,const base::Optional<base::TimeDelta> & duration)458 void AssistantAlarmTimerControllerImpl::PerformAlarmTimerAction(
459 const AlarmTimerAction& action,
460 const std::string& alarm_timer_id,
461 const base::Optional<base::TimeDelta>& duration) {
462 DCHECK(assistant_);
463
464 switch (action) {
465 case AlarmTimerAction::kAddTimeToTimer:
466 if (!duration.has_value()) {
467 LOG(ERROR) << "Ignoring add time to timer action duration.";
468 return;
469 }
470 assistant_->AddTimeToTimer(alarm_timer_id, duration.value());
471 break;
472 case AlarmTimerAction::kPauseTimer:
473 DCHECK(!duration.has_value());
474 assistant_->PauseTimer(alarm_timer_id);
475 break;
476 case AlarmTimerAction::kRemoveAlarmOrTimer:
477 DCHECK(!duration.has_value());
478 assistant_->RemoveAlarmOrTimer(alarm_timer_id);
479 break;
480 case AlarmTimerAction::kResumeTimer:
481 DCHECK(!duration.has_value());
482 assistant_->ResumeTimer(alarm_timer_id);
483 break;
484 }
485 }
486
ScheduleNextTick(const AssistantTimer & timer)487 void AssistantAlarmTimerControllerImpl::ScheduleNextTick(
488 const AssistantTimer& timer) {
489 auto& ticker = tickers_[timer.id];
490 if (ticker.IsRunning())
491 return;
492
493 // The next tick of |timer| should occur at its next full second of remaining
494 // time. Here we are calculating the number of milliseconds to that next full
495 // second.
496 int millis_to_next_full_sec = timer.remaining_time.InMilliseconds() % 1000;
497
498 // If |timer| has already fired, |millis_to_next_full_sec| will be negative.
499 // In this case, we take the inverse of the value to get the correct number of
500 // milliseconds to the next full second of remaining time.
501 if (millis_to_next_full_sec < 0)
502 millis_to_next_full_sec = 1000 + millis_to_next_full_sec;
503
504 // If we are exactly at the boundary of a full second, we want to make sure
505 // we wait until the next second to perform the next tick. Otherwise we'll end
506 // up w/ a superfluous tick that is unnecessary.
507 if (millis_to_next_full_sec == 0)
508 millis_to_next_full_sec = 1000;
509
510 // NOTE: We pass a copy of |timer.id| here as |timer| may no longer exist
511 // when Tick() is called due to the possibility of the |model_| being updated
512 // via a call to OnTimerStateChanged(), such as might happen if a timer is
513 // created, paused, resumed, or removed by LibAssistant.
514 ticker.Start(FROM_HERE,
515 base::TimeDelta::FromMilliseconds(millis_to_next_full_sec),
516 base::BindOnce(&AssistantAlarmTimerControllerImpl::Tick,
517 base::Unretained(this), timer.id));
518 }
519
Tick(const std::string & timer_id)520 void AssistantAlarmTimerControllerImpl::Tick(const std::string& timer_id) {
521 const auto* timer = model_.GetTimerById(timer_id);
522 DCHECK(timer);
523
524 // We don't tick paused timers. Once the |timer| resumes, ticking will resume.
525 if (timer->state == AssistantTimerState::kPaused)
526 return;
527
528 // Update |timer| to reflect the new amount of |remaining_time|.
529 AssistantTimerPtr updated_timer = std::make_unique<AssistantTimer>(*timer);
530 updated_timer->remaining_time = updated_timer->fire_time - base::Time::Now();
531
532 // If there is no remaining time left on the timer, we ensure that our timer
533 // is marked as |kFired|. Since LibAssistant may be a bit slow to notify us of
534 // the change in state, we set the value ourselves to eliminate UI jank.
535 // NOTE: We use the rounded value of |remaining_time| since that's what we are
536 // displaying to the user and otherwise would be out of sync for ticks
537 // occurring at full second boundary values.
538 if (std::round(updated_timer->remaining_time.InSecondsF()) <= 0.f)
539 updated_timer->state = AssistantTimerState::kFired;
540
541 model_.AddOrUpdateTimer(std::move(updated_timer));
542 }
543
544 } // namespace ash
545