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