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/system/power/power_notification_controller.h"
6 
7 #include "ash/public/cpp/ash_switches.h"
8 #include "ash/public/cpp/notification_utils.h"
9 #include "ash/resources/vector_icons/vector_icons.h"
10 #include "ash/strings/grit/ash_strings.h"
11 #include "ash/system/power/battery_notification.h"
12 #include "ash/system/power/dual_role_notification.h"
13 #include "base/command_line.h"
14 #include "base/i18n/number_formatting.h"
15 #include "base/logging.h"
16 #include "base/numerics/safe_conversions.h"
17 #include "ui/base/l10n/l10n_util.h"
18 #include "ui/chromeos/devicetype_utils.h"
19 #include "ui/message_center/message_center.h"
20 #include "ui/message_center/public/cpp/notification.h"
21 #include "ui/message_center/public/cpp/notification_delegate.h"
22 
23 using message_center::MessageCenter;
24 using message_center::Notification;
25 
26 namespace ash {
27 
28 namespace {
29 
30 const char kNotifierPower[] = "ash.power";
31 
32 // Informs the PowerNotificationController when a USB notification is closed.
33 class UsbNotificationDelegate : public message_center::NotificationDelegate {
34  public:
UsbNotificationDelegate(PowerNotificationController * controller)35   explicit UsbNotificationDelegate(PowerNotificationController* controller)
36       : controller_(controller) {}
37 
38   // Overridden from message_center::NotificationDelegate.
Close(bool by_user)39   void Close(bool by_user) override {
40     if (by_user)
41       controller_->NotifyUsbNotificationClosedByUser();
42   }
43 
44  private:
45   ~UsbNotificationDelegate() override = default;
46 
47   PowerNotificationController* const controller_;
48 
49   DISALLOW_COPY_AND_ASSIGN(UsbNotificationDelegate);
50 };
51 
GetNotificationStateString(PowerNotificationController::NotificationState notification_state)52 std::string GetNotificationStateString(
53     PowerNotificationController::NotificationState notification_state) {
54   switch (notification_state) {
55     case PowerNotificationController::NOTIFICATION_NONE:
56       return "none";
57     case PowerNotificationController::NOTIFICATION_LOW_POWER:
58       return "low power";
59     case PowerNotificationController::NOTIFICATION_CRITICAL:
60       return "critical power";
61   }
62   NOTREACHED() << "Unknown state " << notification_state;
63   return "Unknown state";
64 }
65 
LogBatteryForUsbCharger(PowerNotificationController::NotificationState state,int battery_percent)66 void LogBatteryForUsbCharger(
67     PowerNotificationController::NotificationState state,
68     int battery_percent) {
69   VLOG(1) << "Showing " << GetNotificationStateString(state)
70           << " notification. USB charger is connected. "
71           << "Battery percentage: " << battery_percent << "%.";
72 }
73 
LogBatteryForNoCharger(PowerNotificationController::NotificationState state,int remaining_minutes)74 void LogBatteryForNoCharger(
75     PowerNotificationController::NotificationState state,
76     int remaining_minutes) {
77   VLOG(1) << "Showing " << GetNotificationStateString(state)
78           << " notification. No charger connected."
79           << " Remaining time: " << remaining_minutes << " minutes.";
80 }
81 
82 }  // namespace
83 
84 const char PowerNotificationController::kUsbNotificationId[] = "usb-charger";
85 
PowerNotificationController(message_center::MessageCenter * message_center)86 PowerNotificationController::PowerNotificationController(
87     message_center::MessageCenter* message_center)
88     : message_center_(message_center) {
89   PowerStatus::Get()->AddObserver(this);
90 }
91 
~PowerNotificationController()92 PowerNotificationController::~PowerNotificationController() {
93   PowerStatus::Get()->RemoveObserver(this);
94   message_center_->RemoveNotification(kUsbNotificationId, false);
95 }
96 
OnPowerStatusChanged()97 void PowerNotificationController::OnPowerStatusChanged() {
98   bool battery_alert = UpdateNotificationState();
99 
100   // Factory testing may place the battery into unusual states.
101   if (base::CommandLine::ForCurrentProcess()->HasSwitch(
102           switches::kAshHideNotificationsForFactory)) {
103     return;
104   }
105 
106   MaybeShowUsbChargerNotification();
107   MaybeShowDualRoleNotification();
108 
109   if (battery_alert) {
110     // Remove any existing notification so it's dismissed before adding a new
111     // one. Otherwise we might update a "low battery" notification to "critical"
112     // without it being shown again.
113     battery_notification_.reset();
114     battery_notification_.reset(
115         new BatteryNotification(message_center_, notification_state_));
116   } else if (notification_state_ == NOTIFICATION_NONE) {
117     battery_notification_.reset();
118   } else if (battery_notification_.get()) {
119     battery_notification_->Update(notification_state_);
120   }
121 
122   battery_was_full_ = PowerStatus::Get()->IsBatteryFull();
123   usb_charger_was_connected_ = PowerStatus::Get()->IsUsbChargerConnected();
124   line_power_was_connected_ = PowerStatus::Get()->IsLinePowerConnected();
125 }
126 
MaybeShowUsbChargerNotification()127 bool PowerNotificationController::MaybeShowUsbChargerNotification() {
128   const PowerStatus& status = *PowerStatus::Get();
129 
130   // We show the notification if a USB charger is connected but the battery
131   // isn't full (since some ECs may choose to use a lower power rail when the
132   // battery is full even when a high-power charger is connected).
133   const bool show = status.IsUsbChargerConnected() && !status.IsBatteryFull();
134 
135   // Check if the notification needs to be created.
136   if (show && !usb_charger_was_connected_ && !usb_notification_dismissed_) {
137     bool on_battery = PowerStatus::Get()->IsBatteryPresent();
138     std::unique_ptr<Notification> notification = CreateSystemNotification(
139         message_center::NOTIFICATION_TYPE_SIMPLE, kUsbNotificationId,
140         l10n_util::GetStringUTF16(
141             on_battery ? IDS_ASH_STATUS_TRAY_LOW_POWER_CHARGER_TITLE
142                        : IDS_ASH_STATUS_TRAY_LOW_POWER_ADAPTER_TITLE),
143         on_battery
144             ? ui::SubstituteChromeOSDeviceType(
145                   IDS_ASH_STATUS_TRAY_LOW_POWER_CHARGER_MESSAGE_SHORT)
146             : l10n_util::GetStringFUTF16(
147                   IDS_ASH_STATUS_TRAY_LOW_POWER_ADAPTER_MESSAGE_SHORT,
148                   ui::GetChromeOSDeviceName(),
149                   base::FormatDouble(
150                       PowerStatus::Get()->GetPreferredMinimumPower(), 0)),
151         base::string16(), GURL(),
152         message_center::NotifierId(
153             message_center::NotifierType::SYSTEM_COMPONENT, kNotifierPower),
154         message_center::RichNotificationData(),
155         new UsbNotificationDelegate(this), kNotificationLowPowerChargerIcon,
156         message_center::SystemNotificationWarningLevel::WARNING);
157     notification->set_pinned(on_battery);
158     notification->set_never_timeout(!on_battery);
159     message_center_->AddNotification(std::move(notification));
160     return true;
161   }
162 
163   if (!show && usb_charger_was_connected_ && !battery_was_full_) {
164     // USB charger was unplugged or identified as a different type or battery
165     // reached the full state while the notification was showing.
166     message_center_->RemoveNotification(kUsbNotificationId, false);
167     if (!status.IsLinePowerConnected())
168       usb_notification_dismissed_ = false;
169     return true;
170   }
171 
172   return false;
173 }
174 
MaybeShowDualRoleNotification()175 void PowerNotificationController::MaybeShowDualRoleNotification() {
176   const PowerStatus& status = *PowerStatus::Get();
177   if (!status.HasDualRoleDevices()) {
178     dual_role_notification_.reset();
179     return;
180   }
181 
182   if (!dual_role_notification_)
183     dual_role_notification_.reset(new DualRoleNotification(message_center_));
184   dual_role_notification_->Update();
185 }
186 
UpdateNotificationState()187 bool PowerNotificationController::UpdateNotificationState() {
188   const PowerStatus& status = *PowerStatus::Get();
189   if (!status.IsBatteryPresent() || status.IsBatteryTimeBeingCalculated() ||
190       status.IsMainsChargerConnected()) {
191     notification_state_ = NOTIFICATION_NONE;
192     return false;
193   }
194 
195   return status.IsUsbChargerConnected()
196              ? UpdateNotificationStateForRemainingPercentage()
197              : UpdateNotificationStateForRemainingTime();
198 }
199 
UpdateNotificationStateForRemainingTime()200 bool PowerNotificationController::UpdateNotificationStateForRemainingTime() {
201   const base::Optional<base::TimeDelta> remaining_time =
202       PowerStatus::Get()->GetBatteryTimeToEmpty();
203 
204   // Check that powerd actually provided an estimate. It doesn't if the battery
205   // current is so close to zero that the estimate would be huge.
206   if (!remaining_time) {
207     notification_state_ = NOTIFICATION_NONE;
208     return false;
209   }
210 
211   // The notification includes a rounded minutes value, so round the estimate
212   // received from the power manager to match.
213   const int remaining_minutes =
214       base::ClampRound(*remaining_time / base::TimeDelta::FromMinutes(1));
215 
216   if (remaining_minutes >= kNoWarningMinutes ||
217       PowerStatus::Get()->IsBatteryFull()) {
218     notification_state_ = NOTIFICATION_NONE;
219     return false;
220   }
221 
222   switch (notification_state_) {
223     case NOTIFICATION_NONE:
224       if (remaining_minutes <= kCriticalMinutes) {
225         notification_state_ = NOTIFICATION_CRITICAL;
226         LogBatteryForNoCharger(notification_state_, remaining_minutes);
227         return true;
228       }
229       if (remaining_minutes <= kLowPowerMinutes) {
230         notification_state_ = NOTIFICATION_LOW_POWER;
231         LogBatteryForNoCharger(notification_state_, remaining_minutes);
232         return true;
233       }
234       return false;
235     case NOTIFICATION_LOW_POWER:
236       if (remaining_minutes <= kCriticalMinutes) {
237         notification_state_ = NOTIFICATION_CRITICAL;
238         LogBatteryForNoCharger(notification_state_, remaining_minutes);
239         return true;
240       }
241       return false;
242     case NOTIFICATION_CRITICAL:
243       return false;
244   }
245   NOTREACHED();
246   return false;
247 }
248 
249 bool PowerNotificationController::
UpdateNotificationStateForRemainingPercentage()250     UpdateNotificationStateForRemainingPercentage() {
251   // The notification includes a rounded percentage, so round the value received
252   // from the power manager to match.
253   const int remaining_percentage =
254       PowerStatus::Get()->GetRoundedBatteryPercent();
255 
256   if (remaining_percentage >= kNoWarningPercentage ||
257       PowerStatus::Get()->IsBatteryFull()) {
258     notification_state_ = NOTIFICATION_NONE;
259     return false;
260   }
261 
262   switch (notification_state_) {
263     case NOTIFICATION_NONE:
264       if (remaining_percentage <= kCriticalPercentage) {
265         notification_state_ = NOTIFICATION_CRITICAL;
266         LogBatteryForUsbCharger(notification_state_, remaining_percentage);
267         return true;
268       }
269       if (remaining_percentage <= kLowPowerPercentage) {
270         notification_state_ = NOTIFICATION_LOW_POWER;
271         LogBatteryForUsbCharger(notification_state_, remaining_percentage);
272         return true;
273       }
274       return false;
275     case NOTIFICATION_LOW_POWER:
276       if (remaining_percentage <= kCriticalPercentage) {
277         notification_state_ = NOTIFICATION_CRITICAL;
278         LogBatteryForUsbCharger(notification_state_, remaining_percentage);
279         return true;
280       }
281       return false;
282     case NOTIFICATION_CRITICAL:
283       return false;
284   }
285   NOTREACHED();
286   return false;
287 }
288 
NotifyUsbNotificationClosedByUser()289 void PowerNotificationController::NotifyUsbNotificationClosedByUser() {
290   usb_notification_dismissed_ = true;
291 }
292 
293 }  // namespace ash
294