# HG changeset patch
# Node ID a6f7f9da8267
# Parent  ba3a49903905
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
@@ -60,6 +60,7 @@
 	'purplegio.c',
 	'purplehistoryadapter.c',
 	'purplehistorymanager.c',
+	'purpleidlemanager.c',
 	'purpleidleui.c',
 	'purpleimconversation.c',
 	'purplekeyvaluepair.c',
@@ -182,6 +183,7 @@
 	'purplegio.h',
 	'purplehistoryadapter.h',
 	'purplehistorymanager.h',
+	'purpleidlemanager.h',
 	'purpleidleui.h',
 	'purpleimconversation.h',
 	'purpleattachment.h',
@@ -317,6 +319,10 @@
 	'xmlnode.h'
 ]
 
+purple_private_headers = [
+	'purpleidlemanagerprivate.h',
+	'purpleprivate.h',
+]
 
 enums = gnome.mkenums_simple('purpleenums',
     sources : purple_enumheaders,
@@ -358,8 +364,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,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 at the value for
+ * [property@IdleManager:timestamp] as it is most likely what the user is
+ * looking for based on the settings the 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], so [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 obscene 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 */
diff --git a/libpurple/purpleidlemanager.c b/libpurple/purpleidlemanager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleidlemanager.c
@@ -0,0 +1,189 @@
+/*
+ * 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);
+}
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_PRIVATE_H */
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -15,6 +15,7 @@
     'file_transfer',
     '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_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 an 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(&timestamp, 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(&timestamp1, g_date_time_unref);
+	g_clear_pointer(&timestamp2, g_date_time_unref);
+	g_clear_pointer(&timestamp3, 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();
+}
