aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCharles Kerr <charles.kerr@canonical.com>2014-07-30 10:52:30 +0000
committerCI bot <ps-jenkins@lists.canonical.com>2014-07-30 10:52:30 +0000
commitb4f91b1d69a66c2d3e97feeed357243ddfda451f (patch)
tree90e88aefae6fc2eeb49ad4afed75b1f8d2788e6a
parent967a5479db7d280255f669102f045076e09420d8 (diff)
parentb46ec7d0cc35d10197a1a91f473e0a655188f6eb (diff)
downloadayatana-indicator-power-b4f91b1d69a66c2d3e97feeed357243ddfda451f.tar.gz
ayatana-indicator-power-b4f91b1d69a66c2d3e97feeed357243ddfda451f.tar.bz2
ayatana-indicator-power-b4f91b1d69a66c2d3e97feeed357243ddfda451f.zip
Add low-battery notifications. Fixes: 1317858, 1317860
Approved by: PS Jenkins bot, Lars Uebernickel
-rw-r--r--CMakeLists.txt4
-rw-r--r--data/com.canonical.indicator.power.Battery.xml23
-rw-r--r--debian/control14
-rw-r--r--src/CMakeLists.txt5
-rw-r--r--src/dbus-shared.h28
-rw-r--r--src/device.h2
-rw-r--r--src/main.c8
-rw-r--r--src/notifier.c445
-rw-r--r--src/notifier.h74
-rw-r--r--src/service.c21
-rw-r--r--tests/CMakeLists.txt17
-rw-r--r--tests/device-provider-mock.c107
-rw-r--r--tests/device-provider-mock.h79
-rw-r--r--tests/glib-fixture.h141
-rw-r--r--tests/indicator-power-service-cmdline-battery.cc124
-rw-r--r--tests/manual17
-rw-r--r--tests/test-notify.cc410
17 files changed, 1505 insertions, 14 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 130a662..569100d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -37,6 +37,7 @@ pkg_check_modules(SERVICE_DEPS REQUIRED
gio-2.0>=2.36
gio-unix-2.0>=2.36
gudev-1.0>=204
+ libnotify>=0.7.6
url-dispatcher-1>=1)
include_directories (SYSTEM ${SERVICE_DEPS_INCLUDE_DIRS})
@@ -62,7 +63,8 @@ add_custom_target (cppcheck COMMAND cppcheck --enable=all -q --error-exitcode=2
##
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
- set(C_WARNING_ARGS "${C_WARNING_ARGS} -Weverything -Wno-c++98-compat")
+ set(C_WARNING_ARGS "${C_WARNING_ARGS} -Weverything")
+ set(C_WARNING_ARGS "${C_WARNING_ARGS} -Wno-c++98-compat -Wno-padded") # these are annoying
set(C_WARNING_ARGS "${C_WARNING_ARGS} -Wno-documentation") # gtk-doc != doxygen
else()
set(C_WARNING_ARGS "${C_WARNING_ARGS} -Wall -Wextra -Wpedantic -Wformat=2")
diff --git a/data/com.canonical.indicator.power.Battery.xml b/data/com.canonical.indicator.power.Battery.xml
new file mode 100644
index 0000000..eca4524
--- /dev/null
+++ b/data/com.canonical.indicator.power.Battery.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node xmlns:doc="http://www.freedesktop.org/dbus/1.0/doc.dtd">
+ <interface name="com.canonical.indicator.power.Battery">
+
+ <property name="PowerLevel" type="s" access="read">
+ <doc:doc>
+ <doc:description>
+ <doc:para>The battery's power level. Possible values: 'ok', 'low', 'very_low', 'critical'</doc:para>
+ </doc:description>
+ </doc:doc>
+ </property>
+
+ <property name="IsWarning" type="b" access="read">
+ <doc:doc>
+ <doc:description>
+ <doc:para>Whether or not indicator-power-service is warning the user about low battery power.</doc:para>
+ </doc:description>
+ </doc:doc>
+ </property>
+
+ </interface>
+</node>
diff --git a/debian/control b/debian/control
index 0944f42..00c0864 100644
--- a/debian/control
+++ b/debian/control
@@ -3,14 +3,20 @@ Section: gnome
Priority: optional
Maintainer: Ubuntu Core Developers <ubuntu-devel-discuss@lists.ubuntu.com>
Build-Depends: cmake,
- debhelper (>= 9),
- dh-translations,
- intltool,
- libgtest-dev,
+ libnotify-dev (>= 0.7.6),
libglib2.0-dev (>= 2.36),
libgudev-1.0-dev,
liburl-dispatcher1-dev,
python:any,
+# for packaging
+ debhelper (>= 9),
+ dh-translations,
+ intltool,
+# for tests
+ libgtest-dev,
+ python3-dbusmock,
+ dbus-test-runner,
+ libdbustest1-dev,
Standards-Version: 3.9.2
Homepage: https://launchpad.net/indicator-power
# If you aren't a member of ~indicator-applet-developers but need to upload
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a39b945..7a4a297 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -10,6 +10,7 @@ set(SERVICE_MANUAL_SOURCES
ib-brightness-uscreen-control.c
device-provider.c
device.c
+ notifier.c
service.c)
# generated sources
@@ -19,6 +20,10 @@ add_gdbus_codegen_with_namespace(SERVICE_GENERATED_SOURCES dbus-upower
org.freedesktop
Dbus
${CMAKE_CURRENT_SOURCE_DIR}/org.freedesktop.UPower.xml)
+add_gdbus_codegen_with_namespace(SERVICE_GENERATED_SOURCES dbus-battery
+ com.canonical.indicator.power
+ Dbus
+ ${CMAKE_SOURCE_DIR}/data/com.canonical.indicator.power.Battery.xml)
# add the bin dir to our include path so the code can find the generated header files
include_directories(${CMAKE_CURRENT_BINARY_DIR})
diff --git a/src/dbus-shared.h b/src/dbus-shared.h
new file mode 100644
index 0000000..bf54034
--- /dev/null
+++ b/src/dbus-shared.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ * Ted Gould <ted@canonical.com>
+ */
+
+#ifndef DBUS_SHARED_H
+#define DBUS_SHARED_H
+
+#define BUS_NAME "com.canonical.indicator.power"
+#define BUS_PATH "/com/canonical/indicator/power"
+
+#endif /* DBUS_SHARED_H */
+
diff --git a/src/device.h b/src/device.h
index 3a10f89..d867707 100644
--- a/src/device.h
+++ b/src/device.h
@@ -24,7 +24,7 @@ License along with this library. If not, see
#ifndef __INDICATOR_POWER_DEVICE_H__
#define __INDICATOR_POWER_DEVICE_H__
-#include <glib-object.h>
+#include <gio/gio.h> /* GIcon */
G_BEGIN_DECLS
diff --git a/src/main.c b/src/main.c
index 7363284..d7953e6 100644
--- a/src/main.c
+++ b/src/main.c
@@ -41,9 +41,9 @@ on_name_lost (gpointer instance G_GNUC_UNUSED, gpointer loop)
int
main (int argc G_GNUC_UNUSED, char ** argv G_GNUC_UNUSED)
{
- GMainLoop * loop;
- IndicatorPowerService * service;
IndicatorPowerDeviceProvider * device_provider;
+ IndicatorPowerService * service;
+ GMainLoop * loop;
/* boilerplate i18n */
setlocale (LC_ALL, "");
@@ -59,8 +59,8 @@ main (int argc G_GNUC_UNUSED, char ** argv G_GNUC_UNUSED)
g_main_loop_run (loop);
/* cleanup */
- g_clear_object (&device_provider);
- g_clear_object (&service);
g_main_loop_unref (loop);
+ g_clear_object (&service);
+ g_clear_object (&device_provider);
return 0;
}
diff --git a/src/notifier.c b/src/notifier.c
new file mode 100644
index 0000000..81cd6f1
--- /dev/null
+++ b/src/notifier.c
@@ -0,0 +1,445 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+#include "dbus-battery.h"
+#include "dbus-shared.h"
+#include "notifier.h"
+
+#include <libnotify/notify.h>
+
+#include <glib/gi18n.h>
+
+#define HINT_INTERACTIVE "x-canonical-switch-to-application"
+
+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; /* com.canonical.indicator.power.Battery skeleton */
+}
+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;
+ }
+}
+
+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;
+}
+
+/***
+**** 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
+notification_show(IndicatorPowerNotifier * self)
+{
+ priv_t * const p = get_priv(self);
+ gdouble pct;
+ char * body;
+ NotifyNotification * nn;
+ GError * error;
+
+ notification_clear(self);
+
+ /* create the notification */
+ pct = indicator_power_device_get_percentage(p->battery);
+ body = g_strdup_printf(_("%.0f%% charge remaining"), pct);
+ nn = notify_notification_new(_("Battery Low"), body, NULL);
+ g_free (body);
+ /*notify_notification_set_hint(nn, HINT_INTERACTIVE, g_variant_new_boolean(TRUE));*/
+
+ /* 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);
+ }
+}
+
+/***
+****
+***/
+
+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;
+
+ 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;
+
+ /* 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));
+ 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);
+
+ indicator_power_notifier_set_bus (self, NULL);
+ notification_clear (self);
+ indicator_power_notifier_set_battery (self, NULL);
+ g_clear_object (&p->dbus_battery);
+
+ 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;
+
+ if (!instance_count++)
+ {
+ if (!notify_init("indicator-power-service"))
+ {
+ g_critical("Unable to initialize libnotify! Notifications might not be shown.");
+ }
+ }
+}
+
+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));
+}
diff --git a/src/notifier.h b/src/notifier.h
new file mode 100644
index 0000000..18e25d7
--- /dev/null
+++ b/src/notifier.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+#ifndef __INDICATOR_POWER_NOTIFIER_H__
+#define __INDICATOR_POWER_NOTIFIER_H__
+
+#include <gio/gio.h>
+
+#include "device.h"
+
+G_BEGIN_DECLS
+
+/* standard GObject macros */
+#define INDICATOR_POWER_NOTIFIER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), INDICATOR_TYPE_POWER_NOTIFIER, IndicatorPowerNotifier))
+#define INDICATOR_TYPE_POWER_NOTIFIER (indicator_power_notifier_get_type())
+#define INDICATOR_IS_POWER_NOTIFIER(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), INDICATOR_TYPE_POWER_NOTIFIER))
+#define INDICATOR_POWER_NOTIFIER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), INDICATOR_TYPE_POWER_NOTIFIER, IndicatorPowerNotifierClass))
+
+typedef struct _IndicatorPowerNotifier IndicatorPowerNotifier;
+typedef struct _IndicatorPowerNotifierClass IndicatorPowerNotifierClass;
+
+/**
+ * The Indicator Power Notifier.
+ */
+struct _IndicatorPowerNotifier
+{
+ /*< private >*/
+ GObject parent;
+};
+
+struct _IndicatorPowerNotifierClass
+{
+ GObjectClass parent_class;
+};
+
+/***
+****
+***/
+
+GType indicator_power_notifier_get_type (void);
+
+IndicatorPowerNotifier * indicator_power_notifier_new (void);
+
+void indicator_power_notifier_set_bus (IndicatorPowerNotifier * self,
+ GDBusConnection * connection);
+
+void indicator_power_notifier_set_battery (IndicatorPowerNotifier * self,
+ IndicatorPowerDevice * battery);
+
+#define POWER_LEVEL_STR_OK "ok"
+#define POWER_LEVEL_STR_LOW "low"
+#define POWER_LEVEL_STR_VERY_LOW "very_low"
+#define POWER_LEVEL_STR_CRITICAL "critical"
+const char * indicator_power_notifier_get_power_level (IndicatorPowerDevice * battery);
+
+G_END_DECLS
+
+#endif /* __INDICATOR_POWER_NOTIFIER_H__ */
diff --git a/src/service.c b/src/service.c
index 7478d0f..0cd448b 100644
--- a/src/service.c
+++ b/src/service.c
@@ -22,8 +22,10 @@
#include <gio/gio.h>
#include <url-dispatcher.h>
+#include "dbus-shared.h"
#include "device.h"
#include "device-provider.h"
+#include "notifier.h"
#include "ib-brightness-control.h"
#include "ib-brightness-uscreen-control.h"
#include "service.h"
@@ -120,6 +122,7 @@ struct _IndicatorPowerServicePrivate
GList * devices; /* IndicatorPowerDevice */
IndicatorPowerDeviceProvider * device_provider;
+ IndicatorPowerNotifier * notifier;
};
typedef IndicatorPowerServicePrivate priv_t;
@@ -592,14 +595,14 @@ rebuild_now (IndicatorPowerService * self, guint sections)
struct ProfileMenuInfo * desktop = &p->menus[PROFILE_DESKTOP];
struct ProfileMenuInfo * greeter = &p->menus[PROFILE_DESKTOP_GREETER];
- if (p->conn == NULL) /* we haven't built the menus yet */
- return;
-
if (sections & SECTION_HEADER)
{
g_simple_action_set_state (p->header_action, create_header_state (self));
}
+ if (p->conn == NULL) /* we haven't built the menus yet */
+ return;
+
if (sections & SECTION_DEVICES)
{
rebuild_section (desktop->submenu, 0, create_desktop_devices_section (self, PROFILE_DESKTOP));
@@ -821,6 +824,9 @@ on_bus_acquired (GDBusConnection * connection,
p->conn = g_object_ref (G_OBJECT (connection));
+ /* export the battery properties */
+ indicator_power_notifier_set_bus (p->notifier, connection);
+
/* export the actions */
if ((id = g_dbus_connection_export_action_group (connection,
BUS_PATH,
@@ -920,6 +926,12 @@ on_devices_changed (IndicatorPowerService * self)
g_clear_object (&p->primary_device);
p->primary_device = indicator_power_service_choose_primary_device (p->devices);
+ /* update the notifier's battery */
+ if ((p->primary_device != NULL) && (indicator_power_device_get_kind(p->primary_device) == UP_DEVICE_KIND_BATTERY))
+ indicator_power_notifier_set_battery (p->notifier, p->primary_device);
+ else
+ indicator_power_notifier_set_battery (p->notifier, NULL);
+
/* update the battery-level action's state */
if (p->primary_device == NULL)
battery_level = 0;
@@ -1001,6 +1013,7 @@ my_dispose (GObject * o)
g_clear_object (&p->settings);
}
+ g_clear_object (&p->notifier);
g_clear_object (&p->brightness_action);
g_clear_object (&p->battery_level_action);
g_clear_object (&p->header_action);
@@ -1035,6 +1048,8 @@ indicator_power_service_init (IndicatorPowerService * self)
p->settings = g_settings_new ("com.canonical.indicator.power");
+ p->notifier = indicator_power_notifier_new ();
+
uscreen_proxy = uscreen_get_proxy(&brightness_params);
if (uscreen_proxy != NULL)
{
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index c5ad09d..a0d24af 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -5,6 +5,11 @@ add_library (gtest STATIC
set_target_properties (gtest PROPERTIES INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} ${GTEST_INCLUDE_DIR})
set_target_properties (gtest PROPERTIES COMPILE_FLAGS ${COMPILE_FLAGS} -w)
+# dbustest
+pkg_check_modules(DBUSTEST REQUIRED
+ dbustest-1>=14.04.0)
+include_directories (SYSTEM ${DBUSTEST_INCLUDE_DIRS})
+
# add warnings
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g ${C_WARNING_ARGS}")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-weak-vtables -Wno-global-constructors") # Google Test
@@ -40,7 +45,17 @@ function(add_test_by_name name)
add_executable (${TEST_NAME} ${TEST_NAME}.cc gschemas.compiled)
add_test (${TEST_NAME} ${TEST_NAME})
add_dependencies (${TEST_NAME} libindicatorpowerservice)
- target_link_libraries (${TEST_NAME} indicatorpowerservice gtest ${SERVICE_DEPS_LIBRARIES} ${GTEST_LIBS})
+ target_link_libraries (${TEST_NAME} indicatorpowerservice gtest ${DBUSTEST_LIBRARIES} ${SERVICE_DEPS_LIBRARIES} ${GTEST_LIBS})
endfunction()
+add_test_by_name(test-notify)
+add_test(NAME dear-reader-the-next-test-takes-80-seconds COMMAND true)
add_test_by_name(test-device)
+###
+###
+
+set (APP_NAME indicator-power-service-cmdline-battery)
+add_executable (${APP_NAME} ${APP_NAME}.cc device-provider-mock.c)
+add_dependencies (${APP_NAME} libindicatorpowerservice)
+target_link_libraries (${APP_NAME} indicatorpowerservice ${SERVICE_DEPS_LIBRARIES})
+
diff --git a/tests/device-provider-mock.c b/tests/device-provider-mock.c
new file mode 100644
index 0000000..afca178
--- /dev/null
+++ b/tests/device-provider-mock.c
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+#include "device.h"
+#include "device-provider.h"
+#include "device-provider-mock.h"
+
+/***
+**** GObject boilerplate
+***/
+
+static void indicator_power_device_provider_interface_init (
+ IndicatorPowerDeviceProviderInterface * iface);
+
+G_DEFINE_TYPE_WITH_CODE (
+ IndicatorPowerDeviceProviderMock,
+ indicator_power_device_provider_mock,
+ G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (INDICATOR_TYPE_POWER_DEVICE_PROVIDER,
+ indicator_power_device_provider_interface_init))
+
+/***
+**** IndicatorPowerDeviceProvider virtual functions
+***/
+
+static GList *
+my_get_devices (IndicatorPowerDeviceProvider * provider)
+{
+ IndicatorPowerDeviceProviderMock * self = INDICATOR_POWER_DEVICE_PROVIDER_MOCK(provider);
+
+ return g_list_copy_deep (self->devices, (GCopyFunc)g_object_ref, NULL);
+}
+
+/***
+**** GObject virtual functions
+***/
+
+static void
+my_dispose (GObject * o)
+{
+ IndicatorPowerDeviceProviderMock * self = INDICATOR_POWER_DEVICE_PROVIDER_MOCK(o);
+
+ g_list_free_full (self->devices, g_object_unref);
+
+ G_OBJECT_CLASS (indicator_power_device_provider_mock_parent_class)->dispose (o);
+}
+
+/***
+**** Instantiation
+***/
+
+static void
+indicator_power_device_provider_mock_class_init (IndicatorPowerDeviceProviderMockClass * klass)
+{
+ GObjectClass * object_class;
+
+ object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = my_dispose;
+}
+
+static void
+indicator_power_device_provider_interface_init (IndicatorPowerDeviceProviderInterface * iface)
+{
+ iface->get_devices = my_get_devices;
+}
+
+static void
+indicator_power_device_provider_mock_init (IndicatorPowerDeviceProviderMock * self)
+{
+}
+
+/***
+**** Public API
+***/
+
+IndicatorPowerDeviceProvider *
+indicator_power_device_provider_mock_new (void)
+{
+ gpointer o = g_object_new (INDICATOR_TYPE_POWER_DEVICE_PROVIDER_MOCK, NULL);
+
+ return INDICATOR_POWER_DEVICE_PROVIDER (o);
+}
+
+void
+indicator_power_device_provider_add_device (IndicatorPowerDeviceProviderMock * provider,
+ IndicatorPowerDevice * device)
+{
+ provider->devices = g_list_append (provider->devices, g_object_ref(device));
+
+ g_signal_connect_swapped (device, "notify", G_CALLBACK(indicator_power_device_provider_emit_devices_changed), provider);
+}
diff --git a/tests/device-provider-mock.h b/tests/device-provider-mock.h
new file mode 100644
index 0000000..4d06924
--- /dev/null
+++ b/tests/device-provider-mock.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+#ifndef __INDICATOR_POWER_DEVICE_PROVIDER_MOCK__H__
+#define __INDICATOR_POWER_DEVICE_PROVIDER_MOCK__H__
+
+#include <glib-object.h> /* parent class */
+
+#include "device.h"
+#include "device-provider.h"
+
+G_BEGIN_DECLS
+
+#define INDICATOR_TYPE_POWER_DEVICE_PROVIDER_MOCK \
+ (indicator_power_device_provider_mock_get_type())
+
+#define INDICATOR_POWER_DEVICE_PROVIDER_MOCK(o) \
+ (G_TYPE_CHECK_INSTANCE_CAST ((o), \
+ INDICATOR_TYPE_POWER_DEVICE_PROVIDER_MOCK, \
+ IndicatorPowerDeviceProviderMock))
+
+#define INDICATOR_POWER_DEVICE_PROVIDER_MOCK_GET_CLASS(o) \
+ (G_TYPE_INSTANCE_GET_CLASS ((o), \
+ INDICATOR_TYPE_POWER_DEVICE_PROVIDER_MOCK, \
+ IndicatorPowerDeviceProviderMockClass))
+
+#define INDICATOR_IS_POWER_DEVICE_PROVIDER_MOCK(o) \
+ (G_TYPE_CHECK_INSTANCE_TYPE ((o), \
+ INDICATOR_TYPE_POWER_DEVICE_PROVIDER_MOCK))
+
+typedef struct _IndicatorPowerDeviceProviderMock
+ IndicatorPowerDeviceProviderMock;
+typedef struct _IndicatorPowerDeviceProviderMockPriv
+ IndicatorPowerDeviceProviderMockPriv;
+typedef struct _IndicatorPowerDeviceProviderMockClass
+ IndicatorPowerDeviceProviderMockClass;
+
+/**
+ * An IndicatorPowerDeviceProvider which gets its devices from Mock.
+ */
+struct _IndicatorPowerDeviceProviderMock
+{
+ GObject parent_instance;
+
+ /*< private >*/
+ GList * devices;
+};
+
+struct _IndicatorPowerDeviceProviderMockClass
+{
+ GObjectClass parent_class;
+};
+
+GType indicator_power_device_provider_mock_get_type (void);
+
+IndicatorPowerDeviceProvider * indicator_power_device_provider_mock_new (void);
+
+void indicator_power_device_provider_add_device (IndicatorPowerDeviceProviderMock * provider,
+ IndicatorPowerDevice * device);
+
+G_END_DECLS
+
+#endif /* __INDICATOR_POWER_DEVICE_PROVIDER_MOCK__H__ */
diff --git a/tests/glib-fixture.h b/tests/glib-fixture.h
new file mode 100644
index 0000000..d333ab2
--- /dev/null
+++ b/tests/glib-fixture.h
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+#include <map>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <gio/gio.h>
+
+#include <gtest/gtest.h>
+
+#include <locale.h> // setlocale()
+
+class GlibFixture : public ::testing::Test
+{
+ private:
+
+ GLogFunc realLogHandler;
+
+ std::map<GLogLevelFlags,size_t> expected_log;
+ std::map<GLogLevelFlags,std::vector<std::string>> log;
+
+ void test_log_counts()
+ {
+ const GLogLevelFlags levels_to_test[] = { G_LOG_LEVEL_ERROR,
+ G_LOG_LEVEL_CRITICAL,
+ G_LOG_LEVEL_MESSAGE,
+ G_LOG_LEVEL_WARNING };
+
+ for(const auto& level : levels_to_test)
+ {
+ const auto& v = log[level];
+ const auto n = v.size();
+
+ EXPECT_EQ(expected_log[level], n);
+
+ if (expected_log[level] != n)
+ for (size_t i=0; i<n; ++i)
+ g_print("%d %s\n", (n+1), v[i].c_str());
+ }
+
+ expected_log.clear();
+ log.clear();
+ }
+
+ static void default_log_handler(const gchar * log_domain,
+ GLogLevelFlags log_level,
+ const gchar * message,
+ gpointer self)
+ {
+ auto tmp = g_strdup_printf ("%s:%d \"%s\"", log_domain, (int)log_level, message);
+ static_cast<GlibFixture*>(self)->log[log_level].push_back(tmp);
+ g_free(tmp);
+ }
+
+ protected:
+
+ void increment_expected_errors(GLogLevelFlags level, size_t n=1)
+ {
+ expected_log[level] += n;
+ }
+
+ virtual void SetUp()
+ {
+ setlocale(LC_ALL, "C.UTF-8");
+
+ loop = g_main_loop_new(nullptr, false);
+
+ g_log_set_default_handler(default_log_handler, this);
+
+ g_unsetenv("DISPLAY");
+ }
+
+ virtual void TearDown()
+ {
+ test_log_counts();
+
+ g_log_set_default_handler(realLogHandler, this);
+
+ g_clear_pointer(&loop, g_main_loop_unref);
+ }
+
+ private:
+
+ static gboolean
+ wait_for_signal__timeout(gpointer name)
+ {
+ g_error("%s: timed out waiting for signal '%s'", G_STRLOC, (char*)name);
+ return G_SOURCE_REMOVE;
+ }
+
+ static gboolean
+ wait_msec__timeout(gpointer loop)
+ {
+ g_main_loop_quit(static_cast<GMainLoop*>(loop));
+ return G_SOURCE_CONTINUE;
+ }
+
+ protected:
+
+ /* convenience func to loop while waiting for a GObject's signal */
+ void wait_for_signal(gpointer o, const gchar * signal, const guint timeout_seconds=5)
+ {
+ // wait for the signal or for timeout, whichever comes first
+ const auto handler_id = g_signal_connect_swapped(o, signal,
+ G_CALLBACK(g_main_loop_quit),
+ loop);
+ const auto timeout_id = g_timeout_add_seconds(timeout_seconds,
+ wait_for_signal__timeout,
+ loop);
+ g_main_loop_run(loop);
+ g_source_remove(timeout_id);
+ g_signal_handler_disconnect(o, handler_id);
+ }
+
+ /* convenience func to loop for N msec */
+ void wait_msec(guint msec=50)
+ {
+ const auto id = g_timeout_add(msec, wait_msec__timeout, loop);
+ g_main_loop_run(loop);
+ g_source_remove(id);
+ }
+
+ GMainLoop * loop;
+};
diff --git a/tests/indicator-power-service-cmdline-battery.cc b/tests/indicator-power-service-cmdline-battery.cc
new file mode 100644
index 0000000..a7a86a1
--- /dev/null
+++ b/tests/indicator-power-service-cmdline-battery.cc
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+#include <cstdlib>
+
+#include <locale.h> // setlocale()
+#include <libintl.h> // bindtextdomain()
+#include <unistd.h> // STDIN_FILENO
+
+#include <gio/gio.h>
+
+#include "device-provider-mock.h"
+
+#include "service.h"
+
+/***
+****
+***/
+
+static void
+on_name_lost (gpointer instance G_GNUC_UNUSED, gpointer loop)
+{
+ g_message ("exiting: service couldn't acquire or lost ownership of busname");
+ g_main_loop_quit ((GMainLoop*)loop);
+}
+
+static IndicatorPowerDevice * battery = nullptr;
+
+static GMainLoop * loop = nullptr;
+
+static gboolean on_command_stream_available (GIOChannel *source,
+ GIOCondition /*condition*/,
+ gpointer /*user_data*/)
+{
+ gchar * str = nullptr;
+ GError * error = nullptr;
+ auto status = g_io_channel_read_line (source, &str, nullptr, nullptr, &error);
+ g_assert_no_error (error);
+
+ if (status == G_IO_STATUS_NORMAL)
+ {
+ g_strstrip (str);
+
+ if (!g_strcmp0 (str, "charging"))
+ {
+ g_object_set (battery, INDICATOR_POWER_DEVICE_STATE, UP_DEVICE_STATE_CHARGING, nullptr);
+ }
+ else if (!g_strcmp0 (str, "discharging"))
+ {
+ g_object_set (battery, INDICATOR_POWER_DEVICE_STATE, UP_DEVICE_STATE_DISCHARGING, nullptr);
+ }
+ else
+ {
+ g_object_set (battery, INDICATOR_POWER_DEVICE_PERCENTAGE, atof(str), nullptr);
+ }
+ }
+ else if (status == G_IO_STATUS_EOF)
+ {
+ g_main_loop_quit (loop);
+ }
+
+ g_free (str);
+ return G_SOURCE_CONTINUE;
+}
+
+/* this is basically indicator-power-service with a custom provider */
+int
+main (int argc G_GNUC_UNUSED, char ** argv G_GNUC_UNUSED)
+{
+ g_message ("This app is basically the same as indicator-power-service but,\n"
+ "instead of the system's real devices, sees a single fake battery\n"
+ "which can be manipulated by typing commands:\n"
+ "'charging', 'discharging', a charge percentage, or ctrl-c.");
+
+ IndicatorPowerDeviceProvider * device_provider;
+ IndicatorPowerService * service;
+
+ g_assert(g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, true));
+ g_assert(g_setenv("GSETTINGS_BACKEND", "memory", true));
+
+ /* boilerplate i18n */
+ setlocale (LC_ALL, "");
+ bindtextdomain (GETTEXT_PACKAGE, GNOMELOCALEDIR);
+ textdomain (GETTEXT_PACKAGE);
+
+ /* read lines from the command line */
+ auto channel = g_io_channel_unix_new (STDIN_FILENO);
+ auto watch_tag = g_io_add_watch (channel, G_IO_IN, on_command_stream_available, nullptr);
+
+ /* run */
+ battery = indicator_power_device_new ("/some/path", UP_DEVICE_KIND_BATTERY, 50.0, UP_DEVICE_STATE_DISCHARGING, 30*60);
+ device_provider = indicator_power_device_provider_mock_new ();
+ indicator_power_device_provider_add_device (INDICATOR_POWER_DEVICE_PROVIDER_MOCK(device_provider), battery);
+ service = indicator_power_service_new (device_provider);
+ loop = g_main_loop_new (NULL, FALSE);
+ g_signal_connect (service, INDICATOR_POWER_SERVICE_SIGNAL_NAME_LOST,
+ G_CALLBACK(on_name_lost), loop);
+ g_main_loop_run (loop);
+
+ /* cleanup */
+ g_main_loop_unref (loop);
+ g_source_remove (watch_tag);
+ g_io_channel_unref (channel);
+ g_clear_object (&service);
+ g_clear_object (&device_provider);
+ g_clear_object (&battery);
+ return 0;
+}
diff --git a/tests/manual b/tests/manual
index d3a22e1..a542cac 100644
--- a/tests/manual
+++ b/tests/manual
@@ -22,3 +22,20 @@ Test-case indicator-power/unity8-items-check
<dd>The menu is populated with items</dd>
</dl>
+Test-case indicator-power/low-battery-popups
+<dl>
+ <dt>Open a terminal</dt>
+ <dt>Stop the currently-running power indicator: "stop indicator-power"</dt>
+ <dt>Start the fake battery harness in the tests/build/ directory: "indicator-power-service-cmdline-battery"</dt>
+ <dd>Battery indicator should update, showing a discharging battery with a 50% charge</dd>
+ <dt>Type: "10" (no quotes) and press Enter</dt>
+ <dd>A popup should appear saying 'Battery low - 10% charge remaining'</dd>
+ <dd>Battery indicator's icon should show a low charge</dd>
+ <dd>Battery indicator's "Charge level" menuitem should show a 10% charge</dd>
+ <dt>Type: "9" (no quotes) and press Enter</dt>
+ <dd>The 'Battery low' popup should NOT appear, since we've already been notified</dd>
+ <dd>Battery indicator's "Charge level" menuitem should show a 9% charge</dd>
+ <dt>Type: "5" (no quotes) and press Enter</dt>
+ <dd>No 'Battery low' popup SHOULD appear, since 5% is the next warning threshold</dd>
+ <dd>Battery indicator's "Charge level" menuitem should show a 5% charge</dd>
+</dl>
diff --git a/tests/test-notify.cc b/tests/test-notify.cc
new file mode 100644
index 0000000..b5166a0
--- /dev/null
+++ b/tests/test-notify.cc
@@ -0,0 +1,410 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+
+#include "glib-fixture.h"
+
+#include "dbus-shared.h"
+#include "device.h"
+#include "notifier.h"
+
+#include <gtest/gtest.h>
+
+#include <libdbustest/dbus-test.h>
+
+#include <libnotify/notify.h>
+
+#include <glib.h>
+#include <gio/gio.h>
+
+/***
+****
+***/
+
+class NotifyFixture: public GlibFixture
+{
+private:
+
+ typedef GlibFixture super;
+
+ static constexpr char const * NOTIFY_BUSNAME {"org.freedesktop.Notifications"};
+ static constexpr char const * NOTIFY_INTERFACE {"org.freedesktop.Notifications"};
+ static constexpr char const * NOTIFY_PATH {"/org/freedesktop/Notifications"};
+
+protected:
+
+ DbusTestService * service = nullptr;
+ DbusTestDbusMock * mock = nullptr;
+ DbusTestDbusMockObject * obj = nullptr;
+ GDBusConnection * bus = nullptr;
+
+ static constexpr int FIRST_NOTIFY_ID {1234};
+
+ static constexpr int NOTIFICATION_CLOSED_EXPIRED {1};
+ static constexpr int NOTIFICATION_CLOSED_DISMISSED {2};
+ static constexpr int NOTIFICATION_CLOSED_API {3};
+ static constexpr int NOTIFICATION_CLOSED_UNDEFINED {4};
+
+ static constexpr char const * APP_NAME {"indicator-power-service"};
+
+ static constexpr char const * METHOD_CLOSE {"CloseNotification"};
+ static constexpr char const * METHOD_NOTIFY {"Notify"};
+ static constexpr char const * METHOD_GET_CAPS {"GetCapabilities"};
+ static constexpr char const * METHOD_GET_INFO {"GetServerInformation"};
+ static constexpr char const * SIGNAL_CLOSED {"NotificationClosed"};
+
+ static constexpr char const * HINT_TIMEOUT {"x-canonical-snap-decisions-timeout"};
+
+protected:
+
+ void SetUp()
+ {
+ super::SetUp();
+
+ // init DBusMock / dbus-test-runner
+
+ service = dbus_test_service_new(nullptr);
+
+ GError * error = nullptr;
+ mock = dbus_test_dbus_mock_new(NOTIFY_BUSNAME);
+ obj = dbus_test_dbus_mock_get_object(mock,
+ NOTIFY_PATH,
+ NOTIFY_INTERFACE,
+ &error);
+ g_assert_no_error (error);
+
+ // METHOD_GET_INFO
+ dbus_test_dbus_mock_object_add_method(mock, obj, METHOD_GET_INFO,
+ nullptr,
+ G_VARIANT_TYPE("(ssss)"),
+ "ret = ('mock-notify', 'test vendor', '1.0', '1.1')",
+ &error);
+ g_assert_no_error (error);
+
+ // METHOD_NOTIFY
+ auto str = g_strdup_printf("try:\n"
+ " self.NextNotifyId\n"
+ "except AttributeError:\n"
+ " self.NextNotifyId = %d\n"
+ "ret = self.NextNotifyId\n"
+ "self.NextNotifyId += 1\n",
+ FIRST_NOTIFY_ID);
+ dbus_test_dbus_mock_object_add_method(mock, obj, METHOD_NOTIFY,
+ G_VARIANT_TYPE("(susssasa{sv}i)"),
+ G_VARIANT_TYPE_UINT32,
+ str,
+ &error);
+ g_assert_no_error (error);
+ g_free (str);
+
+ // METHOD_CLOSE
+ str = g_strdup_printf("self.EmitSignal('%s', '%s', 'uu', [ args[0], %d ])",
+ NOTIFY_INTERFACE,
+ SIGNAL_CLOSED,
+ NOTIFICATION_CLOSED_API);
+ dbus_test_dbus_mock_object_add_method(mock, obj, METHOD_CLOSE,
+ G_VARIANT_TYPE("(u)"),
+ nullptr,
+ str,
+ &error);
+ g_assert_no_error (error);
+ g_free (str);
+
+ dbus_test_service_add_task(service, DBUS_TEST_TASK(mock));
+ dbus_test_service_start_tasks(service);
+
+ bus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr);
+ g_dbus_connection_set_exit_on_close(bus, FALSE);
+ g_object_add_weak_pointer(G_OBJECT(bus), (gpointer *)&bus);
+
+ notify_init(APP_NAME);
+ }
+
+ virtual void TearDown()
+ {
+ notify_uninit();
+
+ g_clear_object(&mock);
+ g_clear_object(&service);
+ g_object_unref(bus);
+
+ // wait a little while for the scaffolding to shut down,
+ // but don't block on it forever...
+ unsigned int cleartry = 0;
+ while ((bus != nullptr) && (cleartry < 50))
+ {
+ g_usleep(100000);
+ while (g_main_pending())
+ g_main_iteration(true);
+ cleartry++;
+ }
+
+ super::TearDown();
+ }
+};
+
+/***
+****
+***/
+
+// simple test to confirm the NotifyFixture plumbing all works
+TEST_F(NotifyFixture, HelloWorld)
+{
+}
+
+/***
+****
+***/
+
+
+namespace
+{
+ static constexpr double percent_critical {2.0};
+ static constexpr double percent_very_low {5.0};
+ static constexpr double percent_low {10.0};
+
+ void set_battery_percentage (IndicatorPowerDevice * battery, gdouble p)
+ {
+ g_object_set (battery, INDICATOR_POWER_DEVICE_PERCENTAGE, p, nullptr);
+ }
+}
+
+TEST_F(NotifyFixture, PercentageToLevel)
+{
+ auto battery = indicator_power_device_new ("/object/path",
+ UP_DEVICE_KIND_BATTERY,
+ 50.0,
+ UP_DEVICE_STATE_DISCHARGING,
+ 30);
+
+ // confirm that the power levels trigger at the right percentages
+ for (int i=100; i>=0; --i)
+ {
+ set_battery_percentage (battery, i);
+ const auto level = indicator_power_notifier_get_power_level(battery);
+
+ if (i <= percent_critical)
+ EXPECT_STREQ (POWER_LEVEL_STR_CRITICAL, level);
+ else if (i <= percent_very_low)
+ EXPECT_STREQ (POWER_LEVEL_STR_VERY_LOW, level);
+ else if (i <= percent_low)
+ EXPECT_STREQ (POWER_LEVEL_STR_LOW, level);
+ else
+ EXPECT_STREQ (POWER_LEVEL_STR_OK, level);
+ }
+
+ g_object_unref (battery);
+}
+
+/***
+****
+***/
+
+// scaffolding to monitor PropertyChanged signals
+namespace
+{
+ enum
+ {
+ FIELD_POWER_LEVEL = (1<<0),
+ FIELD_IS_WARNING = (1<<1)
+ };
+
+ struct ChangedParams
+ {
+ std::string power_level = POWER_LEVEL_STR_OK;
+ bool is_warning = false;
+ uint32_t fields = 0;
+ };
+
+ void on_battery_property_changed (GDBusConnection *connection G_GNUC_UNUSED,
+ const gchar *sender_name G_GNUC_UNUSED,
+ const gchar *object_path G_GNUC_UNUSED,
+ const gchar *interface_name G_GNUC_UNUSED,
+ const gchar *signal_name G_GNUC_UNUSED,
+ GVariant *parameters,
+ gpointer gchanged_params)
+ {
+ g_return_if_fail (g_variant_n_children (parameters) == 3);
+ auto dict = g_variant_get_child_value (parameters, 1);
+ g_return_if_fail (g_variant_is_of_type (dict, G_VARIANT_TYPE_DICTIONARY));
+ auto changed_params = static_cast<ChangedParams*>(gchanged_params);
+
+ const char * power_level;
+ if (g_variant_lookup (dict, "PowerLevel", "&s", &power_level, nullptr))
+ {
+ changed_params->power_level = power_level;
+ changed_params->fields |= FIELD_POWER_LEVEL;
+ }
+
+ gboolean is_warning;
+ if (g_variant_lookup (dict, "IsWarning", "b", &is_warning, nullptr))
+ {
+ changed_params->is_warning = is_warning;
+ changed_params->fields |= FIELD_IS_WARNING;
+ }
+
+ g_variant_unref (dict);
+ }
+}
+
+TEST_F(NotifyFixture, LevelsDuringBatteryDrain)
+{
+ auto battery = indicator_power_device_new ("/object/path",
+ UP_DEVICE_KIND_BATTERY,
+ 50.0,
+ UP_DEVICE_STATE_DISCHARGING,
+ 30);
+
+ // set up a notifier and give it the battery so changing the battery's
+ // charge should show up on the bus.
+ auto notifier = indicator_power_notifier_new ();
+ indicator_power_notifier_set_battery (notifier, battery);
+ indicator_power_notifier_set_bus (notifier, bus);
+ wait_msec();
+
+ ChangedParams changed_params;
+ auto sub_tag = g_dbus_connection_signal_subscribe (bus,
+ nullptr,
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ BUS_PATH"/Battery",
+ nullptr,
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ on_battery_property_changed,
+ &changed_params,
+ nullptr);
+
+ // confirm that draining the battery puts
+ // the power_level change through its paces
+ for (int i=100; i>=0; --i)
+ {
+ changed_params = ChangedParams();
+ EXPECT_TRUE (changed_params.fields == 0);
+
+ const auto old_level = indicator_power_notifier_get_power_level(battery);
+ set_battery_percentage (battery, i);
+ const auto new_level = indicator_power_notifier_get_power_level(battery);
+ wait_msec();
+
+ if (old_level == new_level)
+ {
+ EXPECT_EQ (0, (changed_params.fields & FIELD_POWER_LEVEL));
+ }
+ else
+ {
+ EXPECT_EQ (FIELD_POWER_LEVEL, (changed_params.fields & FIELD_POWER_LEVEL));
+ EXPECT_EQ (new_level, changed_params.power_level);
+ }
+ }
+
+ // cleanup
+ g_dbus_connection_signal_unsubscribe (bus, sub_tag);
+ g_object_unref (notifier);
+ g_object_unref (battery);
+}
+
+/***
+****
+***/
+
+TEST_F(NotifyFixture, EventsThatChangeNotifications)
+{
+ // GetCapabilities returns an array containing 'actions', so that we'll
+ // get snap decisions and the 'IsWarning' property
+ GError * error = nullptr;
+ dbus_test_dbus_mock_object_add_method (mock,
+ obj,
+ METHOD_GET_CAPS,
+ nullptr,
+ G_VARIANT_TYPE_STRING_ARRAY,
+ "ret = ['actions', 'body']",
+ &error);
+ g_assert_no_error (error);
+
+ auto battery = indicator_power_device_new ("/object/path",
+ UP_DEVICE_KIND_BATTERY,
+ percent_low + 1.0,
+ UP_DEVICE_STATE_DISCHARGING,
+ 30);
+
+ // set up a notifier and give it the battery so changing the battery's
+ // charge should show up on the bus.
+ auto notifier = indicator_power_notifier_new ();
+ indicator_power_notifier_set_battery (notifier, battery);
+ indicator_power_notifier_set_bus (notifier, bus);
+ ChangedParams changed_params;
+ auto sub_tag = g_dbus_connection_signal_subscribe (bus,
+ nullptr,
+ "org.freedesktop.DBus.Properties",
+ "PropertiesChanged",
+ BUS_PATH"/Battery",
+ nullptr,
+ G_DBUS_SIGNAL_FLAGS_NONE,
+ on_battery_property_changed,
+ &changed_params,
+ nullptr);
+
+ // test setup case
+ wait_msec();
+ EXPECT_STREQ (POWER_LEVEL_STR_OK, changed_params.power_level.c_str());
+
+ // change the percent past the 'low' threshold and confirm that
+ // a) the power level changes
+ // b) we get a notification
+ changed_params = ChangedParams();
+ set_battery_percentage (battery, percent_low);
+ wait_msec();
+ EXPECT_EQ (FIELD_POWER_LEVEL|FIELD_IS_WARNING, changed_params.fields);
+ EXPECT_EQ (indicator_power_notifier_get_power_level(battery), changed_params.power_level);
+ EXPECT_TRUE (changed_params.is_warning);
+
+ // now test that the warning changes if the level goes down even lower...
+ changed_params = ChangedParams();
+ set_battery_percentage (battery, percent_very_low);
+ wait_msec();
+ EXPECT_EQ (FIELD_POWER_LEVEL, changed_params.fields);
+ EXPECT_STREQ (POWER_LEVEL_STR_VERY_LOW, changed_params.power_level.c_str());
+
+ // ...and that the warning is taken down if the battery is plugged back in...
+ changed_params = ChangedParams();
+ g_object_set (battery, INDICATOR_POWER_DEVICE_STATE, UP_DEVICE_STATE_CHARGING, nullptr);
+ wait_msec();
+ EXPECT_EQ (FIELD_IS_WARNING, changed_params.fields);
+ EXPECT_FALSE (changed_params.is_warning);
+
+ // ...and that it comes back if we unplug again...
+ changed_params = ChangedParams();
+ g_object_set (battery, INDICATOR_POWER_DEVICE_STATE, UP_DEVICE_STATE_DISCHARGING, nullptr);
+ wait_msec();
+ EXPECT_EQ (FIELD_IS_WARNING, changed_params.fields);
+ EXPECT_TRUE (changed_params.is_warning);
+
+ // ...and that it's taken down if the power level is OK
+ changed_params = ChangedParams();
+ set_battery_percentage (battery, percent_low+1);
+ wait_msec();
+ EXPECT_EQ (FIELD_POWER_LEVEL|FIELD_IS_WARNING, changed_params.fields);
+ EXPECT_STREQ (POWER_LEVEL_STR_OK, changed_params.power_level.c_str());
+ EXPECT_FALSE (changed_params.is_warning);
+
+ // cleanup
+ g_dbus_connection_signal_unsubscribe (bus, sub_tag);
+ g_object_unref (notifier);
+ g_object_unref (battery);
+}