diff options
30 files changed, 1689 insertions, 53 deletions
diff --git a/Makefile.am b/Makefile.am index f8141a8..a48804e 100644 --- a/Makefile.am +++ b/Makefile.am @@ -12,10 +12,10 @@ endif if BUILD_TESTS SUBDIRS += \ - test + tests # build src first -test: src +tests: src libmessaging-menu endif diff --git a/configure.ac b/configure.ac index e27f41d..b9c8800 100644 --- a/configure.ac +++ b/configure.ac @@ -43,10 +43,13 @@ GLIB_REQUIRED_VERSION=2.35.4 INTROSPECTION_REQUIRED_VERSION=1.32.0 PKG_CHECK_MODULES(APPLET, gio-unix-2.0 >= $GIO_UNIX_REQUIRED_VERSION - glib-2.0 >= $GLIB_REQUIRED_VERSION) + glib-2.0 >= $GLIB_REQUIRED_VERSION + accountsservice) PKG_CHECK_MODULES(GIO, gio-unix-2.0 >= $GIO_UNIX_REQUIRED_VERSION) +PKG_CHECK_MODULES(DBUSTEST, dbustest-1) + AC_SUBST(APPLET_CFLAGS) AC_SUBST(APPLET_LIBS) @@ -171,7 +174,7 @@ data/icons/scalable/status/Makefile data/icons/scalable/categories/Makefile data/upstart/Makefile po/Makefile.in -test/Makefile +tests/Makefile libmessaging-menu/Makefile libmessaging-menu/messaging-menu.pc doc/Makefile diff --git a/data/com.canonical.indicator.messages b/data/com.canonical.indicator.messages index 2210b6b..572a4a7 100644 --- a/data/com.canonical.indicator.messages +++ b/data/com.canonical.indicator.messages @@ -6,11 +6,14 @@ Position=50 [desktop] ObjectPath=/com/canonical/indicator/messages/desktop +[desktop_greeter] +ObjectPath=/com/canonical/indicator/messages/desktop_greeter + [phone] ObjectPath=/com/canonical/indicator/messages/phone Position=100 [phone_greeter] -ObjectPath=/com/canonical/indicator/messages/phone +ObjectPath=/com/canonical/indicator/messages/phone_greeter Position=100 diff --git a/debian/changelog b/debian/changelog index 230a105..e1820d8 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,56 @@ +indicator-messages (13.10.1+15.04.20150311-0ubuntu1) vivid; urgency=medium + + [ Ted Gould ] + * Sanity tests to test basic features of the messaging menu + + -- CI Train Bot <ci-train-bot@canonical.com> Wed, 11 Mar 2015 16:21:18 +0000 + +indicator-messages (13.10.1+15.04.20150304-0ubuntu1) vivid; urgency=medium + + [ Ted Gould ] + * Make the message actions have the message ID in their names like the + muxer + + -- CI Train Bot <ci-train-bot@canonical.com> Wed, 04 Mar 2015 23:59:37 +0000 + +indicator-messages (13.10.1+15.04.20150219-0ubuntu1) vivid; urgency=medium + + [ Ted Gould ] + * Provide functions that allow for action names directly (LP: + #1385331) + + -- CI Train Bot <ci-train-bot@canonical.com> Thu, 19 Feb 2015 15:55:54 +0000 + +indicator-messages (13.10.1+15.04.20150126-0ubuntu1) vivid; urgency=low + + [ Lars Uebernickel ] + * unescape_action_name: fix logic error (& instead of |) (LP: + #1414025) + + -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 26 Jan 2015 22:07:44 +0000 + +indicator-messages (13.10.1+15.04.20150112-0ubuntu1) vivid; urgency=low + + [ Lars Uebernickel ] + * desktop menu: don't warn when no default handler for a mime type is + found (LP: #1389725) + * Escape message and source ids when using them as action names (LP: + #1386584) + + -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 12 Jan 2015 18:39:08 +0000 + +indicator-messages (13.10.1+15.04.20141103-0ubuntu1) vivid; urgency=low + + [ Ted Gould ] + * Add filtering for lock screen. (LP: #1358340) + + [ Lars Uebernickel ] + * libmessaging-menu: allow numbers in object paths (LP: #1384811) + * libmessaging-menu: fix section link in the documentation and add + descriptions (LP: #1313561) + + -- Ubuntu daily release <ps-jenkins@lists.canonical.com> Mon, 03 Nov 2014 20:56:30 +0000 + indicator-messages (13.10.1+14.10.20141009-0ubuntu1) utopic; urgency=low [ Ted Gould ] diff --git a/debian/control b/debian/control index b2c899d..eccb874 100644 --- a/debian/control +++ b/debian/control @@ -10,6 +10,8 @@ Build-Depends: debhelper (>= 9), gobject-introspection (>= 0.9.12-4~), gtk-doc-tools, intltool, + libaccountsservice-dev, + libdbustest1-dev, libgirepository1.0-dev (>= 0.9.12), libgtest-dev, python3-dbusmock, diff --git a/libmessaging-menu/messaging-menu-app.c b/libmessaging-menu/messaging-menu-app.c index 689a388..83dfb41 100644 --- a/libmessaging-menu/messaging-menu-app.c +++ b/libmessaging-menu/messaging-menu-app.c @@ -25,7 +25,7 @@ #include <string.h> /** - * SECTION:messaging-menu + * SECTION:messaging-menu-app * @title: MessagingMenuApp * @short_description: An application section in the messaging menu * @include: messaging-menu.h @@ -207,7 +207,7 @@ messaging_menu_app_get_dbus_object_path (MessagingMenuApp *app) g_app_info_get_id (G_APP_INFO (app->appinfo)), NULL); - g_strcanon (path, "/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", '_'); + g_strcanon (path, "/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", '_'); return path; } diff --git a/libmessaging-menu/messaging-menu-message.c b/libmessaging-menu/messaging-menu-message.c index 02b329d..4bf8b7c 100644 --- a/libmessaging-menu/messaging-menu-message.c +++ b/libmessaging-menu/messaging-menu-message.c @@ -19,6 +19,13 @@ #include "messaging-menu-message.h" +/** + * SECTION:messaging-menu-message + * @title: MessagingMenuMessage + * @short_description: A single message in the messaging menu + * @include: messaging-menu.h + */ + typedef GObjectClass MessagingMenuMessageClass; struct _MessagingMenuMessage diff --git a/po/POTFILES.in b/po/POTFILES.in index 6cb028f..4cb6565 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,5 +1,4 @@ [encoding: UTF-8] -test/indicator-messages-service-activate.c src/im-phone-menu.c src/messages-service.c src/im-desktop-menu.c diff --git a/src/Makefile.am b/src/Makefile.am index bc674c2..36e6a93 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -10,6 +10,8 @@ indicator_messages_service_SOURCES = \ gactionmuxer.h \ gsettingsstrv.c \ gsettingsstrv.h \ + im-accounts-service.c \ + im-accounts-service.h \ im-menu.c \ im-menu.h \ im-phone-menu.c \ diff --git a/src/im-accounts-service.c b/src/im-accounts-service.c new file mode 100644 index 0000000..b7ab15d --- /dev/null +++ b/src/im-accounts-service.c @@ -0,0 +1,223 @@ +/* + * 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: + * Ted Gould <ted@canonical.com> + */ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include <act/act.h> + +#include "im-accounts-service.h" + +typedef struct _ImAccountsServicePrivate ImAccountsServicePrivate; + +struct _ImAccountsServicePrivate { + ActUserManager * user_manager; + GDBusProxy * touch_settings; + GCancellable * cancel; +}; + +#define IM_ACCOUNTS_SERVICE_GET_PRIVATE(o) \ +(G_TYPE_INSTANCE_GET_PRIVATE ((o), IM_ACCOUNTS_SERVICE_TYPE, ImAccountsServicePrivate)) + +static void im_accounts_service_class_init (ImAccountsServiceClass *klass); +static void im_accounts_service_init (ImAccountsService *self); +static void im_accounts_service_dispose (GObject *object); +static void im_accounts_service_finalize (GObject *object); +static void user_changed (ActUserManager * manager, ActUser * user, gpointer user_data); +static void on_user_manager_loaded (ActUserManager * manager, GParamSpec * pspect, gpointer user_data); +static void security_privacy_ready (GObject * obj, GAsyncResult * res, gpointer user_data); + +G_DEFINE_TYPE (ImAccountsService, im_accounts_service, G_TYPE_OBJECT); + +static void +im_accounts_service_class_init (ImAccountsServiceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (klass, sizeof (ImAccountsServicePrivate)); + + object_class->dispose = im_accounts_service_dispose; + object_class->finalize = im_accounts_service_finalize; +} + +static void +im_accounts_service_init (ImAccountsService *self) +{ + ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(self); + + priv->cancel = g_cancellable_new(); + + priv->user_manager = act_user_manager_get_default(); + g_signal_connect(priv->user_manager, "user-changed", G_CALLBACK(user_changed), self); + g_signal_connect(priv->user_manager, "notify::is-loaded", G_CALLBACK(on_user_manager_loaded), self); + + gboolean isLoaded = FALSE; + g_object_get(G_OBJECT(priv->user_manager), "is-loaded", &isLoaded, NULL); + if (isLoaded) { + on_user_manager_loaded(priv->user_manager, NULL, NULL); + } +} + +static void +im_accounts_service_dispose (GObject *object) +{ + ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(object); + + if (priv->cancel != NULL) { + g_cancellable_cancel(priv->cancel); + g_clear_object(&priv->cancel); + } + + g_clear_object(&priv->user_manager); + + G_OBJECT_CLASS (im_accounts_service_parent_class)->dispose (object); +} + +static void +im_accounts_service_finalize (GObject *object) +{ + G_OBJECT_CLASS (im_accounts_service_parent_class)->finalize (object); +} + +/* Handles a User getting updated */ +static void +user_changed (ActUserManager * manager, ActUser * user, gpointer user_data) +{ + if (g_strcmp0(act_user_get_user_name(user), g_get_user_name()) != 0) { + return; + } + + ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(user_data); + g_debug("User Updated"); + + /* Clear old proxies */ + g_clear_object(&priv->touch_settings); + + /* Start getting a new proxy */ + g_dbus_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + NULL, + "org.freedesktop.Accounts", + act_user_get_object_path(user), + "com.ubuntu.touch.AccountsService.SecurityPrivacy", + priv->cancel, + security_privacy_ready, + user_data); +} + +/* Respond to the async of setting up the proxy. Mostly we get it or we error. */ +static void +security_privacy_ready (GObject * obj, GAsyncResult * res, gpointer user_data) +{ + GError * error = NULL; + GDBusProxy * proxy = g_dbus_proxy_new_for_bus_finish(res, &error); + + if (error != NULL) { + g_warning("Unable to get a proxy on accounts service for touch settings: %s", error->message); + g_error_free(error); + return; + } + + ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(user_data); + /* Ensure we didn't get a proxy while we weren't looking */ + g_clear_object(&priv->touch_settings); + priv->touch_settings = proxy; +} + +/* When the user manager is loaded see if we have a user already loaded + along with. */ +static void +on_user_manager_loaded (ActUserManager * manager, GParamSpec * pspect, gpointer user_data) +{ + ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(user_data); + ActUser * user = NULL; + + g_debug("Accounts Manager Loaded"); + + user = act_user_manager_get_user(priv->user_manager, g_get_user_name()); + if (user != NULL) { + user_changed(priv->user_manager, user, user_data); + g_object_unref(user); + } +} + +/* Not the most testable way to do this but, it is a less invasive one, and we'll + probably restructure this codebase soonish */ +/* Gets an account service wrapper reference, so then it can be free'd */ +ImAccountsService * +im_accounts_service_ref_default (void) +{ + static ImAccountsService * as = NULL; + if (as == NULL) { + as = IM_ACCOUNTS_SERVICE(g_object_new(IM_ACCOUNTS_SERVICE_TYPE, NULL)); + g_object_add_weak_pointer(G_OBJECT(as), (gpointer *)&as); + return as; + } + + return g_object_ref(as); +} + +/* The draws attention setting is very legacy right now, we've patched and not changed + things much. We're gonna do better in the future, this function abstracts out the ugly */ +void +im_accounts_service_set_draws_attention (ImAccountsService * service, gboolean draws_attention) +{ + g_return_if_fail(IM_IS_ACCOUNTS_SERVICE(service)); + ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(service); + + if (priv->touch_settings == NULL) { + return; + } + + g_dbus_connection_call(g_dbus_proxy_get_connection(priv->touch_settings), + g_dbus_proxy_get_name(priv->touch_settings), + g_dbus_proxy_get_object_path(priv->touch_settings), + "org.freedesktop.Accounts.User", + "SetXHasMessages", + g_variant_new("(b)", draws_attention), + NULL, /* reply */ + G_DBUS_CALL_FLAGS_NONE, + -1, /* timeout */ + priv->cancel, /* cancellable */ + NULL, NULL); /* cb */ +} + +/* Looks at the property that is set by settings. We default to off in any case + that we can or we don't know what the state is. */ +gboolean +im_accounts_service_get_show_on_greeter (ImAccountsService * service) +{ + g_return_val_if_fail(IM_IS_ACCOUNTS_SERVICE(service), FALSE); + + ImAccountsServicePrivate * priv = IM_ACCOUNTS_SERVICE_GET_PRIVATE(service); + + if (priv->touch_settings == NULL) { + return FALSE; + } + + GVariant * val = g_dbus_proxy_get_cached_property(priv->touch_settings, "MessagesWelcomeScreen"); + if (val == NULL) { + return FALSE; + } + + gboolean retval = g_variant_get_boolean(val); + g_variant_unref(val); + return retval; +} diff --git a/src/im-accounts-service.h b/src/im-accounts-service.h new file mode 100644 index 0000000..d7611d8 --- /dev/null +++ b/src/im-accounts-service.h @@ -0,0 +1,53 @@ +/* + * 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: + * Ted Gould <ted@canonical.com> + */ + +#ifndef __IM_ACCOUNTS_SERVICE_H__ +#define __IM_ACCOUNTS_SERVICE_H__ + +#include <glib.h> +#include <glib-object.h> + +G_BEGIN_DECLS + +#define IM_ACCOUNTS_SERVICE_TYPE (im_accounts_service_get_type ()) +#define IM_ACCOUNTS_SERVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), IM_ACCOUNTS_SERVICE_TYPE, ImAccountsService)) +#define IM_ACCOUNTS_SERVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), IM_ACCOUNTS_SERVICE_TYPE, ImAccountsServiceClass)) +#define IM_IS_ACCOUNTS_SERVICE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), IM_ACCOUNTS_SERVICE_TYPE)) +#define IM_IS_ACCOUNTS_SERVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), IM_ACCOUNTS_SERVICE_TYPE)) +#define IM_ACCOUNTS_SERVICE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), IM_ACCOUNTS_SERVICE_TYPE, ImAccountsServiceClass)) + +typedef struct _ImAccountsService ImAccountsService; +typedef struct _ImAccountsServiceClass ImAccountsServiceClass; + +struct _ImAccountsServiceClass { + GObjectClass parent_class; +}; + +struct _ImAccountsService { + GObject parent; +}; + +GType im_accounts_service_get_type (void); +ImAccountsService * im_accounts_service_ref_default (void); +void im_accounts_service_set_draws_attention (ImAccountsService * service, gboolean draws_attention); +gboolean im_accounts_service_get_show_on_greeter (ImAccountsService * service); + +G_END_DECLS + +#endif diff --git a/src/im-application-list.c b/src/im-application-list.c index 931e9ad..36b2ff3 100644 --- a/src/im-application-list.c +++ b/src/im-application-list.c @@ -22,6 +22,7 @@ #include "indicator-messages-application.h" #include "gactionmuxer.h" #include "indicator-desktop-shortcuts.h" +#include "im-accounts-service.h" #include <gio/gdesktopappinfo.h> #include <string.h> @@ -41,6 +42,8 @@ struct _ImApplicationList GSimpleAction * statusaction; GHashTable *app_status; + + ImAccountsService * as; }; G_DEFINE_TYPE (ImApplicationList, im_application_list, G_TYPE_OBJECT); @@ -131,6 +134,62 @@ _g_action_group_has_actions (GActionGroup * ag) return retval; } +static gchar * +escape_action_name (const gchar *name) +{ + static const gchar *xdigits = "0123456789abcdef"; + GString *escaped; + gchar c; + + g_return_val_if_fail (name != NULL, NULL); + + escaped = g_string_new (NULL); + while ((c = *name++)) + { + if (g_ascii_isalnum (c) || c == '.') + { + g_string_append_c (escaped, c); + } + else + { + g_string_append_c (escaped, '-'); + g_string_append_c (escaped, xdigits[c >> 4]); + g_string_append_c (escaped, xdigits[c & 0xf]); + } + } + + return g_string_free (escaped, FALSE); +} + +static gchar * +unescape_action_name (const gchar *name) +{ + GString *unescaped; + gint i; + + g_return_val_if_fail (name != NULL, NULL); + + unescaped = g_string_new (NULL); + for (i = 0; name[i]; i++) + { + gint one, two; + + if (name[i] == '-' && + (one = g_ascii_xdigit_value (name[i + 1])) >= 0 && + (two = g_ascii_xdigit_value (name[i + 2])) >= 0) + { + g_string_append_c (unescaped, (one << 4) | two); + i += 2; + } + else + { + g_string_append_c (unescaped, name[i]); + } + } + + return g_string_free (unescaped, FALSE); +} + /* Check to see if either of our action groups has any actions, if so return TRUE so we get chosen! */ static gboolean @@ -170,9 +229,11 @@ im_application_list_update_root_action (ImApplicationList *list) if (g_hash_table_find (list->applications, application_draws_attention, NULL)) { base_icon_name = "indicator-messages-new-%s"; accessible_name = _("New Messages"); + im_accounts_service_set_draws_attention(list->as, TRUE); } else { base_icon_name = "indicator-messages-%s"; accessible_name = _("Messages"); + im_accounts_service_set_draws_attention(list->as, FALSE); } /* Include the IM state in the icon */ @@ -284,18 +345,30 @@ application_update_draws_attention (Application * app) return was_drawing_attention != app->draws_attention; } +static void +im_application_list_source_removed_action (Application *app, + const gchar *action_name) +{ + g_action_map_remove_action (G_ACTION_MAP(app->source_actions), action_name); + g_signal_emit (app->list, signals[SOURCE_REMOVED], 0, app->id, action_name); + + if (application_update_draws_attention(app)) + im_application_list_update_root_action (app->list); +} + /* Remove a source from an application, signal up and update the status of the draws attention flag. */ static void im_application_list_source_removed (Application *app, const gchar *id) { - g_action_map_remove_action (G_ACTION_MAP(app->source_actions), id); + gchar *action_name; - g_signal_emit (app->list, signals[SOURCE_REMOVED], 0, app->id, id); + action_name = escape_action_name (id); - if (application_update_draws_attention(app)) - im_application_list_update_root_action (app->list); + im_application_list_source_removed_action (app, action_name); + + g_free (action_name); } static void @@ -304,9 +377,11 @@ im_application_list_source_activated (GSimpleAction *action, gpointer user_data) { Application *app = user_data; - const gchar *source_id; + const gchar *action_name; + gchar *source_id; - source_id = g_action_get_name (G_ACTION (action)); + action_name = g_action_get_name (G_ACTION (action)); + source_id = unescape_action_name (action_name); if (g_variant_get_boolean (parameter)) { @@ -323,20 +398,35 @@ im_application_list_source_activated (GSimpleAction *action, app->cancellable, NULL, NULL); } - im_application_list_source_removed (app, source_id); + im_application_list_source_removed_action (app, action_name); + + g_free (source_id); } static void -im_application_list_message_removed (Application *app, - const gchar *id) +im_application_list_message_removed_action (Application *app, + const gchar *action_name) { - g_action_map_remove_action (G_ACTION_MAP(app->message_actions), id); - g_action_muxer_remove (app->message_sub_actions, id); + g_action_map_remove_action (G_ACTION_MAP(app->message_actions), action_name); + g_action_muxer_remove (app->message_sub_actions, action_name); if (application_update_draws_attention(app)) im_application_list_update_root_action (app->list); - g_signal_emit (app->list, signals[MESSAGE_REMOVED], 0, app->id, id); + g_signal_emit (app->list, signals[MESSAGE_REMOVED], 0, app->id, action_name); +} + +static void +im_application_list_message_removed (Application *app, + const gchar *id) +{ + gchar *action_name; + + action_name = escape_action_name (id); + + im_application_list_message_removed_action(app, action_name); + + g_free (action_name); } static void @@ -345,9 +435,11 @@ im_application_list_message_activated (GSimpleAction *action, gpointer user_data) { Application *app = user_data; - const gchar *message_id; + const gchar *action_name; + gchar *message_id; - message_id = g_action_get_name (G_ACTION (action)); + action_name = g_action_get_name (G_ACTION (action)); + message_id = unescape_action_name (action_name); if (g_variant_get_boolean (parameter)) { @@ -366,7 +458,9 @@ im_application_list_message_activated (GSimpleAction *action, app->cancellable, NULL, NULL); } - im_application_list_message_removed (app, message_id); + im_application_list_message_removed_action (app, action_name); + + g_free (message_id); } static void @@ -376,11 +470,11 @@ im_application_list_sub_message_activated (GSimpleAction *action, { Application *app = user_data; const gchar *message_id; - const gchar *action_id; + gchar *action_id; GVariantBuilder builder; message_id = g_object_get_data (G_OBJECT (action), "message"); - action_id = g_action_get_name (G_ACTION (action)); + action_id = unescape_action_name (g_action_get_name (G_ACTION (action))); g_variant_builder_init (&builder, G_VARIANT_TYPE ("av")); if (parameter) @@ -394,6 +488,8 @@ im_application_list_sub_message_activated (GSimpleAction *action, NULL, NULL); im_application_list_message_removed (app, message_id); + + g_free (action_id); } static void @@ -418,11 +514,11 @@ im_application_list_remove_all (GSimpleAction *action, source_actions = g_action_group_list_actions (G_ACTION_GROUP (app->source_actions)); for (it = source_actions; *it; it++) - im_application_list_source_removed (app, *it); + im_application_list_source_removed_action (app, *it); message_actions = g_action_group_list_actions (G_ACTION_GROUP (app->message_actions)); for (it = message_actions; *it; it++) - im_application_list_message_removed (app, *it); + im_application_list_message_removed_action (app, *it); if (app->proxy != NULL) /* If it is remote, we tell the app we've cleared */ { @@ -451,6 +547,8 @@ im_application_list_dispose (GObject *object) g_clear_pointer (&list->applications, g_hash_table_unref); g_clear_object (&list->muxer); + g_clear_object (&list->as); + G_OBJECT_CLASS (im_application_list_parent_class)->dispose (object); } @@ -602,6 +700,8 @@ im_application_list_init (ImApplicationList *list) list->muxer = g_action_muxer_new (); g_action_muxer_insert (list->muxer, NULL, G_ACTION_GROUP (list->globalactions)); + list->as = im_accounts_service_ref_default(); + im_application_list_update_root_action (list); } @@ -792,6 +892,7 @@ im_application_list_source_added (Application *app, GVariant *serialized_icon = NULL; GVariant *state; GSimpleAction *action; + gchar *action_name; g_variant_get (source, "(&s&s@avux&sb)", &id, &label, &maybe_serialized_icon, &count, &time, &string, &draws_attention); @@ -802,12 +903,13 @@ im_application_list_source_added (Application *app, visible = count > 0 || time != 0 || (string != NULL && string[0] != '\0'); state = g_variant_new ("(uxsb)", count, time, string, draws_attention); - action = g_simple_action_new_stateful (id, G_VARIANT_TYPE_BOOLEAN, state); + action_name = escape_action_name (id); + action = g_simple_action_new_stateful (action_name, G_VARIANT_TYPE_BOOLEAN, state); g_signal_connect (action, "activate", G_CALLBACK (im_application_list_source_activated), app); g_action_map_add_action (G_ACTION_MAP(app->source_actions), G_ACTION (action)); - g_signal_emit (app->list, signals[SOURCE_ADDED], 0, app->id, id, label, serialized_icon, visible); + g_signal_emit (app->list, signals[SOURCE_ADDED], 0, app->id, action_name, label, serialized_icon, visible); if (visible && draws_attention && app->draws_attention == FALSE) { @@ -815,6 +917,7 @@ im_application_list_source_added (Application *app, im_application_list_update_root_action (app->list); } + g_free (action_name); g_object_unref (action); if (serialized_icon) g_variant_unref (serialized_icon); @@ -834,6 +937,7 @@ im_application_list_source_changed (Application *app, gboolean draws_attention; GVariant *serialized_icon = NULL; gboolean visible; + gchar *action_name; g_variant_get (source, "(&s&s@avux&sb)", &id, &label, &maybe_serialized_icon, &count, &time, &string, &draws_attention); @@ -841,12 +945,14 @@ im_application_list_source_changed (Application *app, if (g_variant_n_children (maybe_serialized_icon) == 1) g_variant_get_child (maybe_serialized_icon, 0, "v", &serialized_icon); - g_action_group_change_action_state (G_ACTION_GROUP (app->source_actions), id, + action_name = escape_action_name (id); + + g_action_group_change_action_state (G_ACTION_GROUP (app->source_actions), action_name, g_variant_new ("(uxsb)", count, time, string, draws_attention)); visible = count > 0 || time != 0 || (string != NULL && string[0] != '\0'); - g_signal_emit (app->list, signals[SOURCE_CHANGED], 0, app->id, id, label, serialized_icon, visible); + g_signal_emit (app->list, signals[SOURCE_CHANGED], 0, app->id, action_name, label, serialized_icon, visible); if (application_update_draws_attention (app)) im_application_list_update_root_action (app->list); @@ -854,6 +960,7 @@ im_application_list_source_changed (Application *app, if (serialized_icon) g_variant_unref (serialized_icon); g_variant_unref (maybe_serialized_icon); + g_free (action_name); } static void @@ -958,6 +1065,7 @@ im_application_list_message_added (Application *app, GSimpleAction *action; GIcon *app_icon; GVariant *actions = NULL; + gchar *action_name; g_variant_get (message, "(&s@av&s&s&sxaa{sv}b)", &id, &maybe_serialized_icon, &title, &subtitle, &body, &time, &action_iter, &draws_attention); @@ -965,7 +1073,8 @@ im_application_list_message_added (Application *app, if (g_variant_n_children (maybe_serialized_icon) == 1) g_variant_get_child (maybe_serialized_icon, 0, "v", &serialized_icon); - action = g_simple_action_new (id, G_VARIANT_TYPE_BOOLEAN); + action_name = escape_action_name (id); + action = g_simple_action_new (action_name, G_VARIANT_TYPE_BOOLEAN); g_object_set_qdata(G_OBJECT(action), message_action_draws_attention_quark(), GINT_TO_POINTER(draws_attention)); g_signal_connect (action, "activate", G_CALLBACK (im_application_list_message_activated), app); g_action_map_add_action (G_ACTION_MAP(app->message_actions), G_ACTION (action)); @@ -987,6 +1096,7 @@ im_application_list_message_added (Application *app, GVariant *hint; GVariantBuilder dict_builder; gchar *prefixed_name; + gchar *escaped_name; if (!g_variant_lookup (entry, "name", "&s", &name)) { @@ -998,14 +1108,15 @@ im_application_list_message_added (Application *app, g_variant_lookup (entry, "parameter-type", "&g", &type); hint = g_variant_lookup_value (entry, "parameter-hint", NULL); - action = g_simple_action_new (name, type ? G_VARIANT_TYPE (type) : NULL); + escaped_name = escape_action_name (name); + action = g_simple_action_new (escaped_name, type ? G_VARIANT_TYPE (type) : NULL); g_object_set_data_full (G_OBJECT (action), "message", g_strdup (id), g_free); g_signal_connect (action, "activate", G_CALLBACK (im_application_list_sub_message_activated), app); g_action_map_add_action (G_ACTION_MAP(action_group), G_ACTION (action)); g_variant_builder_init (&dict_builder, G_VARIANT_TYPE ("a{sv}")); - prefixed_name = g_strjoin (".", app->id, "msg-actions", id, name, NULL); + prefixed_name = g_strjoin (".", app->id, "msg-actions", action_name, escaped_name, NULL); g_variant_builder_add (&dict_builder, "{sv}", "name", g_variant_new_string (prefixed_name)); if (label) @@ -1028,9 +1139,10 @@ im_application_list_message_added (Application *app, g_object_unref (action); g_variant_unref (entry); g_free (prefixed_name); + g_free (escaped_name); } - g_action_muxer_insert (app->message_sub_actions, id, G_ACTION_GROUP (action_group)); + g_action_muxer_insert (app->message_sub_actions, action_name, G_ACTION_GROUP (action_group)); actions = g_variant_builder_end (&actions_builder); g_object_unref (action_group); @@ -1045,9 +1157,10 @@ im_application_list_message_added (Application *app, app_icon = get_symbolic_app_icon (app->info); g_signal_emit (app->list, signals[MESSAGE_ADDED], 0, - app->id, app_icon, id, serialized_icon, title, + app->id, app_icon, action_name, serialized_icon, title, subtitle, body, actions, time, draws_attention); + g_free (action_name); g_variant_iter_free (action_iter); g_object_unref (action); if (serialized_icon) diff --git a/src/im-desktop-menu.c b/src/im-desktop-menu.c index 137e222..c762735 100644 --- a/src/im-desktop-menu.c +++ b/src/im-desktop-menu.c @@ -81,12 +81,15 @@ g_app_info_is_default_for_uri_scheme (GAppInfo *info, const gchar *uri_scheme) { GAppInfo *default_info; - gboolean is_default; + gboolean is_default = FALSE; default_info = g_app_info_get_default_for_uri_scheme (uri_scheme); - is_default = g_app_info_equal (info, default_info); + if (default_info) + { + is_default = g_app_info_equal (info, default_info); + g_object_unref (default_info); + } - g_object_unref (default_info); return is_default; } diff --git a/src/im-menu.c b/src/im-menu.c index 55d4685..0c39b97 100644 --- a/src/im-menu.c +++ b/src/im-menu.c @@ -18,12 +18,15 @@ */ #include "im-menu.h" +#include "im-accounts-service.h" struct _ImMenuPrivate { GMenu *toplevel_menu; GMenu *menu; ImApplicationList *applist; + gboolean on_greeter; + ImAccountsService *as; }; G_DEFINE_TYPE_WITH_PRIVATE (ImMenu, im_menu, G_TYPE_OBJECT) @@ -32,6 +35,7 @@ enum { PROP_0, PROP_APPLICATION_LIST, + PROP_ON_GREETER, NUM_PROPERTIES }; @@ -43,6 +47,7 @@ im_menu_finalize (GObject *object) g_object_unref (priv->toplevel_menu); g_object_unref (priv->menu); g_object_unref (priv->applist); + g_object_unref (priv->as); G_OBJECT_CLASS (im_menu_parent_class)->finalize (object); } @@ -60,6 +65,9 @@ im_menu_get_property (GObject *object, case PROP_APPLICATION_LIST: g_value_set_object (value, priv->applist); break; + case PROP_ON_GREETER: + g_value_set_boolean (value, priv->on_greeter); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); @@ -79,6 +87,9 @@ im_menu_set_property (GObject *object, case PROP_APPLICATION_LIST: /* construct only */ priv->applist = g_value_dup_object (value); break; + case PROP_ON_GREETER: + priv->on_greeter = g_value_get_boolean (value); + break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); @@ -100,6 +111,12 @@ im_menu_class_init (ImMenuClass *class) G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (object_class, PROP_ON_GREETER, + g_param_spec_boolean ("on-greeter", "", "", + FALSE, + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS)); } static void @@ -110,6 +127,8 @@ im_menu_init (ImMenu *menu) priv->toplevel_menu = g_menu_new (); priv->menu = g_menu_new (); + priv->on_greeter = FALSE; + priv->as = im_accounts_service_ref_default(); root = g_menu_item_new (NULL, "indicator.messages"); g_menu_item_set_attribute (root, "x-canonical-type", "s", "com.canonical.indicator.root"); @@ -225,3 +244,17 @@ im_menu_insert_item_sorted (ImMenu *menu, g_menu_insert_item (priv->menu, position, item); } + +/* Whether the menu should show extra data on it. Depends on the greeter + status and user settings */ +gboolean +im_menu_show_data (ImMenu *menu) +{ + g_return_val_if_fail (IM_IS_MENU (menu), FALSE); + ImMenuPrivate *priv = im_menu_get_instance_private (IM_MENU (menu)); + + if (!priv->on_greeter) + return TRUE; + + return im_accounts_service_get_show_on_greeter(priv->as); +} diff --git a/src/im-menu.h b/src/im-menu.h index f67abc8..b76d616 100644 --- a/src/im-menu.h +++ b/src/im-menu.h @@ -64,4 +64,6 @@ void im_menu_insert_item_sorted (ImMenu gint first, gint last); +gboolean im_menu_show_data (ImMenu *menu); + #endif diff --git a/src/im-phone-menu.c b/src/im-phone-menu.c index 754fc2b..58a23ff 100644 --- a/src/im-phone-menu.c +++ b/src/im-phone-menu.c @@ -142,12 +142,13 @@ im_phone_menu_init (ImPhoneMenu *menu) } ImPhoneMenu * -im_phone_menu_new (ImApplicationList *applist) +im_phone_menu_new (ImApplicationList *applist, gboolean greeter) { g_return_val_if_fail (IM_IS_APPLICATION_LIST (applist), NULL); return g_object_new (IM_TYPE_PHONE_MENU, "application-list", applist, + "on-greeter", greeter, NULL); } @@ -179,10 +180,12 @@ im_phone_menu_add_message (ImPhoneMenu *menu, gint n_messages; gint pos; GVariant *serialized_app_icon; + gboolean show_data; g_return_if_fail (IM_IS_PHONE_MENU (menu)); g_return_if_fail (app_id); + show_data = im_menu_show_data(IM_MENU (menu)); action_name = g_strconcat (app_id, ".msg.", id, NULL); item = g_menu_item_new (title, NULL); @@ -190,8 +193,10 @@ im_phone_menu_add_message (ImPhoneMenu *menu, g_menu_item_set_attribute (item, "x-canonical-type", "s", "com.canonical.indicator.messages.messageitem"); g_menu_item_set_attribute (item, "x-canonical-message-id", "s", id); - g_menu_item_set_attribute (item, "x-canonical-subtitle", "s", subtitle); - g_menu_item_set_attribute (item, "x-canonical-text", "s", body); + if (show_data) + g_menu_item_set_attribute (item, "x-canonical-subtitle", "s", subtitle); + if (show_data) + g_menu_item_set_attribute (item, "x-canonical-text", "s", body); g_menu_item_set_attribute (item, "x-canonical-time", "x", time); if (serialized_icon) @@ -203,7 +208,7 @@ im_phone_menu_add_message (ImPhoneMenu *menu, g_variant_unref (serialized_app_icon); } - if (actions) + if (actions && show_data) g_menu_item_set_attribute (item, "x-canonical-message-actions", "v", actions); n_messages = g_menu_model_get_n_items (G_MENU_MODEL (menu->message_section)); diff --git a/src/im-phone-menu.h b/src/im-phone-menu.h index 4f96c8c..813634d 100644 --- a/src/im-phone-menu.h +++ b/src/im-phone-menu.h @@ -33,7 +33,8 @@ typedef struct _ImPhoneMenu ImPhoneMenu; GType im_phone_menu_get_type (void); -ImPhoneMenu * im_phone_menu_new (ImApplicationList *applist); +ImPhoneMenu * im_phone_menu_new (ImApplicationList *applist, + gboolean greeter); void im_phone_menu_add_message (ImPhoneMenu *menu, const gchar *app_id, diff --git a/src/messages-service.c b/src/messages-service.c index d1ccbbc..d2c7e92 100644 --- a/src/messages-service.c +++ b/src/messages-service.c @@ -273,8 +273,10 @@ main (int argc, char ** argv) } menus = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_object_unref); - g_hash_table_insert (menus, "phone", im_phone_menu_new (applications)); + g_hash_table_insert (menus, "phone", im_phone_menu_new (applications, FALSE)); + g_hash_table_insert (menus, "phone_greeter", im_phone_menu_new (applications, TRUE)); g_hash_table_insert (menus, "desktop", im_desktop_menu_new (applications)); + g_hash_table_insert (menus, "desktop_greeter", im_desktop_menu_new (applications)); g_unix_signal_add(SIGTERM, sig_term_handler, mainloop); diff --git a/test/applications/test.desktop b/test/applications/test.desktop deleted file mode 100644 index c2332b9..0000000 --- a/test/applications/test.desktop +++ /dev/null @@ -1,2 +0,0 @@ -[Desktop Entry] -Type=Application diff --git a/test/Makefile.am b/tests/Makefile.am index b679615..bd559fd 100644 --- a/test/Makefile.am +++ b/tests/Makefile.am @@ -1,5 +1,6 @@ -check_LIBRARIES = libgtest.a +CLEANFILES= +check_LTLIBRARIES = libgtest.la check_PROGRAMS = test-gactionmuxer TESTS = $(check_PROGRAMS) @@ -7,15 +8,22 @@ TESTS = $(check_PROGRAMS) AM_CPPFLAGS = $(GTEST_CPPFLAGS) \ -I${top_srcdir}/src -nodist_libgtest_a_SOURCES = \ +###################################### +# Google Test +###################################### + +nodist_libgtest_la_SOURCES = \ $(GTEST_SOURCE)/src/gtest-all.cc \ $(GTEST_SOURCE)/src/gtest_main.cc -libgtest_a_CPPFLAGS = \ +libgtest_la_CPPFLAGS = \ $(GTEST_CPPFLAGS) -w \ $(AM_CPPFLAGS) -libgtest_a_CXXFLAGS = \ +libgtest_la_CXXFLAGS = \ $(AM_CXXFLAGS) +###################################### +# GActionMixer +###################################### test_gactionmuxer_SOURCES = \ test-gactionmuxer.cpp @@ -27,8 +35,46 @@ test_gactionmuxer_CPPFLAGS = \ test_gactionmuxer_LDADD = \ $(APPLET_LIBS) \ libindicator-messages-service.la \ - libgtest.a + libgtest.la + +###################################### +# Indicator Test +###################################### + +SCHEMA_COMPILED_DIR="$(abs_builddir)/gsettings-schemas-compiled/" + +indicator_test_SOURCES = \ + indicator-test.cpp + +indicator_test_CPPFLAGS = \ + -DINDICATOR_MESSAGES_SERVICE_BINARY="\"$(abs_top_builddir)/src/indicator-messages-service\"" \ + -DSCHEMA_DIR="\"$(SCHEMA_COMPILED_DIR)\"" \ + -DXDG_DATA_DIRS="\"$(abs_srcdir)/\"" \ + -I$(top_srcdir)/libmessaging-menu \ + -std=c++11 \ + $(APPLET_CFLAGS) \ + $(DBUSTEST_CFLAGS) \ + $(AM_CPPFLAGS) + +indicator_test_LDADD = \ + $(APPLET_LIBS) \ + $(DBUSTEST_LIBS) \ + $(top_builddir)/libmessaging-menu/libmessaging-menu.la \ + libgtest.la \ + -lc -lpthread + +indicator-test.cpp: schemas-compiled.stamp + +schemas-compiled.stamp: $(top_srcdir)/data/*gschema.xml + @rm -rf $(SCHEMA_COMPILED_DIR) + @mkdir -p $(SCHEMA_COMPILED_DIR) + @cp -f $(top_srcdir)/data/*gschema.xml $(SCHEMA_COMPILED_DIR) + @`pkg-config gio-2.0 --variable glib_compile_schemas` $(SCHEMA_COMPILED_DIR) + @touch schemas-compiled.stamp +CLEANFILES += schemas-compiled.stamp +TESTS += indicator-test +check_PROGRAMS += indicator-test ###################################### # Lib containing code under test @@ -41,7 +87,7 @@ libindicator_messages_service_la_SOURCES = \ $(top_builddir)/common/indicator-messages-service.c \ $(top_builddir)/common/indicator-messages-service.h \ $(top_srcdir)/src/gactionmuxer.c \ - $(top_srcdir)/src/gactionmuxer.h + $(top_srcdir)/src/gactionmuxer.h \ $(top_srcdir)/src/dbus-data.h libindicator_messages_service_ladir = \ diff --git a/tests/accounts-service-mock.h b/tests/accounts-service-mock.h new file mode 100644 index 0000000..301fcd4 --- /dev/null +++ b/tests/accounts-service-mock.h @@ -0,0 +1,134 @@ +/* + * 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 as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY 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: + * Ted Gould <ted@canonical.com> + */ + +#include <memory> +#include <libdbustest/dbus-test.h> + +class AccountsServiceMock +{ + DbusTestDbusMock * mock = nullptr; + DbusTestDbusMockObject * soundobj = nullptr; + DbusTestDbusMockObject * userobj = nullptr; + DbusTestDbusMockObject * syssoundobj = nullptr; + DbusTestDbusMockObject * privacyobj = nullptr; + + public: + AccountsServiceMock () { + mock = dbus_test_dbus_mock_new("org.freedesktop.Accounts"); + + dbus_test_task_set_bus(DBUS_TEST_TASK(mock), DBUS_TEST_SERVICE_BUS_SYSTEM); + + DbusTestDbusMockObject * baseobj = dbus_test_dbus_mock_get_object(mock, "/org/freedesktop/Accounts", "org.freedesktop.Accounts", NULL); + + dbus_test_dbus_mock_object_add_method(mock, baseobj, + "CacheUser", G_VARIANT_TYPE_STRING, G_VARIANT_TYPE_OBJECT_PATH, + "ret = dbus.ObjectPath('/user')\n", NULL); + dbus_test_dbus_mock_object_add_method(mock, baseobj, + "FindUserById", G_VARIANT_TYPE_INT64, G_VARIANT_TYPE_OBJECT_PATH, + "ret = dbus.ObjectPath('/user')\n", NULL); + dbus_test_dbus_mock_object_add_method(mock, baseobj, + "FindUserByName", G_VARIANT_TYPE_STRING, G_VARIANT_TYPE_OBJECT_PATH, + "ret = dbus.ObjectPath('/user')\n", NULL); + dbus_test_dbus_mock_object_add_method(mock, baseobj, + "ListCachedUsers", NULL, G_VARIANT_TYPE_OBJECT_PATH_ARRAY, + "ret = [ dbus.ObjectPath('/user') ]\n", NULL); + dbus_test_dbus_mock_object_add_method(mock, baseobj, + "UncacheUser", G_VARIANT_TYPE_STRING, NULL, + "", NULL); + + userobj = dbus_test_dbus_mock_get_object(mock, "/user", "org.freedesktop.Accounts.User", NULL); + dbus_test_dbus_mock_object_add_property(mock, userobj, + "UserName", G_VARIANT_TYPE_STRING, + g_variant_new_string(g_get_user_name()), NULL); + dbus_test_dbus_mock_object_add_method(mock, baseobj, + "SetXHasMessages", G_VARIANT_TYPE_BOOLEAN, nullptr, + "", NULL); + + soundobj = dbus_test_dbus_mock_get_object(mock, "/user", "com.canonical.indicator.sound.AccountsService", NULL); + dbus_test_dbus_mock_object_add_property(mock, soundobj, + "Timestamp", G_VARIANT_TYPE_UINT64, + g_variant_new_uint64(0), NULL); + dbus_test_dbus_mock_object_add_property(mock, soundobj, + "PlayerName", G_VARIANT_TYPE_STRING, + g_variant_new_string(""), NULL); + dbus_test_dbus_mock_object_add_property(mock, soundobj, + "PlayerIcon", G_VARIANT_TYPE_VARIANT, + g_variant_new_variant(g_variant_new_string("")), NULL); + dbus_test_dbus_mock_object_add_property(mock, soundobj, + "Running", G_VARIANT_TYPE_BOOLEAN, + g_variant_new_boolean(FALSE), NULL); + dbus_test_dbus_mock_object_add_property(mock, soundobj, + "State", G_VARIANT_TYPE_STRING, + g_variant_new_string(""), NULL); + dbus_test_dbus_mock_object_add_property(mock, soundobj, + "Title", G_VARIANT_TYPE_STRING, + g_variant_new_string(""), NULL); + dbus_test_dbus_mock_object_add_property(mock, soundobj, + "Artist", G_VARIANT_TYPE_STRING, + g_variant_new_string(""), NULL); + dbus_test_dbus_mock_object_add_property(mock, soundobj, + "Album", G_VARIANT_TYPE_STRING, + g_variant_new_string(""), NULL); + dbus_test_dbus_mock_object_add_property(mock, soundobj, + "ArtUrl", G_VARIANT_TYPE_STRING, + g_variant_new_string(""), NULL); + + syssoundobj = dbus_test_dbus_mock_get_object(mock, "/user", "com.ubuntu.touch.AccountsService.Sound", NULL); + dbus_test_dbus_mock_object_add_property(mock, syssoundobj, + "SilentMode", G_VARIANT_TYPE_BOOLEAN, + g_variant_new_boolean(FALSE), NULL); + + privacyobj = dbus_test_dbus_mock_get_object(mock, "/user", "com.ubuntu.touch.AccountsService.SecurityPrivacy", NULL); + dbus_test_dbus_mock_object_add_property(mock, privacyobj, + "MessagesWelcomeScreen", G_VARIANT_TYPE_BOOLEAN, + g_variant_new_boolean(true), NULL); + dbus_test_dbus_mock_object_add_property(mock, privacyobj, + "StatsWelcomeScreen", G_VARIANT_TYPE_BOOLEAN, + g_variant_new_boolean(true), NULL); + } + + ~AccountsServiceMock () { + g_debug("Destroying the Accounts Service Mock"); + g_clear_object(&mock); + } + + void setSilentMode (bool modeValue) { + dbus_test_dbus_mock_object_update_property(mock, syssoundobj, + "SilentMode", g_variant_new_boolean(modeValue ? TRUE : FALSE), + NULL); + } + + operator std::shared_ptr<DbusTestTask> () { + return std::shared_ptr<DbusTestTask>( + DBUS_TEST_TASK(g_object_ref(mock)), + [](DbusTestTask * task) { g_clear_object(&task); }); + } + + operator DbusTestTask* () { + return DBUS_TEST_TASK(mock); + } + + operator DbusTestDbusMock* () { + return mock; + } + + DbusTestDbusMockObject * get_sound () { + return soundobj; + } +}; diff --git a/tests/applications/test.desktop b/tests/applications/test.desktop new file mode 100644 index 0000000..63ccb6b --- /dev/null +++ b/tests/applications/test.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Type=Application +Name=Test +Exec=test +Icon=test-app diff --git a/tests/applications/test2.desktop b/tests/applications/test2.desktop new file mode 100644 index 0000000..63ccb6b --- /dev/null +++ b/tests/applications/test2.desktop @@ -0,0 +1,5 @@ +[Desktop Entry] +Type=Application +Name=Test +Exec=test +Icon=test-app diff --git a/tests/indicator-fixture.h b/tests/indicator-fixture.h new file mode 100644 index 0000000..e2c4316 --- /dev/null +++ b/tests/indicator-fixture.h @@ -0,0 +1,688 @@ +/* + * 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 as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY 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: + * Ted Gould <ted@canonical.com> + */ + +#include <memory> +#include <algorithm> +#include <string> +#include <functional> +#include <future> + +#include <gtest/gtest.h> +#include <gio/gio.h> +#include <libdbustest/dbus-test.h> + +class IndicatorFixture : public ::testing::Test +{ + private: + std::string _indicatorPath; + std::string _indicatorAddress; + std::vector<std::shared_ptr<DbusTestTask>> _mocks; + protected: + std::chrono::milliseconds _eventuallyTime; + + private: + class PerRunData { + public: + /* We're private in the fixture but other than that we don't care, + we don't leak out. This object's purpose isn't to hide data it is + to make the lifecycle of the items more clear. */ + std::shared_ptr<GMenuModel> _menu; + std::shared_ptr<GActionGroup> _actions; + DbusTestService * _session_service; + DbusTestService * _system_service; + DbusTestTask * _test_indicator; + DbusTestTask * _test_dummy; + GDBusConnection * _session; + GDBusConnection * _system; + + PerRunData (const std::string& indicatorPath, const std::string& indicatorAddress, std::vector<std::shared_ptr<DbusTestTask>>& mocks) + : _menu(nullptr) + , _session(nullptr) + { + _session_service = dbus_test_service_new(nullptr); + dbus_test_service_set_bus(_session_service, DBUS_TEST_SERVICE_BUS_SESSION); + + _system_service = dbus_test_service_new(nullptr); + dbus_test_service_set_bus(_system_service, DBUS_TEST_SERVICE_BUS_SYSTEM); + + _test_indicator = DBUS_TEST_TASK(dbus_test_process_new(indicatorPath.c_str())); + dbus_test_task_set_name(_test_indicator, "Indicator"); + dbus_test_service_add_task(_session_service, _test_indicator); + + _test_dummy = dbus_test_task_new(); + dbus_test_task_set_wait_for(_test_dummy, indicatorAddress.c_str()); + dbus_test_task_set_name(_test_dummy, "Dummy"); + dbus_test_service_add_task(_session_service, _test_dummy); + + for(auto task : mocks) { + if (dbus_test_task_get_bus(task.get()) == DBUS_TEST_SERVICE_BUS_SYSTEM) { + dbus_test_service_add_task(_system_service, task.get()); + } else { + dbus_test_service_add_task(_session_service, task.get()); + } + } + + g_debug("Starting System Bus"); + dbus_test_service_start_tasks(_system_service); + _system = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr); + g_dbus_connection_set_exit_on_close(_system, FALSE); + + g_debug("Starting Session Bus"); + dbus_test_service_start_tasks(_session_service); + _session = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, nullptr); + g_dbus_connection_set_exit_on_close(_session, FALSE); + } + + virtual ~PerRunData (void) { + _menu.reset(); + _actions.reset(); + + /* D-Bus Test Stuff */ + g_clear_object(&_test_dummy); + g_clear_object(&_test_indicator); + g_clear_object(&_session_service); + g_clear_object(&_system_service); + + /* Wait for D-Bus session bus to go */ + if (!g_dbus_connection_is_closed(_session)) { + g_dbus_connection_close_sync(_session, nullptr, nullptr); + } + g_clear_object(&_session); + + if (!g_dbus_connection_is_closed(_system)) { + g_dbus_connection_close_sync(_system, nullptr, nullptr); + } + g_clear_object(&_system); + } + }; + + std::shared_ptr<PerRunData> run; + + public: + virtual ~IndicatorFixture() = default; + + IndicatorFixture (const std::string& path, + const std::string& addr) + : _indicatorPath(path) + , _indicatorAddress(addr) + , _eventuallyTime(std::chrono::seconds(5)) + { + }; + + + protected: + virtual void SetUp() override + { + run = std::make_shared<PerRunData>(_indicatorPath, _indicatorAddress, _mocks); + + _mocks.clear(); + } + + virtual void TearDown() override + { + run.reset(); + } + + void addMock (std::shared_ptr<DbusTestTask> mock) + { + _mocks.push_back(mock); + } + + std::shared_ptr<DbusTestTask> buildBustleMock (const std::string& filename, DbusTestServiceBus bus = DBUS_TEST_SERVICE_BUS_BOTH) + { + return std::shared_ptr<DbusTestTask>([filename, bus]() { + DbusTestTask * bustle = DBUS_TEST_TASK(dbus_test_bustle_new(filename.c_str())); + dbus_test_task_set_name(bustle, "Bustle"); + dbus_test_task_set_bus(bustle, bus); + return bustle; + }(), [](DbusTestTask * bustle) { + g_clear_object(&bustle); + }); + } + + private: + void waitForCore (GObject * obj, const gchar * signalname) { + auto loop = g_main_loop_new(nullptr, FALSE); + + /* Our two exit criteria */ + gulong signal = g_signal_connect_swapped(obj, signalname, G_CALLBACK(g_main_loop_quit), loop); + guint timer = g_timeout_add_seconds(5, [](gpointer user_data) -> gboolean { + g_warning("Menu Timeout"); + g_main_loop_quit((GMainLoop *)user_data); + return G_SOURCE_CONTINUE; + }, loop); + + /* Wait for sync */ + g_main_loop_run(loop); + + /* Clean up */ + g_source_remove(timer); + g_signal_handler_disconnect(obj, signal); + + g_main_loop_unref(loop); + } + + void menuWaitForItems (const std::shared_ptr<GMenuModel>& menu) { + auto count = g_menu_model_get_n_items(menu.get()); + + if (count != 0) + return; + + waitForCore(G_OBJECT(menu.get()), "items-changed"); + } + + void agWaitForActions (const std::shared_ptr<GActionGroup>& group) { + auto list = std::shared_ptr<gchar *>( + g_action_group_list_actions(group.get()), + [](gchar ** list) { + g_strfreev(list); + }); + + if (g_strv_length(list.get()) != 0) { + return; + } + + waitForCore(G_OBJECT(group.get()), "action-added"); + } + + testing::AssertionResult expectEventually (std::function<testing::AssertionResult(void)> &testfunc) { + auto loop = std::shared_ptr<GMainLoop>(g_main_loop_new(nullptr, FALSE), [](GMainLoop * loop) { if (loop != nullptr) g_main_loop_unref(loop); }); + + std::promise<testing::AssertionResult> retpromise; + auto retfuture = retpromise.get_future(); + auto start = std::chrono::steady_clock::now(); + + /* The core of the idle function as an object so we can use the C++-isms + of attaching the variables and make this code reasonably readable */ + std::function<void(void)> idlefunc = [&loop, &retpromise, &testfunc, &start, this]() -> void { + auto result = testfunc(); + + if (result == false && _eventuallyTime > (std::chrono::steady_clock::now() - start)) { + return; + } + + retpromise.set_value(result); + g_main_loop_quit(loop.get()); + }; + + auto idlesrc = g_idle_add([](gpointer data) -> gboolean { + auto func = reinterpret_cast<std::function<void(void)> *>(data); + (*func)(); + return G_SOURCE_CONTINUE; + }, &idlefunc); + + g_main_loop_run(loop.get()); + g_source_remove(idlesrc); + + return retfuture.get(); + } + + protected: + void setMenu (const std::string& path) { + run->_menu.reset(); + + g_debug("Getting Menu: %s:%s", _indicatorAddress.c_str(), path.c_str()); + run->_menu = std::shared_ptr<GMenuModel>(G_MENU_MODEL(g_dbus_menu_model_get(run->_session, _indicatorAddress.c_str(), path.c_str())), [](GMenuModel * modelptr) { + g_clear_object(&modelptr); + }); + + menuWaitForItems(run->_menu); + } + + void setActions (const std::string& path) { + run->_actions.reset(); + + run->_actions = std::shared_ptr<GActionGroup>(G_ACTION_GROUP(g_dbus_action_group_get(run->_session, _indicatorAddress.c_str(), path.c_str())), [](GActionGroup * groupptr) { + g_clear_object(&groupptr); + }); + + agWaitForActions(run->_actions); + } + + void activateAction (const std::string &name, std::shared_ptr<GVariant> ¶meter) { + g_action_group_activate_action(run->_actions.get(), name.c_str(), parameter.get()); + } + + void activateAction (const std::string &name, GVariant * parameter = nullptr) { + std::shared_ptr<GVariant> param; + + if (parameter != nullptr) + param = std::shared_ptr<GVariant>(g_variant_ref_sink(parameter), [](GVariant * var) { + g_variant_unref(var); + }); + + return activateAction(name, param); + } + + testing::AssertionResult expectActionExists (const gchar * nameStr, const std::string& name) { + bool hasit = g_action_group_has_action(run->_actions.get(), name.c_str()); + + if (!hasit) { + auto result = testing::AssertionFailure(); + result << + " Action: " << nameStr << std::endl << + " Expected: " << "Exists" << std::endl << + " Actual: " << "No action found" << std::endl; + + return result; + } + + auto result = testing::AssertionSuccess(); + return result; + } + + template <typename... Args> testing::AssertionResult expectEventuallyActionExists (Args&& ... args) { + std::function<testing::AssertionResult(void)> func = [&]() { + return expectActionExists(std::forward<Args>(args)...); + }; + return expectEventually(func); + } + + testing::AssertionResult expectActionDoesNotExist (const gchar * nameStr, const std::string& name) { + bool hasit = g_action_group_has_action(run->_actions.get(), name.c_str()); + + if (hasit) { + auto result = testing::AssertionFailure(); + result << + " Action: " << nameStr << std::endl << + " Expected: " << "No action found" << std::endl << + " Actual: " << "Exists" << std::endl; + + return result; + } + + auto result = testing::AssertionSuccess(); + return result; + } + + template <typename... Args> testing::AssertionResult expectEventuallyActionDoesNotExist (Args&& ... args) { + std::function<testing::AssertionResult(void)> func = [&]() { + return expectActionDoesNotExist(std::forward<Args>(args)...); + }; + return expectEventually(func); + } + + testing::AssertionResult expectActionEnabled (const char * nameStr, const char * typeStr, const std::string& name, bool enabled) { + auto aenabled = g_action_group_get_action_enabled(run->_actions.get(), name.c_str()); + + if (enabled != aenabled) { + auto result = testing::AssertionFailure(); + result << + " Action: " << nameStr << std::endl << + " Expected: " << enabled << std::endl << + " Actual: " << aenabled << std::endl; + + return result; + } + + auto result = testing::AssertionSuccess(); + return result; + } + + template <typename... Args> testing::AssertionResult expectEventuallyActionEnabled (Args&& ... args) { + std::function<testing::AssertionResult(void)> func = [&]() { + return expectActionEnabled(std::forward<Args>(args)...); + }; + return expectEventually(func); + } + + testing::AssertionResult expectActionStateType (const char * nameStr, const char * typeStr, const std::string& name, const GVariantType * type) { + auto atype = g_action_group_get_action_state_type(run->_actions.get(), name.c_str()); + bool same = false; + + if (atype != nullptr) { + same = g_variant_type_equal(atype, type); + } + + if (!same) { + auto result = testing::AssertionFailure(); + result << + " Action: " << nameStr << std::endl << + " Expected: " << typeStr << std::endl << + " Actual: " << (atype == nullptr ? "(null)" : g_variant_type_peek_string(atype)) << std::endl; + + return result; + } + + auto result = testing::AssertionSuccess(); + return result; + } + + template <typename... Args> testing::AssertionResult expectEventuallyActionStateType (Args&& ... args) { + std::function<testing::AssertionResult(void)> func = [&]() { + return expectActionStateType(std::forward<Args>(args)...); + }; + return expectEventually(func); + } + + testing::AssertionResult expectActionActivationType (const char * nameStr, const char * typeStr, const std::string& name, const GVariantType * type) { + auto atype = g_action_group_get_action_parameter_type(run->_actions.get(), name.c_str()); + bool same = false; + + if (atype != nullptr) { + same = g_variant_type_equal(atype, type); + } + + if (!same) { + auto result = testing::AssertionFailure(); + result << + " Action: " << nameStr << std::endl << + " Expected: " << typeStr << std::endl << + " Actual: " << (atype == nullptr ? "(null)" : g_variant_type_peek_string(atype)) << std::endl; + + return result; + } + + auto result = testing::AssertionSuccess(); + return result; + } + + template <typename... Args> testing::AssertionResult expectEventuallyActionActivationType (Args&& ... args) { + std::function<testing::AssertionResult(void)> func = [&]() { + return expectActionActivationType(std::forward<Args>(args)...); + }; + return expectEventually(func); + } + + testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, std::shared_ptr<GVariant> varref) { + auto aval = std::shared_ptr<GVariant>(g_action_group_get_action_state(run->_actions.get(), name.c_str()), [] (GVariant * varptr) { + if (varptr != nullptr) + g_variant_unref(varptr); + }); + bool match = false; + + if (aval != nullptr) { + match = g_variant_equal(aval.get(), varref.get()); + } + + if (!match) { + gchar * attstr = nullptr; + + if (aval != nullptr) { + attstr = g_variant_print(aval.get(), TRUE); + } else { + attstr = g_strdup("nullptr"); + } + + auto result = testing::AssertionFailure(); + result << + " Action: " << nameStr << std::endl << + " Expected: " << valueStr << std::endl << + " Actual: " << attstr << std::endl; + + g_free(attstr); + + return result; + } else { + auto result = testing::AssertionSuccess(); + return result; + } + } + + testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, GVariant * value) { + auto varref = std::shared_ptr<GVariant>(g_variant_ref_sink(value), [](GVariant * varptr) { + if (varptr != nullptr) + g_variant_unref(varptr); + }); + return expectActionStateIs(nameStr, valueStr, name, varref); + } + + testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, bool value) { + GVariant * var = g_variant_new_boolean(value); + return expectActionStateIs(nameStr, valueStr, name, var); + } + + testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, std::string value) { + GVariant * var = g_variant_new_string(value.c_str()); + return expectActionStateIs(nameStr, valueStr, name, var); + } + + testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, const char * value) { + GVariant * var = g_variant_new_string(value); + return expectActionStateIs(nameStr, valueStr, name, var); + } + + testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, double value) { + GVariant * var = g_variant_new_double(value); + return expectActionStateIs(nameStr, valueStr, name, var); + } + + testing::AssertionResult expectActionStateIs (const char * nameStr, const char * valueStr, const std::string& name, float value) { + GVariant * var = g_variant_new_double(value); + return expectActionStateIs(nameStr, valueStr, name, var); + } + + template <typename... Args> testing::AssertionResult expectEventuallyActionStateIs (Args&& ... args) { + std::function<testing::AssertionResult(void)> func = [&]() { + return expectActionStateIs(std::forward<Args>(args)...); + }; + return expectEventually(func); + } + + + private: + std::shared_ptr<GVariant> getMenuAttributeVal (int location, std::shared_ptr<GMenuModel>& menu, const std::string& attribute, std::shared_ptr<GVariant>& value) { + if (!(location < g_menu_model_get_n_items(menu.get()))) { + return nullptr; + } + + if (location >= g_menu_model_get_n_items(menu.get())) + return nullptr; + + auto menuval = std::shared_ptr<GVariant>(g_menu_model_get_item_attribute_value(menu.get(), location, attribute.c_str(), g_variant_get_type(value.get())), [](GVariant * varptr) { + if (varptr != nullptr) + g_variant_unref(varptr); + }); + + return menuval; + } + + std::shared_ptr<GVariant> getMenuAttributeRecurse (std::vector<int>::const_iterator menuLocation, std::vector<int>::const_iterator menuEnd, const std::string& attribute, std::shared_ptr<GVariant>& value, std::shared_ptr<GMenuModel>& menu) { + if (menuLocation == menuEnd) + return nullptr; + + if (menuLocation + 1 == menuEnd) + return getMenuAttributeVal(*menuLocation, menu, attribute, value); + + auto clearfunc = [](GMenuModel * modelptr) { + g_clear_object(&modelptr); + }; + + auto submenu = std::shared_ptr<GMenuModel>(g_menu_model_get_item_link(menu.get(), *menuLocation, G_MENU_LINK_SUBMENU), clearfunc); + + if (submenu == nullptr) + submenu = std::shared_ptr<GMenuModel>(g_menu_model_get_item_link(menu.get(), *menuLocation, G_MENU_LINK_SECTION), clearfunc); + + if (submenu == nullptr) + return nullptr; + + menuWaitForItems(submenu); + + return getMenuAttributeRecurse(menuLocation + 1, menuEnd, attribute, value, submenu); + } + + protected: + testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, GVariant * value) { + auto varref = std::shared_ptr<GVariant>(g_variant_ref_sink(value), [](GVariant * varptr) { + if (varptr != nullptr) + g_variant_unref(varptr); + }); + + auto attrib = getMenuAttributeRecurse(menuLocation.cbegin(), menuLocation.cend(), attribute, varref, run->_menu); + bool same = false; + + if (attrib != nullptr && varref != nullptr) { + same = g_variant_equal(attrib.get(), varref.get()); + } + + if (!same) { + gchar * attstr = nullptr; + + if (attrib != nullptr) { + attstr = g_variant_print(attrib.get(), TRUE); + } else { + attstr = g_strdup("nullptr"); + } + + auto result = testing::AssertionFailure(); + result << + " Menu: " << menuLocationStr << std::endl << + " Attribute: " << attributeStr << std::endl << + " Expected: " << valueStr << std::endl << + " Actual: " << attstr << std::endl; + + g_free(attstr); + + return result; + } else { + auto result = testing::AssertionSuccess(); + return result; + } + } + + testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, bool value) { + GVariant * var = g_variant_new_boolean(value); + return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var); + } + + testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, std::string value) { + GVariant * var = g_variant_new_string(value.c_str()); + return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var); + } + + testing::AssertionResult expectMenuAttribute (const char * menuLocationStr, const char * attributeStr, const char * valueStr, const std::vector<int> menuLocation, const std::string& attribute, const char * value) { + GVariant * var = g_variant_new_string(value); + return expectMenuAttribute(menuLocationStr, attributeStr, valueStr, menuLocation, attribute, var); + } + + template <typename... Args> testing::AssertionResult expectEventuallyMenuAttribute (Args&& ... args) { + std::function<testing::AssertionResult(void)> func = [&]() { + return expectMenuAttribute(std::forward<Args>(args)...); + }; + return expectEventually(func); + } + + /* Eventually Helpers */ + #define _EVENTUALLY_HELPER(oper) \ + template <typename... Args> testing::AssertionResult expectEventually##oper (Args&& ... args) { \ + std::function<testing::AssertionResult(void)> func = [&]() { \ + return testing::internal::CmpHelper##oper(std::forward<Args>(args)...); \ + }; \ + return expectEventually(func); \ + } + + _EVENTUALLY_HELPER(EQ); + _EVENTUALLY_HELPER(NE); + _EVENTUALLY_HELPER(LT); + _EVENTUALLY_HELPER(GT); + _EVENTUALLY_HELPER(STREQ); + _EVENTUALLY_HELPER(STRNE); + + #undef _EVENTUALLY_HELPER +}; + +/* Menu Attrib */ +#define ASSERT_MENU_ATTRIB(menu, attrib, value) \ + ASSERT_PRED_FORMAT3(IndicatorFixture::expectMenuAttribute, menu, attrib, value) + +#define EXPECT_MENU_ATTRIB(menu, attrib, value) \ + EXPECT_PRED_FORMAT3(IndicatorFixture::expectMenuAttribute, menu, attrib, value) + +#define EXPECT_EVENTUALLY_MENU_ATTRIB(menu, attrib, value) \ + EXPECT_PRED_FORMAT3(IndicatorFixture::expectEventuallyMenuAttribute, menu, attrib, value) + +/* Action Exists */ +#define ASSERT_ACTION_EXISTS(action) \ + ASSERT_PRED_FORMAT1(IndicatorFixture::expectActionExists, action) + +#define EXPECT_ACTION_EXISTS(action) \ + EXPECT_PRED_FORMAT1(IndicatorFixture::expectActionExists, action) + +#define EXPECT_EVENTUALLY_ACTION_EXISTS(action) \ + EXPECT_PRED_FORMAT1(IndicatorFixture::expectEventuallyActionExists, action) + +/* Action Does Not Exist */ +#define ASSERT_ACTION_DOES_NOT_EXIST(action) \ + ASSERT_PRED_FORMAT1(IndicatorFixture::expectActionDoesNotExist, action) + +#define EXPECT_ACTION_DOES_NOT_EXIST(action) \ + EXPECT_PRED_FORMAT1(IndicatorFixture::expectActionDoesNotExist, action) + +#define EXPECT_EVENTUALLY_ACTION_DOES_NOT_EXIST(action) \ + EXPECT_PRED_FORMAT1(IndicatorFixture::expectEventuallyActionDoesNotExist, action) + +/* Action Enabled */ +#define ASSERT_ACTION_ENABLED(action, state) \ + ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionEnabled, action, state) + +#define EXPECT_ACTION_ENABLED(action, state) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionEnabled, action, state) + +#define EXPECT_EVENTUALLY_ACTION_ENABLED(action, state) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyActionEnabled, action, state) + +/* Action State */ +#define ASSERT_ACTION_STATE(action, value) \ + ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionStateIs, action, value) + +#define EXPECT_ACTION_STATE(action, value) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionStateIs, action, value) + +#define EXPECT_EVENTUALLY_ACTION_STATE(action, value) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyActionStateIs, action, value) + +/* Action State Type */ +#define ASSERT_ACTION_STATE_TYPE(action, type) \ + ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionStateType, action, type) + +#define EXPECT_ACTION_STATE_TYPE(action, type) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionStateType, action, type) + +#define EXPECT_EVENTUALLY_ACTION_STATE_TYPE(action, type) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyActionStateType, action, type) + +/* Action Activation Type */ +#define ASSERT_ACTION_ACTIVATION_TYPE(action, type) \ + ASSERT_PRED_FORMAT2(IndicatorFixture::expectActionActivationType, action, type) + +#define EXPECT_ACTION_ACTIVATION_TYPE(action, type) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectActionActivationType, action, type) + +#define EXPECT_EVENTUALLY_ACTION_ACTIVATION_TYPE(action, type) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyActionActivationType, action, type) + +/* Helpers */ + +#define EXPECT_EVENTUALLY_EQ(expected, actual) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyEQ, expected, actual) + +#define EXPECT_EVENTUALLY_NE(expected, actual) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyNE, expected, actual) + +#define EXPECT_EVENTUALLY_LT(expected, actual) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyLT, expected, actual) + +#define EXPECT_EVENTUALLY_GT(expected, actual) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallyGT, expected, actual) + +#define EXPECT_EVENTUALLY_STREQ(expected, actual) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallySTREQ, expected, actual) + +#define EXPECT_EVENTUALLY_STRNE(expected, actual) \ + EXPECT_PRED_FORMAT2(IndicatorFixture::expectEventuallySTRNE, expected, actual) diff --git a/test/indicator-messages-service-activate.build.sh b/tests/indicator-messages-service-activate.build.sh index 87a0316..87a0316 100644 --- a/test/indicator-messages-service-activate.build.sh +++ b/tests/indicator-messages-service-activate.build.sh diff --git a/test/indicator-messages-service-activate.c b/tests/indicator-messages-service-activate.c index f5a26b0..f5a26b0 100644 --- a/test/indicator-messages-service-activate.c +++ b/tests/indicator-messages-service-activate.c diff --git a/tests/indicator-test.cpp b/tests/indicator-test.cpp new file mode 100644 index 0000000..0991db5 --- /dev/null +++ b/tests/indicator-test.cpp @@ -0,0 +1,209 @@ +/* + * Copyright © 2015 Canonical Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY 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: + * Ted Gould <ted@canonical.com> + */ + +#include <gtest/gtest.h> +#include <gio/gio.h> + +#include "indicator-fixture.h" +#include "accounts-service-mock.h" + +#include "messaging-menu-app.h" +#include "messaging-menu-message.h" + +class IndicatorTest : public IndicatorFixture +{ +protected: + IndicatorTest (void) : + IndicatorFixture(INDICATOR_MESSAGES_SERVICE_BINARY, "com.canonical.indicator.messages") + { + } + + std::shared_ptr<AccountsServiceMock> as; + + virtual void SetUp() override + { + g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, TRUE); + g_setenv("GSETTINGS_BACKEND", "memory", TRUE); + + g_setenv("XDG_DATA_DIRS", XDG_DATA_DIRS, TRUE); + + as = std::make_shared<AccountsServiceMock>(); + addMock(*as); + + IndicatorFixture::SetUp(); + } + + virtual void TearDown() override + { + as.reset(); + + IndicatorFixture::TearDown(); + } + +}; + + +TEST_F(IndicatorTest, RootAction) { + setActions("/com/canonical/indicator/messages"); + + EXPECT_EVENTUALLY_ACTION_EXISTS("messages"); + EXPECT_ACTION_STATE_TYPE("messages", G_VARIANT_TYPE("a{sv}")); + EXPECT_ACTION_STATE("messages", g_variant_new_parsed("{'icon': <('themed', <['indicator-messages-offline', 'indicator-messages', 'indicator']>)>, 'title': <'Notifications'>, 'accessible-desc': <'Messages'>, 'visible': <false>}")); +} + +TEST_F(IndicatorTest, SingleMessage) { + setActions("/com/canonical/indicator/messages"); + + auto app = std::shared_ptr<MessagingMenuApp>(messaging_menu_app_new("test.desktop"), [](MessagingMenuApp * app) { g_clear_object(&app); }); + ASSERT_NE(nullptr, app); + messaging_menu_app_register(app.get()); + + EXPECT_EVENTUALLY_ACTION_EXISTS("test.launch"); + + auto msg = std::shared_ptr<MessagingMenuMessage>(messaging_menu_message_new( + "testid", + nullptr, /* no icon */ + "Test Title", + "A subtitle too", + "You only like me for my body", + 0), [](MessagingMenuMessage * msg) { g_clear_object(&msg); }); + messaging_menu_app_append_message(app.get(), msg.get(), nullptr, FALSE); + + EXPECT_EVENTUALLY_ACTION_EXISTS("test.msg.testid"); + + setMenu("/com/canonical/indicator/messages/phone"); + + EXPECT_EVENTUALLY_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "x-canonical-type", "com.canonical.indicator.messages.messageitem"); + EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "label", "Test Title"); + EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "x-canonical-message-id", "testid"); + EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "x-canonical-subtitle", "A subtitle too"); + EXPECT_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "x-canonical-text", "You only like me for my body"); +} + +static void +messageReplyActivate (GObject * obj, gchar * name, GVariant * value, gpointer user_data) { + auto res = reinterpret_cast<std::string *>(user_data); + *res = g_variant_get_string(value, nullptr); +} + +TEST_F(IndicatorTest, MessageReply) { + setActions("/com/canonical/indicator/messages"); + + auto app = std::shared_ptr<MessagingMenuApp>(messaging_menu_app_new("test.desktop"), [](MessagingMenuApp * app) { g_clear_object(&app); }); + ASSERT_NE(nullptr, app); + messaging_menu_app_register(app.get()); + + EXPECT_EVENTUALLY_ACTION_EXISTS("test.launch"); + + auto msg = std::shared_ptr<MessagingMenuMessage>(messaging_menu_message_new( + "messageid", + nullptr, /* no icon */ + "Reply Message", + "A message to reply to", + "In-app replies are for wimps, reply here to save yourself time and be cool.", + 0), [](MessagingMenuMessage * msg) { g_clear_object(&msg); }); + messaging_menu_message_add_action(msg.get(), + "replyid", + "Reply", + G_VARIANT_TYPE_STRING, + nullptr); + messaging_menu_app_append_message(app.get(), msg.get(), nullptr, FALSE); + + EXPECT_EVENTUALLY_ACTION_EXISTS("test.msg.messageid"); + EXPECT_EVENTUALLY_ACTION_EXISTS("test.msg-actions.messageid.replyid"); + EXPECT_ACTION_ACTIVATION_TYPE("test.msg-actions.messageid.replyid", G_VARIANT_TYPE_STRING); + + EXPECT_ACTION_ENABLED("remove-all", true); + + setMenu("/com/canonical/indicator/messages/phone"); + + EXPECT_EVENTUALLY_MENU_ATTRIB(std::vector<int>({0, 0, 0}), "x-canonical-type", "com.canonical.indicator.messages.messageitem"); + + std::string activateResponse; + g_signal_connect(msg.get(), "activate", G_CALLBACK(messageReplyActivate), &activateResponse); + + activateAction("test.msg-actions.messageid.replyid", g_variant_new_string("Reply to me")); + + EXPECT_EVENTUALLY_EQ("Reply to me", activateResponse); + + EXPECT_EVENTUALLY_ACTION_ENABLED("remove-all", false); +} + +TEST_F(IndicatorTest, IconNotification) { + auto normalicon = std::shared_ptr<GVariant>(g_variant_ref_sink(g_variant_new_parsed("{'icon': <('themed', <['indicator-messages-offline', 'indicator-messages', 'indicator']>)>, 'title': <'Notifications'>, 'accessible-desc': <'Messages'>, 'visible': <true>}")), [](GVariant *var) {if (var != nullptr) g_variant_unref(var); }); + auto blueicon = std::shared_ptr<GVariant>(g_variant_ref_sink(g_variant_new_parsed("{'icon': <('themed', <['indicator-messages-new-offline', 'indicator-messages-new', 'indicator-messages', 'indicator']>)>, 'title': <'Notifications'>, 'accessible-desc': <'New Messages'>, 'visible': <true>}")), [](GVariant *var) {if (var != nullptr) g_variant_unref(var); }); + + setActions("/com/canonical/indicator/messages"); + + auto app = std::shared_ptr<MessagingMenuApp>(messaging_menu_app_new("test.desktop"), [](MessagingMenuApp * app) { g_clear_object(&app); }); + ASSERT_NE(nullptr, app); + messaging_menu_app_register(app.get()); + + EXPECT_EVENTUALLY_ACTION_EXISTS("test.launch"); + + EXPECT_ACTION_STATE("messages", normalicon); + + auto app2 = std::shared_ptr<MessagingMenuApp>(messaging_menu_app_new("test2.desktop"), [](MessagingMenuApp * app) { g_clear_object(&app); }); + ASSERT_NE(nullptr, app2); + messaging_menu_app_register(app2.get()); + + EXPECT_EVENTUALLY_ACTION_EXISTS("test2.launch"); + + messaging_menu_app_append_source_with_count(app2.get(), + "countsource", + nullptr, + "Count Source", + 500); + messaging_menu_app_draw_attention(app2.get(), "countsource"); + + EXPECT_EVENTUALLY_ACTION_STATE("messages", blueicon); + + auto msg = std::shared_ptr<MessagingMenuMessage>(messaging_menu_message_new( + "messageid", + nullptr, /* no icon */ + "Message", + "A secret message", + "asdfa;lkweraoweprijas;dvlknasvdoiewur;aslkd", + 0), [](MessagingMenuMessage * msg) { g_clear_object(&msg); }); + messaging_menu_message_set_draws_attention(msg.get(), true); + messaging_menu_app_append_message(app.get(), msg.get(), nullptr, FALSE); + + EXPECT_EVENTUALLY_ACTION_EXISTS("test.msg.messageid"); + EXPECT_ACTION_STATE("messages", blueicon); + + messaging_menu_app_unregister(app2.get()); + app2.reset(); + + EXPECT_EVENTUALLY_ACTION_DOES_NOT_EXIST("test2.msg.countsource"); + EXPECT_ACTION_STATE("messages", blueicon); + + messaging_menu_app_remove_message(app.get(), msg.get()); + + EXPECT_EVENTUALLY_ACTION_STATE("messages", normalicon); + EXPECT_ACTION_ENABLED("remove-all", false); + + messaging_menu_app_append_message(app.get(), msg.get(), nullptr, FALSE); + + EXPECT_EVENTUALLY_ACTION_STATE("messages", blueicon); + EXPECT_ACTION_ENABLED("remove-all", true); + + activateAction("remove-all"); + + EXPECT_EVENTUALLY_ACTION_STATE("messages", normalicon); +} diff --git a/tests/manual b/tests/manual index ed930a9..d638875 100644 --- a/tests/manual +++ b/tests/manual @@ -31,3 +31,50 @@ Test-case indicator-messages/unity8-phone-symbolic-icon <dt>Verify the application icon in the menu item is monochromatic</dt> <dd>On the right side of the item the application icon should have no color</dd> </dl> + +Test-case indicator-messages/unity8-embedded-greeter +<dl> + <dt>NOTE: Only works with embedded greeter, split greeter will require modifications to this test</dt> + <dt>NOTE: Only works on a device that can receive SMS messages</dt> + <dt>Ensure System Settings is set to "Show Messages on Greeter"</dt> + <dt>Send an SMS to the device</dt> + <dd>The notification icon should change color</dd> + <dd>There should be an entry in the messaging menu with the SMS message</dd> + <dd>The item should include the sender and the start of the message</dd> + <dt>Go to the greeter. This can be done by hitting the lock button twice.</dt> + <dt>Ensure the messaging menu has the message</dt> + <dd>The notification icon should have color</dd> + <dd>There should be an entry in the messaging menu with the SMS message</dd> + <dd>The item should include the sender and the start of the message</dd> + <dt>Clear the message in the greeter</dt> + <dd>The message should no longer be in the messaging menu</dd> + <dt>Disable System Settings value "Show Messages on Greeter"</dt> + <dt>Send an SMS to the device</dt> + <dd>The notification icon should change color</dd> + <dd>There should be an entry in the messaging menu with the SMS message</dd> + <dd>The item should include the sender and the start of the message</dd> + <dt>Go to the greeter. This can be done by hitting the lock button twice.</dt> + <dt>Ensure the messaging menu has the message, but it does not include the start of the message</dt> + <dd>The notification icon should have color</dd> + <dd>There should be an entry in the messaging menu with the SMS message</dd> + <dd>The item should include the sender but NOT the start of the message</dd> + <dt>Clear the message in the greeter</dt> + <dd>The message should no longer be in the messaging menu</dd> +</dl> + +Test-case indicator-messages/push-message-twitter +<dl> + <dt>From a shell prompt send a simultated Twitter push notification</dt> + <dd>gdbus call --session --dest com.ubuntu.Postal --object-path /com/ubuntu/Postal/com_2eubuntu_2edeveloper_2ewebapps_2ewebapp_2dtwitter --method com.ubuntu.Postal.Post com.ubuntu.developer.webapps.webapp-twitter_webapp-twitter '"{\"message\": \"foobar\", \"notification\":{\"card\": {\"summary\": \"yes\", \"body\": \"hello\", \"popup\": true, \"persist\": true}}}"'</dd> + <dd>The messaging envelope on the panel should change to highlight a message</dd> + <dt>Open the messaging menu</dt> + <dd>The menu should contain an entry with the Twitter icon for the application</dd> + <dd>The title of the message should be 'yes'</dd> + <dd>The body of the message should be 'hello'</dd> + <dd>At the bottom of them menu there should be a 'Clear All' menu item</dd> + <dt>Clear the message using the 'Clear All' command</dt> + <dd>The Twitter message should disappear</dd> + <dd>The 'Clear All' item should disappear</dd> + <dd>The icon in the panel should return to its original state</dd> +</dl> + diff --git a/test/test-client.py b/tests/test-client.py index 0dbf868..0dbf868 100755 --- a/test/test-client.py +++ b/tests/test-client.py diff --git a/test/test-gactionmuxer.cpp b/tests/test-gactionmuxer.cpp index 5c98c90..5c98c90 100644 --- a/test/test-gactionmuxer.cpp +++ b/tests/test-gactionmuxer.cpp |