1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 #include "Hal.h"
7 #include "HalLog.h"
8 #include <dbus/dbus-glib.h>
9 #include <dbus/dbus-glib-lowlevel.h>
10 #include <mozilla/Attributes.h>
11 #include <mozilla/dom/battery/Constants.h>
12 #include "nsAutoRef.h"
13 #include <cmath>
14
15 /*
16 * Helper that manages the destruction of glib objects as soon as they leave
17 * the current scope.
18 *
19 * We are specializing nsAutoRef class.
20 */
21
22 template <>
23 class nsAutoRefTraits<GHashTable> : public nsPointerRefTraits<GHashTable> {
24 public:
Release(GHashTable * ptr)25 static void Release(GHashTable* ptr) { g_hash_table_unref(ptr); }
26 };
27
28 using namespace mozilla::dom::battery;
29
30 namespace mozilla {
31 namespace hal_impl {
32
33 /**
34 * This is the declaration of UPowerClient class. This class is listening and
35 * communicating to upower daemon through DBus.
36 * There is no header file because this class shouldn't be public.
37 */
38 class UPowerClient {
39 public:
40 static UPowerClient* GetInstance();
41
42 void BeginListening();
43 void StopListening();
44
45 double GetLevel();
46 bool IsCharging();
47 double GetRemainingTime();
48
49 ~UPowerClient();
50
51 private:
52 UPowerClient();
53
54 enum States {
55 eState_Unknown = 0,
56 eState_Charging,
57 eState_Discharging,
58 eState_Empty,
59 eState_FullyCharged,
60 eState_PendingCharge,
61 eState_PendingDischarge
62 };
63
64 /**
65 * Update the currently tracked device.
66 * @return whether everything went ok.
67 */
68 void UpdateTrackedDeviceSync();
69
70 /**
71 * Returns a hash table with the properties of aDevice.
72 * Note: the caller has to unref the hash table.
73 */
74 GHashTable* GetDevicePropertiesSync(DBusGProxy* aProxy);
75 void GetDevicePropertiesAsync(DBusGProxy* aProxy);
76 static void GetDevicePropertiesCallback(DBusGProxy* aProxy,
77 DBusGProxyCall* aCall, void* aData);
78
79 /**
80 * Using the device properties (aHashTable), this method updates the member
81 * variable storing the values we care about.
82 */
83 void UpdateSavedInfo(GHashTable* aHashTable);
84
85 /**
86 * Callback used by 'DeviceChanged' signal.
87 */
88 static void DeviceChanged(DBusGProxy* aProxy, const gchar* aObjectPath,
89 UPowerClient* aListener);
90
91 /**
92 * Callback used by 'PropertiesChanged' signal.
93 * This method is called when the the battery level changes.
94 * (Only with upower >= 0.99)
95 */
96 static void PropertiesChanged(DBusGProxy* aProxy, const gchar*, GHashTable*,
97 char**, UPowerClient* aListener);
98
99 /**
100 * Callback called when mDBusConnection gets a signal.
101 */
102 static DBusHandlerResult ConnectionSignalFilter(DBusConnection* aConnection,
103 DBusMessage* aMessage,
104 void* aData);
105
106 // The DBus connection object.
107 DBusGConnection* mDBusConnection;
108
109 // The DBus proxy object to upower.
110 DBusGProxy* mUPowerProxy;
111
112 // The path of the tracked device.
113 gchar* mTrackedDevice;
114
115 // The DBusGProxy for the tracked device.
116 DBusGProxy* mTrackedDeviceProxy;
117
118 double mLevel;
119 bool mCharging;
120 double mRemainingTime;
121
122 static UPowerClient* sInstance;
123
124 static const guint sDeviceTypeBattery = 2;
125 static const guint64 kUPowerUnknownRemainingTime = 0;
126 };
127
128 /*
129 * Implementation of mozilla::hal_impl::EnableBatteryNotifications,
130 * mozilla::hal_impl::DisableBatteryNotifications,
131 * and mozilla::hal_impl::GetCurrentBatteryInformation.
132 */
133
EnableBatteryNotifications()134 void EnableBatteryNotifications() {
135 UPowerClient::GetInstance()->BeginListening();
136 }
137
DisableBatteryNotifications()138 void DisableBatteryNotifications() {
139 UPowerClient::GetInstance()->StopListening();
140 }
141
GetCurrentBatteryInformation(hal::BatteryInformation * aBatteryInfo)142 void GetCurrentBatteryInformation(hal::BatteryInformation* aBatteryInfo) {
143 UPowerClient* upowerClient = UPowerClient::GetInstance();
144
145 aBatteryInfo->level() = upowerClient->GetLevel();
146 aBatteryInfo->charging() = upowerClient->IsCharging();
147 aBatteryInfo->remainingTime() = upowerClient->GetRemainingTime();
148 }
149
150 /*
151 * Following is the implementation of UPowerClient.
152 */
153
154 UPowerClient* UPowerClient::sInstance = nullptr;
155
156 /* static */
GetInstance()157 UPowerClient* UPowerClient::GetInstance() {
158 if (!sInstance) {
159 sInstance = new UPowerClient();
160 }
161
162 return sInstance;
163 }
164
UPowerClient()165 UPowerClient::UPowerClient()
166 : mDBusConnection(nullptr),
167 mUPowerProxy(nullptr),
168 mTrackedDevice(nullptr),
169 mTrackedDeviceProxy(nullptr),
170 mLevel(kDefaultLevel),
171 mCharging(kDefaultCharging),
172 mRemainingTime(kDefaultRemainingTime) {}
173
~UPowerClient()174 UPowerClient::~UPowerClient() {
175 NS_ASSERTION(!mDBusConnection && !mUPowerProxy && !mTrackedDevice &&
176 !mTrackedDeviceProxy,
177 "The observers have not been correctly removed! "
178 "(StopListening should have been called)");
179 }
180
BeginListening()181 void UPowerClient::BeginListening() {
182 GError* error = nullptr;
183 mDBusConnection = dbus_g_bus_get(DBUS_BUS_SYSTEM, &error);
184
185 if (!mDBusConnection) {
186 HAL_LOG("Failed to open connection to bus: %s\n", error->message);
187 g_error_free(error);
188 return;
189 }
190
191 DBusConnection* dbusConnection =
192 dbus_g_connection_get_connection(mDBusConnection);
193
194 // Make sure we do not exit the entire program if DBus connection get lost.
195 dbus_connection_set_exit_on_disconnect(dbusConnection, false);
196
197 // Listening to signals the DBus connection is going to get so we will know
198 // when it is lost and we will be able to disconnect cleanly.
199 dbus_connection_add_filter(dbusConnection, ConnectionSignalFilter, this,
200 nullptr);
201
202 mUPowerProxy = dbus_g_proxy_new_for_name(
203 mDBusConnection, "org.freedesktop.UPower", "/org/freedesktop/UPower",
204 "org.freedesktop.UPower");
205
206 UpdateTrackedDeviceSync();
207
208 /*
209 * TODO: we should probably listen to DeviceAdded and DeviceRemoved signals.
210 * If we do that, we would have to disconnect from those in StopListening.
211 * It's not yet implemented because it requires testing hot plugging and
212 * removal of a battery.
213 */
214 dbus_g_proxy_add_signal(mUPowerProxy, "DeviceChanged", G_TYPE_STRING,
215 G_TYPE_INVALID);
216 dbus_g_proxy_connect_signal(mUPowerProxy, "DeviceChanged",
217 G_CALLBACK(DeviceChanged), this, nullptr);
218 }
219
StopListening()220 void UPowerClient::StopListening() {
221 // If mDBusConnection isn't initialized, that means we are not really
222 // listening.
223 if (!mDBusConnection) {
224 return;
225 }
226
227 dbus_connection_remove_filter(
228 dbus_g_connection_get_connection(mDBusConnection), ConnectionSignalFilter,
229 this);
230
231 dbus_g_proxy_disconnect_signal(mUPowerProxy, "DeviceChanged",
232 G_CALLBACK(DeviceChanged), this);
233
234 g_free(mTrackedDevice);
235 mTrackedDevice = nullptr;
236
237 if (mTrackedDeviceProxy) {
238 dbus_g_proxy_disconnect_signal(mTrackedDeviceProxy, "PropertiesChanged",
239 G_CALLBACK(PropertiesChanged), this);
240
241 g_object_unref(mTrackedDeviceProxy);
242 mTrackedDeviceProxy = nullptr;
243 }
244
245 g_object_unref(mUPowerProxy);
246 mUPowerProxy = nullptr;
247
248 dbus_g_connection_unref(mDBusConnection);
249 mDBusConnection = nullptr;
250
251 // We should now show the default values, not the latest we got.
252 mLevel = kDefaultLevel;
253 mCharging = kDefaultCharging;
254 mRemainingTime = kDefaultRemainingTime;
255 }
256
UpdateTrackedDeviceSync()257 void UPowerClient::UpdateTrackedDeviceSync() {
258 GType typeGPtrArray =
259 dbus_g_type_get_collection("GPtrArray", DBUS_TYPE_G_OBJECT_PATH);
260 GPtrArray* devices = nullptr;
261 GError* error = nullptr;
262
263 // Reset the current tracked device:
264 g_free(mTrackedDevice);
265 mTrackedDevice = nullptr;
266
267 // Reset the current tracked device proxy:
268 if (mTrackedDeviceProxy) {
269 dbus_g_proxy_disconnect_signal(mTrackedDeviceProxy, "PropertiesChanged",
270 G_CALLBACK(PropertiesChanged), this);
271
272 g_object_unref(mTrackedDeviceProxy);
273 mTrackedDeviceProxy = nullptr;
274 }
275
276 // If that fails, that likely means upower isn't installed.
277 if (!dbus_g_proxy_call(mUPowerProxy, "EnumerateDevices", &error,
278 G_TYPE_INVALID, typeGPtrArray, &devices,
279 G_TYPE_INVALID)) {
280 HAL_LOG("Error: %s\n", error->message);
281 g_error_free(error);
282 return;
283 }
284
285 /*
286 * We are looking for the first device that is a battery.
287 * TODO: we could try to combine more than one battery.
288 */
289 for (guint i = 0; i < devices->len; ++i) {
290 gchar* devicePath = static_cast<gchar*>(g_ptr_array_index(devices, i));
291
292 DBusGProxy* proxy = dbus_g_proxy_new_from_proxy(
293 mUPowerProxy, "org.freedesktop.DBus.Properties", devicePath);
294
295 nsAutoRef<GHashTable> hashTable(GetDevicePropertiesSync(proxy));
296
297 if (g_value_get_uint(static_cast<const GValue*>(
298 g_hash_table_lookup(hashTable, "Type"))) == sDeviceTypeBattery) {
299 UpdateSavedInfo(hashTable);
300 mTrackedDevice = devicePath;
301 mTrackedDeviceProxy = proxy;
302 break;
303 }
304
305 g_object_unref(proxy);
306 g_free(devicePath);
307 }
308
309 if (mTrackedDeviceProxy) {
310 dbus_g_proxy_add_signal(
311 mTrackedDeviceProxy, "PropertiesChanged", G_TYPE_STRING,
312 dbus_g_type_get_map("GHashTable", G_TYPE_STRING, G_TYPE_VALUE),
313 G_TYPE_STRV, G_TYPE_INVALID);
314 dbus_g_proxy_connect_signal(mTrackedDeviceProxy, "PropertiesChanged",
315 G_CALLBACK(PropertiesChanged), this, nullptr);
316 }
317
318 g_ptr_array_free(devices, true);
319 }
320
321 /* static */
DeviceChanged(DBusGProxy * aProxy,const gchar * aObjectPath,UPowerClient * aListener)322 void UPowerClient::DeviceChanged(DBusGProxy* aProxy, const gchar* aObjectPath,
323 UPowerClient* aListener) {
324 if (!aListener->mTrackedDevice) {
325 return;
326 }
327
328 #if GLIB_MAJOR_VERSION >= 2 && GLIB_MINOR_VERSION >= 16
329 if (g_strcmp0(aObjectPath, aListener->mTrackedDevice)) {
330 #else
331 if (g_ascii_strcasecmp(aObjectPath, aListener->mTrackedDevice)) {
332 #endif
333 return;
334 }
335
336 aListener->GetDevicePropertiesAsync(aListener->mTrackedDeviceProxy);
337 }
338
339 /* static */
340 void UPowerClient::PropertiesChanged(DBusGProxy* aProxy, const gchar*,
341 GHashTable*, char**,
342 UPowerClient* aListener) {
343 aListener->GetDevicePropertiesAsync(aListener->mTrackedDeviceProxy);
344 }
345
346 /* static */
347 DBusHandlerResult UPowerClient::ConnectionSignalFilter(
348 DBusConnection* aConnection, DBusMessage* aMessage, void* aData) {
349 if (dbus_message_is_signal(aMessage, DBUS_INTERFACE_LOCAL, "Disconnected")) {
350 static_cast<UPowerClient*>(aData)->StopListening();
351 // We do not return DBUS_HANDLER_RESULT_HANDLED here because the connection
352 // might be shared and some other filters might want to do something.
353 }
354
355 return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
356 }
357
358 GHashTable* UPowerClient::GetDevicePropertiesSync(DBusGProxy* aProxy) {
359 GError* error = nullptr;
360 GHashTable* hashTable = nullptr;
361 GType typeGHashTable =
362 dbus_g_type_get_map("GHashTable", G_TYPE_STRING, G_TYPE_VALUE);
363 if (!dbus_g_proxy_call(aProxy, "GetAll", &error, G_TYPE_STRING,
364 "org.freedesktop.UPower.Device", G_TYPE_INVALID,
365 typeGHashTable, &hashTable, G_TYPE_INVALID)) {
366 HAL_LOG("Error: %s\n", error->message);
367 g_error_free(error);
368 return nullptr;
369 }
370
371 return hashTable;
372 }
373
374 /* static */
375 void UPowerClient::GetDevicePropertiesCallback(DBusGProxy* aProxy,
376 DBusGProxyCall* aCall,
377 void* aData) {
378 GError* error = nullptr;
379 GHashTable* hashTable = nullptr;
380 GType typeGHashTable =
381 dbus_g_type_get_map("GHashTable", G_TYPE_STRING, G_TYPE_VALUE);
382 if (!dbus_g_proxy_end_call(aProxy, aCall, &error, typeGHashTable, &hashTable,
383 G_TYPE_INVALID)) {
384 HAL_LOG("Error: %s\n", error->message);
385 g_error_free(error);
386 } else {
387 sInstance->UpdateSavedInfo(hashTable);
388 hal::NotifyBatteryChange(hal::BatteryInformation(
389 sInstance->mLevel, sInstance->mCharging, sInstance->mRemainingTime));
390 g_hash_table_unref(hashTable);
391 }
392 }
393
394 void UPowerClient::GetDevicePropertiesAsync(DBusGProxy* aProxy) {
395 dbus_g_proxy_begin_call(aProxy, "GetAll", GetDevicePropertiesCallback,
396 nullptr, nullptr, G_TYPE_STRING,
397 "org.freedesktop.UPower.Device", G_TYPE_INVALID);
398 }
399
400 void UPowerClient::UpdateSavedInfo(GHashTable* aHashTable) {
401 bool isFull = false;
402
403 /*
404 * State values are confusing...
405 * First of all, after looking at upower sources (0.9.13), it seems that
406 * PendingDischarge and PendingCharge are not used.
407 * In addition, FullyCharged and Empty states are not clear because we do not
408 * know if the battery is actually charging or not. Those values come directly
409 * from sysfs (in the Linux kernel) which have four states: "Empty", "Full",
410 * "Charging" and "Discharging". In sysfs, "Empty" and "Full" are also only
411 * related to the level, not to the charging state.
412 * In this code, we are going to assume that Full means charging and Empty
413 * means discharging because if that is not the case, the state should not
414 * last a long time (actually, it should disappear at the following update).
415 * It might be even very hard to see real cases where the state is Empty and
416 * the battery is charging or the state is Full and the battery is discharging
417 * given that plugging/unplugging the battery should have an impact on the
418 * level.
419 */
420 switch (g_value_get_uint(
421 static_cast<const GValue*>(g_hash_table_lookup(aHashTable, "State")))) {
422 case eState_Unknown:
423 mCharging = kDefaultCharging;
424 break;
425 case eState_FullyCharged:
426 isFull = true;
427 [[fallthrough]];
428 case eState_Charging:
429 case eState_PendingCharge:
430 mCharging = true;
431 break;
432 case eState_Discharging:
433 case eState_Empty:
434 case eState_PendingDischarge:
435 mCharging = false;
436 break;
437 }
438
439 /*
440 * The battery level might be very close to 100% (like 99%) without
441 * increasing. It seems that upower sets the battery state as 'full' in that
442 * case so we should trust it and not even try to get the value.
443 */
444 if (isFull) {
445 mLevel = 1.0;
446 } else {
447 mLevel = round(g_value_get_double(static_cast<const GValue*>(
448 g_hash_table_lookup(aHashTable, "Percentage")))) *
449 0.01;
450 }
451
452 if (isFull) {
453 mRemainingTime = 0;
454 } else {
455 mRemainingTime = mCharging
456 ? g_value_get_int64(static_cast<const GValue*>(
457 g_hash_table_lookup(aHashTable, "TimeToFull")))
458 : g_value_get_int64(static_cast<const GValue*>(
459 g_hash_table_lookup(aHashTable, "TimeToEmpty")));
460
461 if (mRemainingTime == kUPowerUnknownRemainingTime) {
462 mRemainingTime = kUnknownRemainingTime;
463 }
464 }
465 }
466
467 double UPowerClient::GetLevel() { return mLevel; }
468
469 bool UPowerClient::IsCharging() { return mCharging; }
470
471 double UPowerClient::GetRemainingTime() { return mRemainingTime; }
472
473 } // namespace hal_impl
474 } // namespace mozilla
475