/* * Copyright 2014-2016 Canonical Ltd. * Copyright 2021-2023 Robert Tari * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3, as published * by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranties of * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR * PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . * * Authors: * Charles Kerr * Robert Tari */ #include "datafiles.h" #ifdef LOMIRI_FEATURES_ENABLED #include "dbus-accounts-sound.h" #endif #ifndef LOMIRI_SOUNDSDIR #define LOMIRI_SOUNDSDIR "" #endif #include "dbus-battery.h" #include "dbus-shared.h" #include "notifier.h" #include "utils.h" #include #include #include /* UINT32_MAX */ typedef enum { POWER_LEVEL_CRITICAL, POWER_LEVEL_VERY_LOW, POWER_LEVEL_LOW, POWER_LEVEL_OK } PowerLevel; /** *** GObject Properties **/ enum { PROP_0, PROP_BATTERY, LAST_PROP }; #define PROP_BATTERY_NAME "battery" static GParamSpec * properties[LAST_PROP]; static int instance_count = 0; /** *** **/ typedef struct { /* The battery we're currently watching. This may be a physical battery or it may be an aggregated battery from multiple batteries present on the device. See indicator_power_service_choose_primary_device() and bug #880881 */ IndicatorPowerDevice * battery; PowerLevel power_level; gboolean discharging; NotifyNotification * notify_notification; GDBusConnection * bus; DbusBattery * dbus_battery; /* org.ayatana.indicator.power.Battery skeleton */ gboolean caps_queried; gboolean actions_supported; #ifdef LOMIRI_FEATURES_ENABLED gboolean lomiri_snap_decisions_supported; #endif GCancellable * cancellable; #ifdef LOMIRI_FEATURES_ENABLED DbusAccountsServiceSound * accounts_service_sound_proxy; gboolean accounts_service_sound_proxy_pending; #endif } IndicatorPowerNotifierPrivate; typedef IndicatorPowerNotifierPrivate priv_t; G_DEFINE_TYPE_WITH_PRIVATE(IndicatorPowerNotifier, indicator_power_notifier, G_TYPE_OBJECT) #define get_priv(o) ((priv_t*)indicator_power_notifier_get_instance_private(o)) /*** **** ***/ static const char * power_level_to_dbus_string (const PowerLevel power_level) { switch (power_level) { case POWER_LEVEL_LOW: return POWER_LEVEL_STR_LOW; case POWER_LEVEL_VERY_LOW: return POWER_LEVEL_STR_VERY_LOW; case POWER_LEVEL_CRITICAL: return POWER_LEVEL_STR_CRITICAL; default: return POWER_LEVEL_STR_OK; } } static PowerLevel get_battery_power_level (IndicatorPowerDevice * battery) { static const double percent_critical = 2.0; static const double percent_very_low = 5.0; static const double percent_low = 10.0; gdouble p; PowerLevel ret; g_return_val_if_fail(battery != NULL, POWER_LEVEL_OK); g_return_val_if_fail(indicator_power_device_get_kind(battery) == UP_DEVICE_KIND_BATTERY, POWER_LEVEL_OK); p = indicator_power_device_get_percentage(battery); if (p <= percent_critical) ret = POWER_LEVEL_CRITICAL; else if (p <= percent_very_low) ret = POWER_LEVEL_VERY_LOW; else if (p <= percent_low) ret = POWER_LEVEL_LOW; else ret = POWER_LEVEL_OK; return ret; } static gdouble get_battery_power_percentage (IndicatorPowerDevice * battery) { gdouble ret; g_return_val_if_fail(battery != NULL, 0); g_return_val_if_fail(indicator_power_device_get_kind(battery) == UP_DEVICE_KIND_BATTERY, 0); ret = indicator_power_device_get_percentage(battery); return ret; } /*** **** Sounds ***/ #ifdef LOMIRI_FEATURES_ENABLED static void on_sound_proxy_ready (GObject * source_object G_GNUC_UNUSED, GAsyncResult * res, gpointer gself) { GError * error; error = NULL; DbusAccountsServiceSound * proxy; proxy = dbus_accounts_service_sound_proxy_new_for_bus_finish (res, &error); if (error != NULL) { if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { get_priv(gself)->accounts_service_sound_proxy_pending = FALSE; g_debug("%s Couldn't find accounts service sound proxy: %s", G_STRLOC, error->message); } g_clear_error(&error); } else { IndicatorPowerNotifier * const self = INDICATOR_POWER_NOTIFIER(gself); priv_t * const p = get_priv (self); g_clear_object (&p->accounts_service_sound_proxy); p->accounts_service_sound_proxy = proxy; p->accounts_service_sound_proxy_pending = FALSE; } } static gboolean silent_mode (IndicatorPowerNotifier * self) { priv_t * const p = get_priv (self); /* if we don't have a proxy yet, assume we're in silent mode as a "do no harm" level of response */ if (p->accounts_service_sound_proxy_pending) return TRUE; return (p->accounts_service_sound_proxy != NULL) && dbus_accounts_service_sound_get_silent_mode(p->accounts_service_sound_proxy); } #endif /*** **** Notifications ***/ static void on_notify_notification_finalized (gpointer gself, GObject * dead) { IndicatorPowerNotifier * const self = INDICATOR_POWER_NOTIFIER(gself); priv_t * const p = get_priv(self); g_return_if_fail ((void*)(p->notify_notification) == (void*)dead); p->notify_notification = NULL; dbus_battery_set_is_warning (p->dbus_battery, FALSE); } static void notification_clear (IndicatorPowerNotifier * self) { priv_t * const p = get_priv(self); NotifyNotification * nn; if ((nn = p->notify_notification)) { GError * error = NULL; g_object_weak_unref(G_OBJECT(nn), on_notify_notification_finalized, self); if (!notify_notification_close(nn, &error)) { g_warning("Unable to close notification: %s", error->message); g_error_free(error); } p->notify_notification = NULL; dbus_battery_set_is_warning (p->dbus_battery, FALSE); } } static void on_battery_settings_clicked(NotifyNotification * nn G_GNUC_UNUSED, char * action G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { utils_handle_settings_request(); } static void on_dismiss_clicked(NotifyNotification * nn G_GNUC_UNUSED, char * action G_GNUC_UNUSED, gpointer user_data G_GNUC_UNUSED) { /* no-op; libnotify warns if we have a NULL action callback */ } static void ensure_caps_queried(IndicatorPowerNotifier * self) { priv_t * const p = get_priv(self); if (!p->caps_queried) { gboolean actions_supported; GList * caps; GList * l; #ifdef LOMIRI_FEATURES_ENABLED gboolean lomiri_snap_decisions_supported = FALSE; #endif /* see if actions and snap decisions are supported */ actions_supported = FALSE; caps = notify_get_server_caps(); for (l=caps; l!=NULL; l=l->next) { if (!g_strcmp0(l->data, "actions")) actions_supported = TRUE; #ifdef LOMIRI_FEATURES_ENABLED else if (!g_strcmp0(l->data, "x-lomiri-snap-decisions")) lomiri_snap_decisions_supported = TRUE; #endif } p->actions_supported = actions_supported; #ifdef LOMIRI_FEATURES_ENABLED p->lomiri_snap_decisions_supported = lomiri_snap_decisions_supported; #endif p->caps_queried = TRUE; g_list_free_full(caps, g_free); } } static gboolean are_actions_supported(IndicatorPowerNotifier * self) { priv_t * const p = get_priv(self); ensure_caps_queried(self); return p->actions_supported; } #ifdef LOMIRI_FEATURES_ENABLED static gboolean are_lomiri_snap_decisions_supported(IndicatorPowerNotifier * self) { priv_t * const p = get_priv(self); ensure_caps_queried(self); return p->lomiri_snap_decisions_supported; } #endif static void notification_show(IndicatorPowerNotifier * self) { priv_t * const p = get_priv(self); gdouble pct; const char * title; char * body; GStrv icon_names; const char * icon_name; NotifyNotification * nn; GError * error; const PowerLevel power_level = get_battery_power_level(p->battery); notification_clear(self); g_return_if_fail(power_level != POWER_LEVEL_OK); /* create the notification */ title = power_level == POWER_LEVEL_LOW ? _("Battery Low") : _("Battery Critical"); pct = indicator_power_device_get_percentage(p->battery); body = g_strdup_printf(_("%.0f%% charge remaining"), pct); icon_names = indicator_power_device_get_icon_names(p->battery, FALSE); if (icon_names && *icon_names) icon_name = icon_names[0]; else icon_name = NULL; nn = notify_notification_new(title, body, icon_name); g_strfreev (icon_names); if (are_actions_supported(self)) { #ifdef LOMIRI_FEATURES_ENABLED if (!silent_mode(self)) #endif { gchar* filename = datafile_find(DATAFILE_TYPE_SOUND, LOW_BATTERY_SOUND); if (filename != NULL) { gchar * uri = g_filename_to_uri(filename, NULL, NULL); notify_notification_set_hint(nn, "sound-file", g_variant_new_take_string(uri)); g_clear_pointer(&filename, g_free); } else { g_message("Unable to find '%s' in XDG data dirs, falling back to %s/notifications/", LOMIRI_SOUNDSDIR, LOW_BATTERY_SOUND); notify_notification_set_hint(nn, "sound-file", g_variant_new_string("file://" LOMIRI_SOUNDSDIR "/notifications/" LOW_BATTERY_SOUND)); } } #ifdef LOMIRI_FEATURES_ENABLED if (are_lomiri_snap_decisions_supported(self)) { /* Yes, all supposedly boolean values take strings... */ notify_notification_set_hint(nn, "x-lomiri-snap-decisions", g_variant_new_string("true")); notify_notification_set_hint(nn, "x-lomiri-non-shaped-icon", g_variant_new_string("true")); notify_notification_set_hint(nn, "x-lomiri-private-affirmative-tint", g_variant_new_string("true")); notify_notification_set_hint(nn, "x-lomiri-snap-decisions-timeout", g_variant_new_int32(INT32_MAX)); } #endif notify_notification_set_timeout(nn, NOTIFY_EXPIRES_NEVER); notify_notification_add_action(nn, "dismiss", _("OK"), on_dismiss_clicked, NULL, NULL); notify_notification_add_action(nn, "settings", _("Battery settings"), on_battery_settings_clicked, NULL, NULL); } /* if we can show it, keep it */ error = NULL; if (notify_notification_show(nn, &error)) { p->notify_notification = nn; g_signal_connect(nn, "closed", G_CALLBACK(g_object_unref), NULL); g_object_weak_ref(G_OBJECT(nn), on_notify_notification_finalized, self); dbus_battery_set_is_warning (p->dbus_battery, TRUE); } else { g_critical("Unable to show snap decision for '%s': %s", body, error->message); g_error_free(error); g_object_unref(nn); } g_free (body); } /*** **** ***/ static void on_battery_property_changed (IndicatorPowerNotifier * self) { priv_t * p; PowerLevel old_power_level; PowerLevel new_power_level; gboolean old_discharging; gboolean new_discharging; gdouble new_percentage; g_return_if_fail(INDICATOR_IS_POWER_NOTIFIER(self)); p = get_priv (self); g_return_if_fail(INDICATOR_IS_POWER_DEVICE(p->battery)); old_power_level = p->power_level; new_power_level = get_battery_power_level (p->battery); old_discharging = p->discharging; new_discharging = indicator_power_device_get_state(p->battery) == UP_DEVICE_STATE_DISCHARGING; new_percentage = get_battery_power_percentage (p->battery); /* pop up a 'low battery' notification if either: a) it's already discharging, and its PowerLevel worsens, OR b) it's already got a bad PowerLevel and its state becomes 'discharging */ if ((new_discharging && (old_power_level > new_power_level)) || ((new_power_level != POWER_LEVEL_OK) && new_discharging && !old_discharging)) { notification_show (self); } else if (!new_discharging || (new_power_level == POWER_LEVEL_OK)) { notification_clear (self); } dbus_battery_set_power_level (p->dbus_battery, power_level_to_dbus_string (new_power_level)); dbus_battery_set_power_percentage (p->dbus_battery, new_percentage); dbus_battery_set_is_discharging (p->dbus_battery, new_discharging); p->power_level = new_power_level; p->discharging = new_discharging; } /*** **** GObject virtual functions ***/ static void my_get_property (GObject * o, guint property_id, GValue * value, GParamSpec * pspec) { IndicatorPowerNotifier * const self = INDICATOR_POWER_NOTIFIER (o); priv_t * const p = get_priv (self); switch (property_id) { case PROP_BATTERY: g_value_set_object (value, p->battery); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (o, property_id, pspec); } } static void my_set_property (GObject * o, guint property_id, const GValue * value, GParamSpec * pspec) { IndicatorPowerNotifier * const self = INDICATOR_POWER_NOTIFIER (o); switch (property_id) { case PROP_BATTERY: indicator_power_notifier_set_battery (self, g_value_get_object(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (o, property_id, pspec); } } static void my_dispose (GObject * o) { IndicatorPowerNotifier * const self = INDICATOR_POWER_NOTIFIER(o); priv_t * const p = get_priv (self); if (p->cancellable != NULL) { g_cancellable_cancel(p->cancellable); g_clear_object(&p->cancellable); } indicator_power_notifier_set_bus (self, NULL); notification_clear (self); indicator_power_notifier_set_battery (self, NULL); g_clear_object (&p->dbus_battery); #ifdef LOMIRI_FEATURES_ENABLED g_clear_object (&p->accounts_service_sound_proxy); #endif G_OBJECT_CLASS (indicator_power_notifier_parent_class)->dispose (o); } static void my_finalize (GObject * o G_GNUC_UNUSED) { /* FIXME: This is an awkward place to put this. Ordinarily something like this would go in main(), but we need libnotify to clean itself up before shutting down the bus in the unit tests as well. */ if (!--instance_count) notify_uninit(); } /*** **** Instantiation ***/ static void indicator_power_notifier_init (IndicatorPowerNotifier * self) { priv_t * const p = get_priv (self); /* bind the read-only properties so they'll get pushed to the bus */ p->dbus_battery = dbus_battery_skeleton_new (); p->power_level = POWER_LEVEL_OK; p->cancellable = g_cancellable_new(); if (!instance_count++ && !notify_init(SERVICE_EXEC)) g_critical("Unable to initialize libnotify! Notifications might not be shown."); #ifdef LOMIRI_FEATURES_ENABLED p->accounts_service_sound_proxy_pending = TRUE; gchar* object_path = g_strdup_printf("/org/freedesktop/Accounts/User%lu", (gulong)getuid()); dbus_accounts_service_sound_proxy_new_for_bus( G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES, "org.freedesktop.Accounts", object_path, p->cancellable, on_sound_proxy_ready, self); g_clear_pointer(&object_path, g_free); #endif } static void indicator_power_notifier_class_init (IndicatorPowerNotifierClass * klass) { GObjectClass * object_class = G_OBJECT_CLASS (klass); object_class->dispose = my_dispose; object_class->finalize = my_finalize; object_class->get_property = my_get_property; object_class->set_property = my_set_property; properties[PROP_BATTERY] = g_param_spec_object ( PROP_BATTERY_NAME, "Battery", "The current battery", G_TYPE_OBJECT, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, LAST_PROP, properties); } /*** **** Public API ***/ IndicatorPowerNotifier * indicator_power_notifier_new (void) { GObject * o = g_object_new (INDICATOR_TYPE_POWER_NOTIFIER, NULL); return INDICATOR_POWER_NOTIFIER (o); } void indicator_power_notifier_set_battery (IndicatorPowerNotifier * self, IndicatorPowerDevice * battery) { priv_t * p; g_return_if_fail(INDICATOR_IS_POWER_NOTIFIER(self)); g_return_if_fail((battery == NULL) || INDICATOR_IS_POWER_DEVICE(battery)); g_return_if_fail((battery == NULL) || (indicator_power_device_get_kind(battery) == UP_DEVICE_KIND_BATTERY)); p = get_priv (self); if (p->battery == battery) return; if (p->battery != NULL) { g_signal_handlers_disconnect_by_data (p->battery, self); g_clear_object (&p->battery); dbus_battery_set_power_level (p->dbus_battery, power_level_to_dbus_string (POWER_LEVEL_OK)); notification_clear (self); } if (battery != NULL) { p->battery = g_object_ref (battery); g_signal_connect_swapped (p->battery, "notify::"INDICATOR_POWER_DEVICE_PERCENTAGE, G_CALLBACK(on_battery_property_changed), self); g_signal_connect_swapped (p->battery, "notify::"INDICATOR_POWER_DEVICE_STATE, G_CALLBACK(on_battery_property_changed), self); on_battery_property_changed (self); } } void indicator_power_notifier_set_bus (IndicatorPowerNotifier * self, GDBusConnection * bus) { priv_t * p; GDBusInterfaceSkeleton * skel; g_return_if_fail(INDICATOR_IS_POWER_NOTIFIER(self)); g_return_if_fail((bus == NULL) || G_IS_DBUS_CONNECTION(bus)); p = get_priv (self); if (p->bus == bus) return; skel = G_DBUS_INTERFACE_SKELETON(p->dbus_battery); if (p->bus != NULL) { if (skel != NULL) g_dbus_interface_skeleton_unexport (skel); g_clear_object (&p->bus); } if (bus != NULL) { GError * error; p->bus = g_object_ref (bus); error = NULL; if (!g_dbus_interface_skeleton_export(skel, bus, BUS_PATH"/Battery", &error)) { g_warning ("Unable to export LowBattery properties: %s", error->message); g_error_free (error); } } } const char * indicator_power_notifier_get_power_level (IndicatorPowerDevice * battery) { return power_level_to_dbus_string (get_battery_power_level (battery)); }