1// Copyright 2014 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/system/chromeos/session/tray_session_length_limit.h"
6
7#include <algorithm>
8
9#include "ash/shell.h"
10#include "ash/system/chromeos/label_tray_view.h"
11#include "ash/system/system_notifier.h"
12#include "ash/system/tray/system_tray.h"
13#include "ash/system/tray/system_tray_delegate.h"
14#include "ash/system/tray/system_tray_notifier.h"
15#include "base/logging.h"
16#include "base/strings/utf_string_conversions.h"
17#include "grit/ash_resources.h"
18#include "grit/ash_strings.h"
19#include "ui/base/l10n/l10n_util.h"
20#include "ui/base/l10n/time_format.h"
21#include "ui/base/resource/resource_bundle.h"
22#include "ui/message_center/message_center.h"
23#include "ui/message_center/notification.h"
24#include "ui/views/view.h"
25
26namespace ash {
27namespace {
28
29// If the remaining session time falls below this threshold, the user should be
30// informed that the session is about to expire.
31const int kExpiringSoonThresholdInMinutes = 5;
32
33// Use 500ms interval for updates to notification and tray bubble to reduce the
34// likelihood of a user-visible skip in high load situations (as might happen
35// with 1000ms).
36const int kTimerIntervalInMilliseconds = 500;
37
38}  // namespace
39
40// static
41const char TraySessionLengthLimit::kNotificationId[] =
42    "chrome://session/timeout";
43
44TraySessionLengthLimit::TraySessionLengthLimit(SystemTray* system_tray)
45    : SystemTrayItem(system_tray),
46      limit_state_(LIMIT_NONE),
47      last_limit_state_(LIMIT_NONE),
48      tray_bubble_view_(NULL) {
49  Shell::GetInstance()->system_tray_notifier()->
50      AddSessionLengthLimitObserver(this);
51  Update();
52}
53
54TraySessionLengthLimit::~TraySessionLengthLimit() {
55  Shell::GetInstance()->system_tray_notifier()->
56      RemoveSessionLengthLimitObserver(this);
57}
58
59// Add view to tray bubble.
60views::View* TraySessionLengthLimit::CreateDefaultView(
61    user::LoginStatus status) {
62  CHECK(!tray_bubble_view_);
63  UpdateState();
64  if (limit_state_ == LIMIT_NONE)
65    return NULL;
66  tray_bubble_view_ = new LabelTrayView(
67      NULL /* click_listener */,
68      IDR_AURA_UBER_TRAY_BUBBLE_SESSION_LENGTH_LIMIT);
69  tray_bubble_view_->SetMessage(ComposeTrayBubbleMessage());
70  return tray_bubble_view_;
71}
72
73// View has been removed from tray bubble.
74void TraySessionLengthLimit::DestroyDefaultView() {
75  tray_bubble_view_ = NULL;
76}
77
78void TraySessionLengthLimit::OnSessionStartTimeChanged() {
79  Update();
80}
81
82void TraySessionLengthLimit::OnSessionLengthLimitChanged() {
83  Update();
84}
85
86void TraySessionLengthLimit::Update() {
87  UpdateState();
88  UpdateNotification();
89  UpdateTrayBubbleView();
90}
91
92void TraySessionLengthLimit::UpdateState() {
93  SystemTrayDelegate* delegate = Shell::GetInstance()->system_tray_delegate();
94  if (delegate->GetSessionStartTime(&session_start_time_) &&
95      delegate->GetSessionLengthLimit(&time_limit_)) {
96    const base::TimeDelta expiring_soon_threshold(
97        base::TimeDelta::FromMinutes(kExpiringSoonThresholdInMinutes));
98    remaining_session_time_ = std::max(
99        time_limit_ - (base::TimeTicks::Now() - session_start_time_),
100        base::TimeDelta());
101    limit_state_ = remaining_session_time_ <= expiring_soon_threshold ?
102        LIMIT_EXPIRING_SOON : LIMIT_SET;
103    if (!timer_)
104      timer_.reset(new base::RepeatingTimer<TraySessionLengthLimit>);
105    if (!timer_->IsRunning()) {
106      timer_->Start(FROM_HERE,
107                    base::TimeDelta::FromMilliseconds(
108                        kTimerIntervalInMilliseconds),
109                    this,
110                    &TraySessionLengthLimit::Update);
111    }
112  } else {
113    remaining_session_time_ = base::TimeDelta();
114    limit_state_ = LIMIT_NONE;
115    timer_.reset();
116  }
117}
118
119void TraySessionLengthLimit::UpdateNotification() {
120  message_center::MessageCenter* message_center =
121      message_center::MessageCenter::Get();
122
123  // If state hasn't changed and the notification has already been acknowledged,
124  // we won't re-create it.
125  if (limit_state_ == last_limit_state_ &&
126      !message_center->FindVisibleNotificationById(kNotificationId)) {
127    return;
128  }
129
130  // After state change, any possibly existing notification is removed to make
131  // sure it is re-shown even if it had been acknowledged by the user before
132  // (and in the rare case of state change towards LIMIT_NONE to make the
133  // notification disappear).
134  if (limit_state_ != last_limit_state_ &&
135      message_center->FindVisibleNotificationById(kNotificationId)) {
136    message_center::MessageCenter::Get()->RemoveNotification(
137        kNotificationId, false /* by_user */);
138  }
139
140  // For LIMIT_NONE, there's nothing more to do.
141  if (limit_state_ == LIMIT_NONE) {
142    last_limit_state_ = limit_state_;
143    return;
144  }
145
146  ui::ResourceBundle& bundle = ui::ResourceBundle::GetSharedInstance();
147  message_center::RichNotificationData data;
148  data.should_make_spoken_feedback_for_popup_updates =
149      (limit_state_ != last_limit_state_);
150  scoped_ptr<message_center::Notification> notification(
151      new message_center::Notification(
152          message_center::NOTIFICATION_TYPE_SIMPLE,
153          kNotificationId,
154          base::string16() /* title */,
155          ComposeNotificationMessage() /* message */,
156          bundle.GetImageNamed(
157              IDR_AURA_UBER_TRAY_NOTIFICATION_SESSION_LENGTH_LIMIT),
158          base::string16() /* display_source */,
159          message_center::NotifierId(
160              message_center::NotifierId::SYSTEM_COMPONENT,
161              system_notifier::kNotifierSessionLengthTimeout),
162          data,
163          NULL /* delegate */));
164  notification->SetSystemPriority();
165  if (message_center->FindVisibleNotificationById(kNotificationId))
166    message_center->UpdateNotification(kNotificationId, notification.Pass());
167  else
168    message_center->AddNotification(notification.Pass());
169  last_limit_state_ = limit_state_;
170}
171
172void TraySessionLengthLimit::UpdateTrayBubbleView() const {
173  if (!tray_bubble_view_)
174    return;
175  if (limit_state_ == LIMIT_NONE)
176    tray_bubble_view_->SetMessage(base::string16());
177  else
178    tray_bubble_view_->SetMessage(ComposeTrayBubbleMessage());
179  tray_bubble_view_->Layout();
180}
181
182base::string16 TraySessionLengthLimit::ComposeNotificationMessage() const {
183  return l10n_util::GetStringFUTF16(
184      IDS_ASH_STATUS_TRAY_NOTIFICATION_SESSION_LENGTH_LIMIT,
185      ui::TimeFormat::Detailed(ui::TimeFormat::FORMAT_DURATION,
186                               ui::TimeFormat::LENGTH_LONG,
187                               10,
188                               remaining_session_time_));
189}
190
191base::string16 TraySessionLengthLimit::ComposeTrayBubbleMessage() const {
192  return l10n_util::GetStringFUTF16(
193      IDS_ASH_STATUS_TRAY_BUBBLE_SESSION_LENGTH_LIMIT,
194      ui::TimeFormat::Detailed(ui::TimeFormat::FORMAT_DURATION,
195                               ui::TimeFormat::LENGTH_LONG,
196                               10,
197                               remaining_session_time_));
198}
199
200}  // namespace ash
201