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