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