1 // Copyright (c) 2012 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/power/tray_power.h"
6
7 #include "ash/accessibility_delegate.h"
8 #include "ash/ash_switches.h"
9 #include "ash/shell.h"
10 #include "ash/system/chromeos/power/power_status_view.h"
11 #include "ash/system/date/date_view.h"
12 #include "ash/system/system_notifier.h"
13 #include "ash/system/tray/system_tray_delegate.h"
14 #include "ash/system/tray/tray_constants.h"
15 #include "ash/system/tray/tray_notification_view.h"
16 #include "ash/system/tray/tray_utils.h"
17 #include "base/command_line.h"
18 #include "base/metrics/histogram.h"
19 #include "base/time/time.h"
20 #include "grit/ash_resources.h"
21 #include "grit/ash_strings.h"
22 #include "third_party/icu/source/i18n/unicode/fieldpos.h"
23 #include "third_party/icu/source/i18n/unicode/fmtable.h"
24 #include "ui/accessibility/ax_view_state.h"
25 #include "ui/base/resource/resource_bundle.h"
26 #include "ui/message_center/message_center.h"
27 #include "ui/message_center/notification.h"
28 #include "ui/views/controls/button/button.h"
29 #include "ui/views/controls/image_view.h"
30 #include "ui/views/controls/label.h"
31 #include "ui/views/layout/box_layout.h"
32 #include "ui/views/layout/fill_layout.h"
33 #include "ui/views/layout/grid_layout.h"
34 #include "ui/views/view.h"
35 #include "ui/views/widget/widget.h"
36
37 using message_center::MessageCenter;
38 using message_center::Notification;
39
40 namespace ash {
41 namespace tray {
42 namespace {
43
44 const int kMaxSpringChargerAccessibilityNotifyCount = 3;
45 const int kSpringChargerAccessibilityTimerFirstTimeNotifyInSeconds = 30;
46 const int kSpringChargerAccessibilityTimerRepeatInMinutes = 5;
47
48 }
49
50 // This view is used only for the tray.
51 class PowerTrayView : public views::ImageView {
52 public:
PowerTrayView()53 PowerTrayView()
54 : spring_charger_spoken_notification_count_(0) {
55 UpdateImage();
56 }
57
~PowerTrayView()58 virtual ~PowerTrayView() {
59 }
60
61 // Overriden from views::View.
GetAccessibleState(ui::AXViewState * state)62 virtual void GetAccessibleState(ui::AXViewState* state) OVERRIDE {
63 state->name = accessible_name_;
64 state->role = ui::AX_ROLE_BUTTON;
65 }
66
UpdateStatus(bool battery_alert)67 void UpdateStatus(bool battery_alert) {
68 UpdateImage();
69 SetVisible(PowerStatus::Get()->IsBatteryPresent());
70
71 if (battery_alert) {
72 accessible_name_ = PowerStatus::Get()->GetAccessibleNameString(true);
73 NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);
74 }
75 }
76
SetupNotifyBadCharger()77 void SetupNotifyBadCharger() {
78 // Poll with a shorter duration timer to notify the charger issue
79 // for the first time after the charger dialog is displayed.
80 spring_charger_accessility_timer_.Start(
81 FROM_HERE, base::TimeDelta::FromSeconds(
82 kSpringChargerAccessibilityTimerFirstTimeNotifyInSeconds),
83 this, &PowerTrayView::NotifyChargerIssue);
84 }
85
86 private:
UpdateImage()87 void UpdateImage() {
88 SetImage(PowerStatus::Get()->GetBatteryImage(PowerStatus::ICON_LIGHT));
89 }
90
NotifyChargerIssue()91 void NotifyChargerIssue() {
92 if (!Shell::GetInstance()->accessibility_delegate()->
93 IsSpokenFeedbackEnabled())
94 return;
95
96 if (!Shell::GetInstance()->system_tray_delegate()->
97 IsSpringChargerReplacementDialogVisible()) {
98 spring_charger_accessility_timer_.Stop();
99 return;
100 }
101
102 accessible_name_ = ui::ResourceBundle::GetSharedInstance().
103 GetLocalizedString(IDS_CHARGER_REPLACEMENT_ACCESSIBILTY_NOTIFICATION);
104 NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);
105 ++spring_charger_spoken_notification_count_;
106
107 if (spring_charger_spoken_notification_count_ == 1) {
108 // After notify the charger issue for the first time, repeat the
109 // notification with a longer duration timer.
110 spring_charger_accessility_timer_.Stop();
111 spring_charger_accessility_timer_.Start(
112 FROM_HERE, base::TimeDelta::FromMinutes(
113 kSpringChargerAccessibilityTimerRepeatInMinutes),
114 this, &PowerTrayView::NotifyChargerIssue);
115 } else if (spring_charger_spoken_notification_count_ >=
116 kMaxSpringChargerAccessibilityNotifyCount) {
117 spring_charger_accessility_timer_.Stop();
118 }
119 }
120
121 base::string16 accessible_name_;
122
123 // Tracks how many times the original spring charger accessibility
124 // notification has been spoken.
125 int spring_charger_spoken_notification_count_;
126
127 base::RepeatingTimer<PowerTrayView> spring_charger_accessility_timer_;
128
129 DISALLOW_COPY_AND_ASSIGN(PowerTrayView);
130 };
131
132 class PowerNotificationView : public TrayNotificationView {
133 public:
PowerNotificationView(TrayPower * owner)134 explicit PowerNotificationView(TrayPower* owner)
135 : TrayNotificationView(owner, 0) {
136 power_status_view_ =
137 new PowerStatusView(PowerStatusView::VIEW_NOTIFICATION, true);
138 InitView(power_status_view_);
139 }
140
UpdateStatus()141 void UpdateStatus() {
142 SetIconImage(PowerStatus::Get()->GetBatteryImage(PowerStatus::ICON_DARK));
143 }
144
145 private:
146 PowerStatusView* power_status_view_;
147
148 DISALLOW_COPY_AND_ASSIGN(PowerNotificationView);
149 };
150
151 } // namespace tray
152
153 using tray::PowerNotificationView;
154
155 const int TrayPower::kCriticalMinutes = 5;
156 const int TrayPower::kLowPowerMinutes = 15;
157 const int TrayPower::kNoWarningMinutes = 30;
158 const int TrayPower::kCriticalPercentage = 5;
159 const int TrayPower::kLowPowerPercentage = 10;
160 const int TrayPower::kNoWarningPercentage = 15;
161
TrayPower(SystemTray * system_tray,MessageCenter * message_center)162 TrayPower::TrayPower(SystemTray* system_tray, MessageCenter* message_center)
163 : SystemTrayItem(system_tray),
164 message_center_(message_center),
165 power_tray_(NULL),
166 notification_view_(NULL),
167 notification_state_(NOTIFICATION_NONE),
168 usb_charger_was_connected_(false),
169 line_power_was_connected_(false) {
170 PowerStatus::Get()->AddObserver(this);
171 }
172
~TrayPower()173 TrayPower::~TrayPower() {
174 PowerStatus::Get()->RemoveObserver(this);
175 }
176
CreateTrayView(user::LoginStatus status)177 views::View* TrayPower::CreateTrayView(user::LoginStatus status) {
178 // There may not be enough information when this is created about whether
179 // there is a battery or not. So always create this, and adjust visibility as
180 // necessary.
181 CHECK(power_tray_ == NULL);
182 power_tray_ = new tray::PowerTrayView();
183 power_tray_->UpdateStatus(false);
184 return power_tray_;
185 }
186
CreateDefaultView(user::LoginStatus status)187 views::View* TrayPower::CreateDefaultView(user::LoginStatus status) {
188 // Make sure icon status is up-to-date. (Also triggers stub activation).
189 PowerStatus::Get()->RequestStatusUpdate();
190 return NULL;
191 }
192
CreateNotificationView(user::LoginStatus status)193 views::View* TrayPower::CreateNotificationView(user::LoginStatus status) {
194 CHECK(notification_view_ == NULL);
195 if (!PowerStatus::Get()->IsBatteryPresent())
196 return NULL;
197
198 notification_view_ = new PowerNotificationView(this);
199 notification_view_->UpdateStatus();
200
201 return notification_view_;
202 }
203
DestroyTrayView()204 void TrayPower::DestroyTrayView() {
205 power_tray_ = NULL;
206 }
207
DestroyDefaultView()208 void TrayPower::DestroyDefaultView() {
209 }
210
DestroyNotificationView()211 void TrayPower::DestroyNotificationView() {
212 notification_view_ = NULL;
213 }
214
UpdateAfterLoginStatusChange(user::LoginStatus status)215 void TrayPower::UpdateAfterLoginStatusChange(user::LoginStatus status) {
216 }
217
UpdateAfterShelfAlignmentChange(ShelfAlignment alignment)218 void TrayPower::UpdateAfterShelfAlignmentChange(ShelfAlignment alignment) {
219 SetTrayImageItemBorder(power_tray_, alignment);
220 }
221
OnPowerStatusChanged()222 void TrayPower::OnPowerStatusChanged() {
223 RecordChargerType();
224
225 if (PowerStatus::Get()->IsOriginalSpringChargerConnected()) {
226 if (ash::Shell::GetInstance()->system_tray_delegate()->
227 ShowSpringChargerReplacementDialog()) {
228 power_tray_->SetupNotifyBadCharger();
229 }
230 }
231
232 bool battery_alert = UpdateNotificationState();
233 if (power_tray_)
234 power_tray_->UpdateStatus(battery_alert);
235 if (notification_view_)
236 notification_view_->UpdateStatus();
237
238 // Factory testing may place the battery into unusual states.
239 if (CommandLine::ForCurrentProcess()->HasSwitch(
240 ash::switches::kAshHideNotificationsForFactory))
241 return;
242
243 MaybeShowUsbChargerNotification();
244
245 if (battery_alert)
246 ShowNotificationView();
247 else if (notification_state_ == NOTIFICATION_NONE)
248 HideNotificationView();
249
250 usb_charger_was_connected_ = PowerStatus::Get()->IsUsbChargerConnected();
251 line_power_was_connected_ = PowerStatus::Get()->IsLinePowerConnected();
252 }
253
MaybeShowUsbChargerNotification()254 bool TrayPower::MaybeShowUsbChargerNotification() {
255 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
256 const char kNotificationId[] = "usb-charger";
257 bool usb_charger_is_connected = PowerStatus::Get()->IsUsbChargerConnected();
258
259 // Check for a USB charger being connected.
260 if (usb_charger_is_connected && !usb_charger_was_connected_) {
261 scoped_ptr<Notification> notification(new Notification(
262 message_center::NOTIFICATION_TYPE_SIMPLE,
263 kNotificationId,
264 rb.GetLocalizedString(IDS_ASH_STATUS_TRAY_LOW_POWER_CHARGER_TITLE),
265 rb.GetLocalizedString(
266 IDS_ASH_STATUS_TRAY_LOW_POWER_CHARGER_MESSAGE_SHORT),
267 rb.GetImageNamed(IDR_AURA_NOTIFICATION_LOW_POWER_CHARGER),
268 base::string16(),
269 message_center::NotifierId(
270 message_center::NotifierId::SYSTEM_COMPONENT,
271 system_notifier::kNotifierPower),
272 message_center::RichNotificationData(),
273 NULL));
274 message_center_->AddNotification(notification.Pass());
275 return true;
276 }
277
278 // Check for unplug of a USB charger while the USB charger notification is
279 // showing.
280 if (!usb_charger_is_connected && usb_charger_was_connected_) {
281 message_center_->RemoveNotification(kNotificationId, false);
282 return true;
283 }
284 return false;
285 }
286
UpdateNotificationState()287 bool TrayPower::UpdateNotificationState() {
288 const PowerStatus& status = *PowerStatus::Get();
289 if (!status.IsBatteryPresent() ||
290 status.IsBatteryTimeBeingCalculated() ||
291 status.IsMainsChargerConnected() ||
292 status.IsOriginalSpringChargerConnected()) {
293 notification_state_ = NOTIFICATION_NONE;
294 return false;
295 }
296
297 return status.IsUsbChargerConnected() ?
298 UpdateNotificationStateForRemainingPercentage() :
299 UpdateNotificationStateForRemainingTime();
300 }
301
UpdateNotificationStateForRemainingTime()302 bool TrayPower::UpdateNotificationStateForRemainingTime() {
303 // The notification includes a rounded minutes value, so round the estimate
304 // received from the power manager to match.
305 const int remaining_minutes = static_cast<int>(
306 PowerStatus::Get()->GetBatteryTimeToEmpty().InSecondsF() / 60.0 + 0.5);
307
308 if (remaining_minutes >= kNoWarningMinutes ||
309 PowerStatus::Get()->IsBatteryFull()) {
310 notification_state_ = NOTIFICATION_NONE;
311 return false;
312 }
313
314 switch (notification_state_) {
315 case NOTIFICATION_NONE:
316 if (remaining_minutes <= kCriticalMinutes) {
317 notification_state_ = NOTIFICATION_CRITICAL;
318 return true;
319 }
320 if (remaining_minutes <= kLowPowerMinutes) {
321 notification_state_ = NOTIFICATION_LOW_POWER;
322 return true;
323 }
324 return false;
325 case NOTIFICATION_LOW_POWER:
326 if (remaining_minutes <= kCriticalMinutes) {
327 notification_state_ = NOTIFICATION_CRITICAL;
328 return true;
329 }
330 return false;
331 case NOTIFICATION_CRITICAL:
332 return false;
333 }
334 NOTREACHED();
335 return false;
336 }
337
UpdateNotificationStateForRemainingPercentage()338 bool TrayPower::UpdateNotificationStateForRemainingPercentage() {
339 // The notification includes a rounded percentage, so round the value received
340 // from the power manager to match.
341 const int remaining_percentage =
342 PowerStatus::Get()->GetRoundedBatteryPercent();
343
344 if (remaining_percentage >= kNoWarningPercentage ||
345 PowerStatus::Get()->IsBatteryFull()) {
346 notification_state_ = NOTIFICATION_NONE;
347 return false;
348 }
349
350 switch (notification_state_) {
351 case NOTIFICATION_NONE:
352 if (remaining_percentage <= kCriticalPercentage) {
353 notification_state_ = NOTIFICATION_CRITICAL;
354 return true;
355 }
356 if (remaining_percentage <= kLowPowerPercentage) {
357 notification_state_ = NOTIFICATION_LOW_POWER;
358 return true;
359 }
360 return false;
361 case NOTIFICATION_LOW_POWER:
362 if (remaining_percentage <= kCriticalPercentage) {
363 notification_state_ = NOTIFICATION_CRITICAL;
364 return true;
365 }
366 return false;
367 case NOTIFICATION_CRITICAL:
368 return false;
369 }
370 NOTREACHED();
371 return false;
372 }
373
RecordChargerType()374 void TrayPower::RecordChargerType() {
375 if (!PowerStatus::Get()->IsLinePowerConnected() ||
376 line_power_was_connected_)
377 return;
378
379 ChargerType current_charger = UNKNOWN_CHARGER;
380 if (PowerStatus::Get()->IsMainsChargerConnected()) {
381 current_charger = MAINS_CHARGER;
382 } else if (PowerStatus::Get()->IsUsbChargerConnected()) {
383 current_charger = USB_CHARGER;
384 } else if (PowerStatus::Get()->IsOriginalSpringChargerConnected()) {
385 current_charger =
386 ash::Shell::GetInstance()->system_tray_delegate()->
387 HasUserConfirmedSafeSpringCharger() ?
388 SAFE_SPRING_CHARGER : UNCONFIRMED_SPRING_CHARGER;
389 }
390
391 if (current_charger != UNKNOWN_CHARGER) {
392 UMA_HISTOGRAM_ENUMERATION("Power.ChargerType",
393 current_charger,
394 CHARGER_TYPE_COUNT);
395 }
396 }
397
398 } // namespace ash
399