tray_power.cc revision 7d4cd473f85ac64c3747c96c277f9e506a0d2246
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/ash_switches.h"
8#include "ash/shell.h"
9#include "ash/shell_delegate.h"
10#include "ash/system/chromeos/power/power_status_view.h"
11#include "ash/system/date/date_view.h"
12#include "ash/system/tray/tray_constants.h"
13#include "ash/system/tray/tray_notification_view.h"
14#include "ash/system/tray/tray_utils.h"
15#include "base/command_line.h"
16#include "base/strings/string_number_conversions.h"
17#include "base/strings/stringprintf.h"
18#include "base/strings/utf_string_conversions.h"
19#include "chromeos/dbus/power_supply_status.h"
20#include "grit/ash_resources.h"
21#include "grit/ash_strings.h"
22#include "third_party/icu/public/i18n/unicode/fieldpos.h"
23#include "third_party/icu/public/i18n/unicode/fmtable.h"
24#include "third_party/skia/include/core/SkRect.h"
25#include "ui/base/accessibility/accessible_view_state.h"
26#include "ui/base/l10n/l10n_util.h"
27#include "ui/base/resource/resource_bundle.h"
28#include "ui/gfx/image/image.h"
29#include "ui/gfx/image/image_skia.h"
30#include "ui/gfx/image/image_skia_operations.h"
31#include "ui/gfx/size.h"
32#include "ui/views/controls/button/button.h"
33#include "ui/views/controls/image_view.h"
34#include "ui/views/controls/label.h"
35#include "ui/views/layout/box_layout.h"
36#include "ui/views/layout/fill_layout.h"
37#include "ui/views/layout/grid_layout.h"
38#include "ui/views/view.h"
39#include "ui/views/widget/widget.h"
40
41using chromeos::PowerManagerHandler;
42using chromeos::PowerSupplyStatus;
43
44namespace ash {
45namespace internal {
46
47namespace {
48// Width and height of battery images.
49const int kBatteryImageHeight = 25;
50const int kBatteryImageWidth = 25;
51// Number of different power states.
52const int kNumPowerImages = 15;
53// Top/bottom padding of the text items.
54const int kPaddingVertical = 10;
55// Specify min width of status label for layout.
56const int kLabelMinWidth = 120;
57// Notification times.
58const int kCriticalSeconds = 5 * 60;
59const int kLowPowerSeconds = 15 * 60;
60const int kNoWarningSeconds = 30 * 60;
61// Minimum battery percentage rendered in UI.
62const int kMinBatteryPercent = 1;
63// Notification in battery percentage.
64const double kCriticalPercentage = 5.0;
65const double kLowPowerPercentage = 10.0;
66const double kNoWarningPercentage = 15.0;
67
68base::string16 GetBatteryTimeAccessibilityString(int hour, int min) {
69  DCHECK(hour || min);
70  if (hour && !min) {
71    return Shell::GetInstance()->delegate()->GetTimeDurationLongString(
72        base::TimeDelta::FromHours(hour));
73  }
74  if (min && !hour) {
75    return Shell::GetInstance()->delegate()->GetTimeDurationLongString(
76        base::TimeDelta::FromMinutes(min));
77  }
78  return l10n_util::GetStringFUTF16(
79      IDS_ASH_STATUS_TRAY_BATTERY_TIME_ACCESSIBLE,
80      Shell::GetInstance()->delegate()->GetTimeDurationLongString(
81          base::TimeDelta::FromHours(hour)),
82      Shell::GetInstance()->delegate()->GetTimeDurationLongString(
83          base::TimeDelta::FromMinutes(min)));
84}
85
86}  // namespace
87
88namespace tray {
89
90// This view is used only for the tray.
91class PowerTrayView : public views::ImageView {
92 public:
93  PowerTrayView()
94      : battery_icon_index_(-1),
95        battery_icon_offset_(0),
96        battery_charging_unreliable_(false) {
97    UpdateImage();
98  }
99
100  virtual ~PowerTrayView() {
101  }
102
103  // Overriden from views::View.
104  virtual void GetAccessibleState(ui::AccessibleViewState* state) OVERRIDE {
105    state->name = accessible_name_;
106    state->role = ui::AccessibilityTypes::ROLE_PUSHBUTTON;
107  }
108
109  void UpdatePowerStatus(const PowerSupplyStatus& status,
110                         bool battery_alert) {
111    supply_status_ = status;
112    // Sanitize.
113    if (supply_status_.battery_is_full)
114      supply_status_.battery_percentage = 100.0;
115
116    UpdateImage();
117    SetVisible(status.battery_is_present);
118
119    if (battery_alert) {
120      accessible_name_ = TrayPower::GetAccessibleNameString(status);
121      NotifyAccessibilityEvent(ui::AccessibilityTypes::EVENT_ALERT, true);
122    }
123  }
124
125 private:
126  void UpdateImage() {
127    int index = TrayPower::GetBatteryImageIndex(supply_status_);
128    int offset = TrayPower::GetBatteryImageOffset(supply_status_);
129    bool charging_unreliable =
130        TrayPower::IsBatteryChargingUnreliable(supply_status_);
131    if (battery_icon_index_ != index ||
132        battery_icon_offset_ != offset ||
133        battery_charging_unreliable_ != charging_unreliable) {
134      battery_icon_index_ = index;
135      battery_icon_offset_ = offset;
136      battery_charging_unreliable_ = charging_unreliable;
137      if (battery_icon_index_ != -1)
138        SetImage(TrayPower::GetBatteryImage(battery_icon_index_,
139                                            battery_icon_offset_,
140                                            battery_charging_unreliable_,
141                                            ICON_LIGHT));
142    }
143  }
144
145  PowerSupplyStatus supply_status_;
146  base::string16 accessible_name_;
147
148  // Index of the current icon in the icon array image, or -1 if unknown.
149  int battery_icon_index_;
150  int battery_icon_offset_;
151  bool battery_charging_unreliable_;
152
153  DISALLOW_COPY_AND_ASSIGN(PowerTrayView);
154};
155
156class PowerNotificationView : public TrayNotificationView {
157 public:
158  explicit PowerNotificationView(TrayPower* owner)
159      : TrayNotificationView(owner, 0),
160        battery_icon_index_(-1),
161        battery_icon_offset_(0),
162        battery_charging_unreliable_(false) {
163    power_status_view_ =
164        new PowerStatusView(PowerStatusView::VIEW_NOTIFICATION, true);
165    InitView(power_status_view_);
166  }
167
168  void UpdatePowerStatus(const PowerSupplyStatus& status) {
169    int index = TrayPower::GetBatteryImageIndex(status);
170    int offset = TrayPower::GetBatteryImageOffset(status);
171    bool charging_unreliable = TrayPower::IsBatteryChargingUnreliable(status);
172    if (battery_icon_index_ != index ||
173        battery_icon_offset_ != offset ||
174        battery_charging_unreliable_ != charging_unreliable) {
175      battery_icon_index_ = index;
176      battery_icon_offset_ = offset;
177      battery_charging_unreliable_ = charging_unreliable;
178      if (battery_icon_index_ != -1) {
179        SetIconImage(TrayPower::GetBatteryImage(
180                         battery_icon_index_,
181                         battery_icon_offset_,
182                         battery_charging_unreliable_,
183                         ICON_DARK));
184      }
185    }
186    power_status_view_->UpdatePowerStatus(status);
187  }
188
189 private:
190  PowerStatusView* power_status_view_;
191
192  // Index of the current icon in the icon array image, or -1 if unknown.
193  int battery_icon_index_;
194  int battery_icon_offset_;
195  bool battery_charging_unreliable_;
196
197  DISALLOW_COPY_AND_ASSIGN(PowerNotificationView);
198};
199
200}  // namespace tray
201
202using tray::PowerNotificationView;
203
204TrayPower::TrayPower(SystemTray* system_tray)
205    : SystemTrayItem(system_tray),
206      power_tray_(NULL),
207      notification_view_(NULL),
208      notification_state_(NOTIFICATION_NONE) {
209  PowerManagerHandler::Get()->AddObserver(this);
210}
211
212TrayPower::~TrayPower() {
213  if (PowerManagerHandler::IsInitialized())
214    PowerManagerHandler::Get()->RemoveObserver(this);
215}
216
217// static
218bool TrayPower::IsBatteryChargingUnreliable(
219    const chromeos::PowerSupplyStatus& supply_status) {
220  // Sometimes devices can get into a state where the battery is almost fully
221  // charged and the power subsystem reports "neither charging nor discharging"
222  // despite the battery not at 100%. For now, only report unreliable charging
223  // on USB.
224  // TODO(derat): Update this when the power manager code is refactored for M29.
225  return supply_status.battery_state == PowerSupplyStatus::CONNECTED_TO_USB;
226}
227
228// static
229int TrayPower::GetBatteryImageIndex(
230    const chromeos::PowerSupplyStatus& supply_status) {
231  int image_index = 0;
232  if (supply_status.battery_percentage >= 100) {
233    image_index = kNumPowerImages - 1;
234  } else if (!supply_status.battery_is_present) {
235    image_index = kNumPowerImages;
236  } else {
237    image_index = static_cast<int>(supply_status.battery_percentage /
238                                   100.0 * (kNumPowerImages - 1));
239    image_index = std::max(std::min(image_index, kNumPowerImages - 2), 0);
240  }
241  return image_index;
242}
243
244// static
245int TrayPower::GetBatteryImageOffset(
246    const chromeos::PowerSupplyStatus& supply_status) {
247  if (IsBatteryChargingUnreliable(supply_status) ||
248      !supply_status.line_power_on)
249    return 0;
250  return 1;
251}
252
253// static
254gfx::ImageSkia TrayPower::GetBatteryImage(int image_index,
255                                          int image_offset,
256                                          bool charging_unreliable,
257                                          IconSet icon_set) {
258  gfx::Image all;
259  if (charging_unreliable) {
260    all = ui::ResourceBundle::GetSharedInstance().GetImageNamed(
261        icon_set == ICON_DARK ?
262        IDR_AURA_UBER_TRAY_POWER_SMALL_CHARGING_UNRELIABLE_DARK :
263        IDR_AURA_UBER_TRAY_POWER_SMALL_CHARGING_UNRELIABLE);
264  } else {
265    all = ui::ResourceBundle::GetSharedInstance().GetImageNamed(
266        icon_set == ICON_DARK ?
267        IDR_AURA_UBER_TRAY_POWER_SMALL_DARK : IDR_AURA_UBER_TRAY_POWER_SMALL);
268  }
269  gfx::Rect region(
270      image_offset * kBatteryImageWidth,
271      image_index * kBatteryImageHeight,
272      kBatteryImageWidth, kBatteryImageHeight);
273  return gfx::ImageSkiaOperations::ExtractSubset(*all.ToImageSkia(), region);
274}
275
276// static
277base::string16 TrayPower::GetAccessibleNameString(
278    const chromeos::PowerSupplyStatus& supply_status) {
279  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
280  if (supply_status.line_power_on && supply_status.battery_is_full) {
281    return rb.GetLocalizedString(
282        IDS_ASH_STATUS_TRAY_BATTERY_FULL_CHARGE_ACCESSIBLE);
283  }
284  bool charging_unreliable =
285      IsBatteryChargingUnreliable(supply_status);
286  if (supply_status.battery_percentage < 0.0f) {
287    if (charging_unreliable) {
288      return rb.GetLocalizedString(
289          IDS_ASH_STATUS_TRAY_BATTERY_CHARGING_UNRELIABLE_ACCESSIBLE);
290    }
291    return rb.GetLocalizedString(
292        IDS_ASH_STATUS_TRAY_BATTERY_CALCULATING_ACCESSIBLE);
293  }
294  base::string16 battery_percentage_accessbile = l10n_util::GetStringFUTF16(
295      supply_status.line_power_on ?
296      IDS_ASH_STATUS_TRAY_BATTERY_PERCENT_CHARGING_ACCESSIBLE:
297      IDS_ASH_STATUS_TRAY_BATTERY_PERCENT_ACCESSIBLE ,
298      base::IntToString16(GetRoundedBatteryPercentage(
299          supply_status.battery_percentage)));
300  base::string16 battery_time_accessible = base::string16();
301  if (charging_unreliable) {
302    battery_time_accessible = rb.GetLocalizedString(
303        IDS_ASH_STATUS_TRAY_BATTERY_CHARGING_UNRELIABLE_ACCESSIBLE);
304  } else {
305    if (supply_status.is_calculating_battery_time) {
306      battery_time_accessible = rb.GetLocalizedString(
307          IDS_ASH_STATUS_TRAY_BATTERY_CALCULATING_ACCESSIBLE);
308    } else {
309      base::TimeDelta time = base::TimeDelta::FromSeconds(
310          supply_status.line_power_on ?
311          supply_status.battery_seconds_to_full :
312          supply_status.battery_seconds_to_empty);
313      int hour = time.InHours();
314      int min = (time - base::TimeDelta::FromHours(hour)).InMinutes();
315      if (hour || min) {
316        base::string16 minute = min < 10 ?
317            ASCIIToUTF16("0") + base::IntToString16(min) :
318            base::IntToString16(min);
319        battery_time_accessible =
320            l10n_util::GetStringFUTF16(
321                supply_status.line_power_on ?
322                IDS_ASH_STATUS_TRAY_BATTERY_TIME_UNTIL_FULL_ACCESSIBLE :
323                IDS_ASH_STATUS_TRAY_BATTERY_TIME_LEFT_ACCESSIBLE,
324                GetBatteryTimeAccessibilityString(hour, min));
325      }
326    }
327  }
328  return battery_time_accessible.empty() ?
329      battery_percentage_accessbile :
330      battery_percentage_accessbile + ASCIIToUTF16(". ")
331      + battery_time_accessible;
332}
333
334// static
335int TrayPower::GetRoundedBatteryPercentage(double battery_percentage) {
336  DCHECK(battery_percentage >= 0.0);
337  return std::max(kMinBatteryPercent,
338      static_cast<int>(battery_percentage + 0.5));
339}
340
341views::View* TrayPower::CreateTrayView(user::LoginStatus status) {
342  // There may not be enough information when this is created about whether
343  // there is a battery or not. So always create this, and adjust visibility as
344  // necessary.
345  PowerSupplyStatus power_status =
346      PowerManagerHandler::Get()->GetPowerSupplyStatus();
347  CHECK(power_tray_ == NULL);
348  power_tray_ = new tray::PowerTrayView();
349  power_tray_->UpdatePowerStatus(power_status, false);
350  return power_tray_;
351}
352
353views::View* TrayPower::CreateDefaultView(user::LoginStatus status) {
354  // Make sure icon status is up-to-date. (Also triggers stub activation).
355  RequestStatusUpdate();
356  return NULL;
357}
358
359views::View* TrayPower::CreateNotificationView(user::LoginStatus status) {
360  CHECK(notification_view_ == NULL);
361  PowerSupplyStatus power_status =
362      PowerManagerHandler::Get()->GetPowerSupplyStatus();
363  if (!power_status.battery_is_present)
364    return NULL;
365
366  notification_view_ = new PowerNotificationView(this);
367  notification_view_->UpdatePowerStatus(power_status);
368
369  return notification_view_;
370}
371
372void TrayPower::DestroyTrayView() {
373  power_tray_ = NULL;
374}
375
376void TrayPower::DestroyDefaultView() {
377}
378
379void TrayPower::DestroyNotificationView() {
380  notification_view_ = NULL;
381}
382
383void TrayPower::UpdateAfterLoginStatusChange(user::LoginStatus status) {
384}
385
386void TrayPower::UpdateAfterShelfAlignmentChange(ShelfAlignment alignment) {
387  SetTrayImageItemBorder(power_tray_, alignment);
388}
389
390void TrayPower::OnPowerStatusChanged(
391    const chromeos::PowerSupplyStatus& status) {
392  bool battery_alert = UpdateNotificationState(status);
393  if (power_tray_)
394    power_tray_->UpdatePowerStatus(status, battery_alert);
395  if (notification_view_)
396    notification_view_->UpdatePowerStatus(status);
397
398  // Factory testing may place the battery into unusual states.
399  if (CommandLine::ForCurrentProcess()->HasSwitch(
400          ash::switches::kAshHideNotificationsForFactory))
401    return;
402
403  if (battery_alert)
404    ShowNotificationView();
405  else if (notification_state_ == NOTIFICATION_NONE)
406    HideNotificationView();
407}
408
409void TrayPower::RequestStatusUpdate() const {
410  PowerManagerHandler::Get()->RequestStatusUpdate();
411}
412
413bool TrayPower::UpdateNotificationState(
414    const chromeos::PowerSupplyStatus& status) {
415  if (!status.battery_is_present ||
416      status.is_calculating_battery_time ||
417      status.battery_state == PowerSupplyStatus::CHARGING) {
418    notification_state_ = NOTIFICATION_NONE;
419    return false;
420  }
421
422  if (TrayPower::IsBatteryChargingUnreliable(status)) {
423    return UpdateNotificationStateForRemainingPercentage(
424        status.battery_percentage);
425  } else {
426    return UpdateNotificationStateForRemainingTime(
427        status.battery_seconds_to_empty);
428  }
429}
430
431bool TrayPower::UpdateNotificationStateForRemainingTime(int remaining_seconds) {
432  if (remaining_seconds >= kNoWarningSeconds) {
433    notification_state_ = NOTIFICATION_NONE;
434    return false;
435  }
436
437  switch (notification_state_) {
438    case NOTIFICATION_NONE:
439      if (remaining_seconds <= kCriticalSeconds) {
440        notification_state_ = NOTIFICATION_CRITICAL;
441        return true;
442      } else if (remaining_seconds <= kLowPowerSeconds) {
443        notification_state_ = NOTIFICATION_LOW_POWER;
444        return true;
445      }
446      return false;
447    case NOTIFICATION_LOW_POWER:
448      if (remaining_seconds <= kCriticalSeconds) {
449        notification_state_ = NOTIFICATION_CRITICAL;
450        return true;
451      }
452      return false;
453    case NOTIFICATION_CRITICAL:
454      return false;
455  }
456  NOTREACHED();
457  return false;
458}
459
460bool TrayPower::UpdateNotificationStateForRemainingPercentage(
461    double remaining_percentage) {
462  if (remaining_percentage > kNoWarningPercentage) {
463    notification_state_ = NOTIFICATION_NONE;
464    return false;
465  }
466
467  switch (notification_state_) {
468    case NOTIFICATION_NONE:
469      if (remaining_percentage <= kCriticalPercentage) {
470        notification_state_ = NOTIFICATION_CRITICAL;
471        return true;
472      }
473      if (remaining_percentage <= kLowPowerPercentage) {
474        notification_state_ = NOTIFICATION_LOW_POWER;
475        return true;
476      }
477      return false;
478    case NOTIFICATION_LOW_POWER:
479      if (remaining_percentage <= kCriticalPercentage) {
480        notification_state_ = NOTIFICATION_CRITICAL;
481        return true;
482      }
483      return false;
484    case NOTIFICATION_CRITICAL:
485      return false;
486  }
487  NOTREACHED();
488  return false;
489}
490
491}  // namespace internal
492}  // namespace ash
493