Mon, 23 Oct 2023 22:08:37 -0500
Create PurpleIdleManager for managing idle states
Testing Done:
Ran the unit tests
Bugs closed: PIDGIN-17818
Reviewed at https://reviews.imfreedom.org/r/2586/
--- a/libpurple/core.c Sun Oct 22 20:45:40 2023 -0500 +++ b/libpurple/core.c Mon Oct 23 22:08:37 2023 -0500 @@ -43,6 +43,7 @@ #include "purpleconversation.h" #include "purplecredentialmanager.h" #include "purplehistorymanager.h" +#include "purpleidlemanagerprivate.h" #include "purplemessage.h" #include "purplepath.h" #include "purpleprivate.h" @@ -174,6 +175,7 @@ purple_proxy_init(); purple_xfers_init(); purple_idle_init(); + purple_idle_manager_startup(); /* * Call this early on to try to auto-detect our IP address and @@ -231,6 +233,7 @@ /* Save .xml files, remove signals, etc. */ purple_idle_uninit(); + purple_idle_manager_shutdown(); purple_whiteboard_manager_shutdown(); purple_conversation_manager_shutdown(); purple_conversations_uninit();
--- a/libpurple/meson.build Sun Oct 22 20:45:40 2023 -0500 +++ b/libpurple/meson.build Mon Oct 23 22:08:37 2023 -0500 @@ -60,6 +60,7 @@ 'purplegio.c', 'purplehistoryadapter.c', 'purplehistorymanager.c', + 'purpleidlemanager.c', 'purpleidleui.c', 'purpleimconversation.c', 'purplekeyvaluepair.c', @@ -183,6 +184,7 @@ 'purplegio.h', 'purplehistoryadapter.h', 'purplehistorymanager.h', + 'purpleidlemanager.h', 'purpleidleui.h', 'purpleimconversation.h', 'purpleattachment.h', @@ -320,6 +322,10 @@ 'xmlnode.h' ] +purple_private_headers = [ + 'purpleidlemanagerprivate.h', + 'purpleprivate.h', +] enums = gnome.mkenums_simple('purpleenums', sources : purple_enumheaders, @@ -361,8 +367,8 @@ libpurple_inc = include_directories('.') libpurple = library('purple3', purple_coresources + purple_builtsources + - purple_builtheaders + purple_schemas, - 'purpleprivate.h', + purple_builtheaders + purple_schemas + + purple_private_headers, c_args : ['-DPURPLE_COMPILATION', '-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Purple"'], include_directories : [toplevel_inc, libpurple_inc], install : true,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purpleidlemanager.c Mon Oct 23 22:08:37 2023 -0500 @@ -0,0 +1,188 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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; either version 2 of the License, or + * (at your option) any later version. + * + * 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 <https://www.gnu.org/licenses/>. + */ + +#include "purpleidlemanager.h" +#include "purpleidlemanagerprivate.h" + +#include "util.h" + +enum { + PROP_0, + PROP_TIMESTAMP, + N_PROPERTIES, +}; +static GParamSpec *properties[N_PROPERTIES] = {NULL, }; + +struct _PurpleIdleManager { + GObject parent; + + GHashTable *sources; + char *active_source; +}; + +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +G_DEFINE_TYPE(PurpleIdleManager, purple_idle_manager, G_TYPE_OBJECT) + +static void +purple_idle_manager_finalize(GObject *obj) { + PurpleIdleManager *manager = PURPLE_IDLE_MANAGER(obj); + + g_clear_pointer(&manager->sources, g_hash_table_destroy); + g_clear_pointer(&manager->active_source, g_free); + + G_OBJECT_CLASS(purple_idle_manager_parent_class)->finalize(obj); +} + +static void +purple_idle_manager_get_property(GObject *obj, guint param_id, + GValue *value, GParamSpec *pspec) +{ + PurpleIdleManager *manager = PURPLE_IDLE_MANAGER(obj); + + switch(param_id) { + case PROP_TIMESTAMP: + g_value_set_boxed(value, purple_idle_manager_get_timestamp(manager)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec); + break; + } +} + +static void +purple_idle_manager_init(PurpleIdleManager *manager) { + manager->sources = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, + (GDestroyNotify)g_date_time_unref); +} + +static void +purple_idle_manager_class_init(PurpleIdleManagerClass *klass) { + GObjectClass *obj_class = G_OBJECT_CLASS(klass); + + obj_class->finalize = purple_idle_manager_finalize; + obj_class->get_property = purple_idle_manager_get_property; + + /** + * PurpleIdleManager::timestamp: + * + * The aggregate of the oldest idle timestamp of all of the sources that + * are known. + * + * Since: 3.0.0 + */ + properties[PROP_TIMESTAMP] = g_param_spec_boxed( + "timestamp", "timestamp", + "The aggregate of the oldest timestamp of all sources.", + G_TYPE_DATE_TIME, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties(obj_class, N_PROPERTIES, properties); +} + +/****************************************************************************** + * Private API + *****************************************************************************/ +static PurpleIdleManager *default_manager = NULL; + +void +purple_idle_manager_startup(void) { + if(!PURPLE_IS_IDLE_MANAGER(default_manager)) { + default_manager = g_object_new(PURPLE_TYPE_IDLE_MANAGER, NULL); + g_object_add_weak_pointer(G_OBJECT(default_manager), + (gpointer *)&default_manager); + } +} + +void +purple_idle_manager_shutdown(void) { + g_clear_object(&default_manager); +} + +/****************************************************************************** + * Public API + *****************************************************************************/ +PurpleIdleManager * +purple_idle_manager_get_default(void) { + return default_manager; +} + +gboolean +purple_idle_manager_set_source(PurpleIdleManager *manager, + const char *source, + GDateTime *timestamp) +{ + GHashTableIter iter; + GDateTime *oldest = NULL; + const char *new_source = NULL; + gpointer key; + gpointer value; + + g_return_val_if_fail(PURPLE_IS_IDLE_MANAGER(manager), FALSE); + g_return_val_if_fail(!purple_strempty(source), FALSE); + + /* We're adding/updating a source. */ + if(timestamp != NULL) { + g_hash_table_insert(manager->sources, g_strdup(source), + g_date_time_ref(timestamp)); + } else { + g_hash_table_remove(manager->sources, source); + } + + g_hash_table_iter_init(&iter, manager->sources); + while(g_hash_table_iter_next(&iter, &key, &value)) { + /* If we don't have an oldest yet, use this value. */ + if(oldest == NULL || g_date_time_compare(value, oldest) < 0) { + oldest = value; + new_source = key; + } + } + + /* Finally check if new_source matches old source. */ + if(!purple_strequal(new_source, manager->active_source)) { + /* We have to set the active source before emitting the property change + * otherwise purple_idle_manager_get_timestamp will look up the wrong + * value. + */ + g_free(manager->active_source); + manager->active_source = g_strdup(new_source); + + g_object_notify_by_pspec(G_OBJECT(manager), + properties[PROP_TIMESTAMP]); + + return TRUE; + } + + return FALSE; +} + +GDateTime * +purple_idle_manager_get_timestamp(PurpleIdleManager *manager) { + g_return_val_if_fail(PURPLE_IS_IDLE_MANAGER(manager), NULL); + + if(manager->active_source == NULL) { + return NULL; + } + + return g_hash_table_lookup(manager->sources, manager->active_source); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purpleidlemanager.h Mon Oct 23 22:08:37 2023 -0500 @@ -0,0 +1,122 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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; either version 2 of the License, or + * (at your option) any later version. + * + * 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 <https://www.gnu.org/licenses/>. + */ + +#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION) +# error "only <purple.h> may be included directly" +#endif + +#ifndef PURPLE_IDLE_MANAGER_H +#define PURPLE_IDLE_MANAGER_H + +#include <glib.h> +#include <glib-object.h> + +#define PURPLE_TYPE_IDLE_MANAGER (purple_idle_manager_get_type()) +G_DECLARE_FINAL_TYPE(PurpleIdleManager, purple_idle_manager, PURPLE, + IDLE_MANAGER, GObject) + +/** + * PurpleIdleManager: + * + * The idle manager keeps track of multiple idle sources and aggregates them + * to the oldest one to report a global idle state. + * + * Idle sources include application usage, device usage, or a manually set + * value, among other possibilities. User interfaces should allow users to + * determine what if any idle sources are tracked in the idle manager. + * + * The idle source with the oldest timestamp is used as the value for + * [property@IdleManager:timestamp] as it is most likely what the user is + * looking for based on the settings they would choose in the user interface. + * + * Most users will only ever have a single idle source, but could add an + * additional manual source to set a specific time that they went idle. + * + * If the user has chosen no idle reporting, which means no sources are ever + * added to [class@IdleManager], then [property@IdleManager:timestamp] will + * always be %NULL. + * + * Most users will choose between application and device usage. The difference + * being that application usage is updated whenever you send a message whereas + * device usage is only updated when you haven't interacted with your device. + * + * However, there is also the ability to manually set an idle time via plugins. + * Typically users will manually set their idle time to something exaggerated + * like months or years. + * + * A manual idle source could also be created to replicate an existing idle + * source like the application usage, so that the user can start using the + * application without resetting the idle time. This would in effect allow the + * user to use the application in "stealth mode" by remaining idle. + * + * In both of these examples, the user wishes to remain idle while still using + * the application. This is precisely why the oldest idle time is used as the + * aggregate. + * + * Since: 3.0.0 + */ + +G_BEGIN_DECLS + +/** + * purple_idle_manager_get_default: + * + * Gets the default idle manager that libpurple is using. + * + * Returns: (transfer none): The default idle manager. + * + * Since: 3.0.0 + */ +PurpleIdleManager *purple_idle_manager_get_default(void); + +/** + * purple_idle_manager_set_source: + * @manager: The instance. + * @source: The name of the source. + * @timestamp: (nullable): The new timestamp for @source. + * + * Sets the timestamp of when @source went idle to @timestamp. If @timestamp is + * %NULL, @source will be removed from @manager. + * + * Returns: %TRUE if [property@IdleManager:timestamp] has changed due to this + * call. + * + * Since: 3.0.0 + */ +gboolean purple_idle_manager_set_source(PurpleIdleManager *manager, const char *source, GDateTime *timestamp); + +/** + * purple_idle_manager_get_timestamp: + * @manager: The instance. + * + * Gets the oldest timestamp of all the sources that @manager knows about. + * + * Returns: (transfer none) (nullable): The oldest timestamp or %NULL if no + * sources are idle. + * + * Since: 3.0.0 + */ +GDateTime *purple_idle_manager_get_timestamp(PurpleIdleManager *manager); + +G_END_DECLS + +#endif /* PURPLE_IDLE_MANAGER_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/purpleidlemanagerprivate.h Mon Oct 23 22:08:37 2023 -0500 @@ -0,0 +1,52 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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; either version 2 of the License, or + * (at your option) any later version. + * + * 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 <https://www.gnu.org/licenses/>. + */ + +#ifndef PURPLE_COMPILATION +# error "purpleidlemanagerprivate.h may only be include by libpurple" +#endif + +#ifndef PURPLE_IDLE_MANAGER_PRIVATE_H +#define PURPLE_IDLE_MANAGER_PRIVATE_H + +#include <glib.h> + +/** + * purple_idle_manager_startup: (skip) + * + * Starts up the idle manager by creating the default instance. + * + * Since: 3.0.0 + */ +void purple_idle_manager_startup(void); + +/** + * purple_idle_manager_shutdown: (skip) + * + * Shuts down the idle manager by destroying the default instance. + * + * Since: 3.0.0 + */ +void purple_idle_manager_shutdown(void); + +G_END_DECLS + +#endif /* PURPLE_IDLE_MANAGER_PRIVATE_H */
--- a/libpurple/tests/meson.build Sun Oct 22 20:45:40 2023 -0500 +++ b/libpurple/tests/meson.build Mon Oct 23 22:08:37 2023 -0500 @@ -15,6 +15,7 @@ 'file_transfer', 'history_adapter', 'history_manager', + 'idle_manager', 'image', 'keyvaluepair', 'markup',
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/tests/test_idle_manager.c Mon Oct 23 22:08:37 2023 -0500 @@ -0,0 +1,190 @@ +/* + * Purple - Internet Messaging Library + * Copyright (C) Pidgin Developers <devel@pidgin.im> + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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; either version 2 of the License, or + * (at your option) any later version. + * + * 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 <https://www.gnu.org/licenses/>. + */ + +#include <glib.h> + +#include <purple.h> + +/****************************************************************************** + * Callbacks + *****************************************************************************/ +static void +test_purple_idle_manager_timestamp_changed(G_GNUC_UNUSED GObject *obj, + G_GNUC_UNUSED GParamSpec *pspec, + gpointer data) +{ + guint *counter = data; + + *counter = *counter + 1; +} + +/****************************************************************************** + * Basic Tests + *****************************************************************************/ +static void +test_purple_idle_manager_new(void) { + PurpleIdleManager *manager = NULL; + + manager = g_object_new(PURPLE_TYPE_IDLE_MANAGER, NULL); + + g_assert_true(PURPLE_IS_IDLE_MANAGER(manager)); + + g_clear_object(&manager); +} + +static void +test_purple_idle_manager_single_source(void) { + PurpleIdleManager *manager = NULL; + GDateTime *actual = NULL; + GDateTime *timestamp = NULL; + GDateTime *now = NULL; + gboolean res = FALSE; + guint counter = 0; + + manager = g_object_new(PURPLE_TYPE_IDLE_MANAGER, NULL); + + /* Create a timestamp from an hour ago. */ + now = g_date_time_new_now_local(); + timestamp = g_date_time_add_hours(now, -1); + g_clear_pointer(&now, g_date_time_unref); + + /* Connect our signal to make sure the timestamp got set. */ + g_signal_connect(manager, "notify::timestamp", + G_CALLBACK(test_purple_idle_manager_timestamp_changed), + &counter); + + /* Set the source. */ + res = purple_idle_manager_set_source(manager, "unit-tests", timestamp); + g_assert_true(res); + + /* Verify the source is now the active one. */ + actual = purple_idle_manager_get_timestamp(manager); + g_assert_nonnull(actual); + g_assert_true(g_date_time_equal(actual, timestamp)); + g_clear_pointer(×tamp, g_date_time_unref); + + /* Make sure the notify signal got called. */ + g_assert_cmpuint(counter, ==, 1); + + /* Now remove the source and verify that the notify signal was emitted and + * that there is no active source. + */ + counter = 0; + res = purple_idle_manager_set_source(manager, "unit-tests", NULL); + g_assert_true(res); + + actual = purple_idle_manager_get_timestamp(manager); + g_assert_null(actual); + + g_assert_cmpuint(counter, ==, 1); + + g_clear_object(&manager); +} + +static void +test_purple_idle_manager_multiple_sources(void) { + PurpleIdleManager *manager = NULL; + GDateTime *actual = NULL; + GDateTime *timestamp1 = NULL; + GDateTime *timestamp2 = NULL; + GDateTime *timestamp3 = NULL; + GDateTime *now = NULL; + gboolean res = FALSE; + + manager = g_object_new(PURPLE_TYPE_IDLE_MANAGER, NULL); + + /* Create a few timestamps for testing. */ + now = g_date_time_new_now_local(); + timestamp1 = g_date_time_add_minutes(now, -10); + timestamp2 = g_date_time_add_minutes(now, -60); + timestamp3 = g_date_time_add_minutes(now, -1); + g_clear_pointer(&now, g_date_time_unref); + + /* Set source1 which is 10 minutes idle. */ + res = purple_idle_manager_set_source(manager, "source1", timestamp1); + g_assert_true(res); + + actual = purple_idle_manager_get_timestamp(manager); + g_assert_nonnull(actual); + g_assert_true(g_date_time_equal(actual, timestamp1)); + + /* Set source2 which is 1 hour idle which should take over the global state + * as well. + */ + res = purple_idle_manager_set_source(manager, "source2", timestamp2); + g_assert_true(res); + + actual = purple_idle_manager_get_timestamp(manager); + g_assert_nonnull(actual); + g_assert_true(g_date_time_equal(actual, timestamp2)); + + /* Now remove source2 and verify we fell back to source1. */ + res = purple_idle_manager_set_source(manager, "source2", NULL); + g_assert_true(res); + + actual = purple_idle_manager_get_timestamp(manager); + g_assert_nonnull(actual); + g_assert_true(g_date_time_equal(actual, timestamp1)); + + /* Add source3 that won't cause a change. */ + res = purple_idle_manager_set_source(manager, "source3", timestamp3); + g_assert_false(res); + + actual = purple_idle_manager_get_timestamp(manager); + g_assert_true(g_date_time_equal(actual, timestamp1)); + + /* Remove source3 and verify that source1 is still the active source. */ + res = purple_idle_manager_set_source(manager, "source3", NULL); + g_assert_false(res); + + actual = purple_idle_manager_get_timestamp(manager); + g_assert_true(g_date_time_equal(actual, timestamp1)); + + /* Finally remove source1 and verify that we're no longer idle. */ + res = purple_idle_manager_set_source(manager, "source1", NULL); + g_assert_true(res); + + actual = purple_idle_manager_get_timestamp(manager); + g_assert_null(actual); + + /* Cleanup. */ + g_clear_pointer(×tamp1, g_date_time_unref); + g_clear_pointer(×tamp2, g_date_time_unref); + g_clear_pointer(×tamp3, g_date_time_unref); + g_clear_object(&manager); +} + +/****************************************************************************** + * Main + *****************************************************************************/ +gint +main(int argc, char **argv) { + g_test_init(&argc, &argv, NULL); + + g_test_add_func("/idle-manager/new", test_purple_idle_manager_new); + g_test_add_func("/idle-manager/single-source", + test_purple_idle_manager_single_source); + g_test_add_func("/idle-manager/multiple-sources", + test_purple_idle_manager_multiple_sources); + + return g_test_run(); +}