# HG changeset patch
# Node ID a0d2e938e79f
# Parent  8888e6d1e9a8
diff --git a/libpurple/core.c b/libpurple/core.c
--- a/libpurple/core.c
+++ b/libpurple/core.c
@@ -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"
@@ -183,6 +184,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
@@ -240,6 +242,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();
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -58,6 +58,7 @@
 	'purplegio.c',
 	'purplehistoryadapter.c',
 	'purplehistorymanager.c',
+	'purpleidlemanager.c',
 	'purpleidleui.c',
 	'purpleimconversation.c',
 	'purplekeyvaluepair.c',
@@ -177,6 +178,7 @@
 	'purplegio.h',
 	'purplehistoryadapter.h',
 	'purplehistorymanager.h',
+	'purpleidlemanager.h',
 	'purpleidleui.h',
 	'purpleimconversation.h',
 	'purpleattachment.h',
@@ -310,6 +312,10 @@
 	'xmlnode.h'
 ]
 
+purple_private_headers = [
+	'purpleidlemanagerprivate.h',
+	'purpleprivate.h',
+]
 
 enums = gnome.mkenums_simple('purpleenums',
     sources : purple_enumheaders,
@@ -351,8 +357,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,
diff --git a/libpurple/purpleidlemanager.h b/libpurple/purpleidlemanager.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleidlemanager.h
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ *
+ * 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.
+ * @idle_since: (nullable): The new idle since for @source.
+ *
+ * Sets the idle_size of @source to @idle_since. If @idle_since is %NULL,
+ * @source will be removed from @manager.
+ *
+ * Returns: %TRUE [property@IdleManager:idle-since] has changed due to this
+ *          call.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_idle_manager_set_source(PurpleIdleManager *manager, const char *source, GDateTime *idle_since);
+
+/**
+ * purple_idle_manager_get_idle_since:
+ * @manager: The instance.
+ *
+ * Gets the oldest idle since of all the sources that @manager knows about.
+ *
+ * Returns: (transfer none) (nullable): The new idle since or %NULL if no
+ *          sources are idle.
+ *
+ * Since: 3.0.0
+ */
+GDateTime *purple_idle_manager_get_idle_since(PurpleIdleManager *manager);
+
+G_END_DECLS
+
+#endif /* PURPLE_IDLE_MANAGER_H */
diff --git a/libpurple/purpleidlemanager.c b/libpurple/purpleidlemanager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleidlemanager.c
@@ -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 "purpleidlemanager.h"
+#include "purpleidlemanagerprivate.h"
+
+#include "util.h"
+
+enum {
+	PROP_0,
+	PROP_IDLE_SINCE,
+	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_IDLE_SINCE:
+			g_value_set_boxed(value,
+			                  purple_idle_manager_get_idle_since(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::idle-since:
+	 *
+	 * The aggregate of the oldest idle since of all of the sources that are
+	 * known.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_IDLE_SINCE] = g_param_spec_boxed(
+		"idle-since", "idle-since",
+		"The oldest aggregate idle-since 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 *idle_since)
+{
+	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(idle_since != NULL) {
+		g_hash_table_insert(manager->sources, g_strdup(source),
+		                    g_date_time_ref(idle_since));
+	} 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_idle_since 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_IDLE_SINCE]);
+
+		return TRUE;
+	}
+
+	return FALSE;
+}
+
+GDateTime *
+purple_idle_manager_get_idle_since(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);
+}
diff --git a/libpurple/purpleidlemanagerprivate.h b/libpurple/purpleidlemanagerprivate.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleidlemanagerprivate.h
@@ -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_H */
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -13,6 +13,7 @@
     'credential_provider',
     'history_adapter',
     'history_manager',
+    'idle_manager',
     'image',
     'keyvaluepair',
     'markup',
diff --git a/libpurple/tests/test_idle_manager.c b/libpurple/tests/test_idle_manager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_idle_manager.c
@@ -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_idle_since_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 *idle_since = NULL;
+	GDateTime *now = NULL;
+	gboolean res = FALSE;
+	guint counter = 0;
+
+	manager = g_object_new(PURPLE_TYPE_IDLE_MANAGER, NULL);
+
+	/* Create an idle_since from an hour ago. */
+	now = g_date_time_new_now_local();
+	idle_since = g_date_time_add_hours(now, -1);
+	g_clear_pointer(&now, g_date_time_unref);
+
+	/* Connect our signal to make sure idle-since got set. */
+	g_signal_connect(manager, "notify::idle-since",
+	                 G_CALLBACK(test_purple_idle_manager_idle_since_changed),
+	                 &counter);
+
+	/* Set the source. */
+	res = purple_idle_manager_set_source(manager, "unit-tests", idle_since);
+	g_assert_true(res);
+
+	/* Verify the source is now the active one. */
+	actual = purple_idle_manager_get_idle_since(manager);
+	g_assert_nonnull(actual);
+	g_assert_true(g_date_time_equal(actual, idle_since));
+	g_clear_pointer(&idle_since, 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_idle_since(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 *idle_since1 = NULL;
+	GDateTime *idle_since2 = NULL;
+	GDateTime *idle_since3 = NULL;
+	GDateTime *now = NULL;
+	gboolean res = FALSE;
+
+	manager = g_object_new(PURPLE_TYPE_IDLE_MANAGER, NULL);
+
+	/* Create an idle_since from an hour ago. */
+	now = g_date_time_new_now_local();
+	idle_since1 = g_date_time_add_minutes(now, -10);
+	idle_since2 = g_date_time_add_minutes(now, -60);
+	idle_since3 = 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", idle_since1);
+	g_assert_true(res);
+
+	actual = purple_idle_manager_get_idle_since(manager);
+	g_assert_nonnull(actual);
+	g_assert_true(g_date_time_equal(actual, idle_since1));
+
+	/* Set source2 which is 1 hour idle which should take over the global state
+	 * as well.
+	 */
+	res = purple_idle_manager_set_source(manager, "source2", idle_since2);
+	g_assert_true(res);
+
+	actual = purple_idle_manager_get_idle_since(manager);
+	g_assert_nonnull(actual);
+	g_assert_true(g_date_time_equal(actual, idle_since2));
+
+	/* 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_idle_since(manager);
+	g_assert_nonnull(actual);
+	g_assert_true(g_date_time_equal(actual, idle_since1));
+
+	/* Add source3 that won't cause a change. */
+	res = purple_idle_manager_set_source(manager, "source3", idle_since3);
+	g_assert_false(res);
+
+	actual = purple_idle_manager_get_idle_since(manager);
+	g_assert_true(g_date_time_equal(actual, idle_since1));
+
+	/* 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_idle_since(manager);
+	g_assert_true(g_date_time_equal(actual, idle_since1));
+
+	/* 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_idle_since(manager);
+	g_assert_null(actual);
+
+	/* Cleanup. */
+	g_clear_pointer(&idle_since1, g_date_time_unref);
+	g_clear_pointer(&idle_since2, g_date_time_unref);
+	g_clear_pointer(&idle_since3, 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();
+}
