# HG changeset patch
# Node ID 59d9b3ab593c
# Parent  26ae6ffab6d0
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -56,6 +56,7 @@
 	'purplecredentialprovider.c',
 	'purpledebugui.c',
 	'purplefiletransfer.c',
+	'purplefiletransfermanager.c',
 	'purplegdkpixbuf.c',
 	'purplegio.c',
 	'purplehistoryadapter.c',
@@ -180,6 +181,7 @@
 	'purplecredentialprovider.h',
 	'purpledebugui.h',
 	'purplefiletransfer.h',
+	'purplefiletransfermanager.h',
 	'purplegdkpixbuf.h',
 	'purplegio.h',
 	'purplehistoryadapter.h',
@@ -243,6 +245,7 @@
 ]
 
 purple_private_headers = [
+	'purplefiletransfermanagerprivate.h',
 	'purplepresencemanagerprivate.h',
 	'purpleprivate.h',
 	'purplesavedpresenceprivate.h',
diff --git a/libpurple/purplefiletransfer.h b/libpurple/purplefiletransfer.h
--- a/libpurple/purplefiletransfer.h
+++ b/libpurple/purplefiletransfer.h
@@ -64,7 +64,7 @@
  * Since: 3.0.0
  */
 
-#define PURPLE_TYPE_FILE_TRANSFER purple_file_transfer_get_type()
+#define PURPLE_TYPE_FILE_TRANSFER (purple_file_transfer_get_type())
 
 PURPLE_AVAILABLE_IN_3_0
 G_DECLARE_FINAL_TYPE(PurpleFileTransfer, purple_file_transfer, PURPLE,
diff --git a/libpurple/purplefiletransfermanager.h b/libpurple/purplefiletransfermanager.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplefiletransfermanager.h
@@ -0,0 +1,103 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 <pidgin.h> may be included directly"
+#endif
+
+#ifndef PURPLE_FILE_TRANSFER_MANAGER_H
+#define PURPLE_FILE_TRANSFER_MANAGER_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "purplefiletransfer.h"
+
+G_BEGIN_DECLS
+
+#define PURPLE_TYPE_FILE_TRANSFER_MANAGER (purple_file_transfer_manager_get_type())
+
+/**
+ * PurpleFileTransferManager:
+ *
+ * Keeps track of the [class@FileTransfer]'s that have been added to it and
+ * propagates all of their signals.
+ *
+ * Since: 3.0.0
+ */
+
+PURPLE_AVAILABLE_IN_3_0
+G_DECLARE_FINAL_TYPE(PurpleFileTransferManager, purple_file_transfer_manager,
+                     PURPLE, FILE_TRANSFER_MANAGER, GObject)
+
+/**
+ * purple_file_transfer_manager_get_default:
+ *
+ * Gets the default file transfer manager for libpurple.
+ *
+ * Returns: (transfer none): The default file transfer manager for libpurple.
+ *
+ * Since: 3.0.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleFileTransferManager *purple_file_transfer_manager_get_default(void);
+
+/**
+ * purple_file_transfer_manager_get_default_as_model:
+ *
+ * Gets the default file transfer manager for libpurple as a list model.
+ *
+ * Returns: (transfer none): The default file transfer manager for libpurple.
+ *
+ * Since: 3.0.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+GListModel *purple_file_transfer_manager_get_default_as_model(void);
+
+/**
+ * purple_file_transfer_manager_add:
+ * @manager: The instance.
+ * @transfer: (transfer none): The file transfer to add.
+ *
+ * Adds @transfer to @manager.
+ *
+ * When a transfer is added to [class@FileTransferManager] the manager will
+ * keep track of the transfer and propagate all of it's signals.
+ *
+ * Since: 3.0.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_file_transfer_manager_add(PurpleFileTransferManager *manager, PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_manager_remove:
+ * @manager: The instance.
+ * @transfer: The transfer to remove.
+ *
+ * Removes @transfer from @manager.
+ *
+ * Returns: %TRUE if the item was removed, otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_file_transfer_manager_remove(PurpleFileTransferManager *manager, PurpleFileTransfer *transfer);
+
+G_END_DECLS
+
+#endif /* PURPLE_FILE_TRANSFER_MANAGER_H */
diff --git a/libpurple/purplefiletransfermanager.c b/libpurple/purplefiletransfermanager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purplefiletransfermanager.c
@@ -0,0 +1,276 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * 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 "purplefiletransfermanager.h"
+#include "purplefiletransfermanagerprivate.h"
+
+enum {
+	SIG_ADDED,
+	SIG_REMOVED,
+	SIG_TRANSFER_CHANGED,
+	N_SIGNALS,
+};
+static guint signals[N_SIGNALS] = {};
+
+struct _PurpleFileTransferManager {
+	GObject parent;
+
+	GPtrArray *transfers;
+};
+
+static PurpleFileTransferManager *default_manager = NULL;
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+purple_file_transfer_manager_transfer_changed_cb(PurpleFileTransfer *transfer,
+                                                 GParamSpec *pspec,
+                                                 gpointer data)
+{
+	g_signal_emit(data, signals[SIG_TRANSFER_CHANGED],
+	              g_param_spec_get_name_quark(pspec),
+	              transfer, pspec);
+}
+
+/******************************************************************************
+ * GListModel Implementation
+ *****************************************************************************/
+static GType
+purple_file_transfer_manager_get_item_type(G_GNUC_UNUSED GListModel *list) {
+	return PURPLE_TYPE_FILE_TRANSFER;
+}
+
+static guint
+purple_file_transfer_manager_get_n_items(GListModel *list) {
+	PurpleFileTransferManager *manager = PURPLE_FILE_TRANSFER_MANAGER(list);
+
+	return manager->transfers->len;
+}
+
+static gpointer
+purple_file_transfer_manager_get_item(GListModel *list, guint position) {
+	PurpleFileTransferManager *manager = PURPLE_FILE_TRANSFER_MANAGER(list);
+	PurpleFileTransfer *transfer = NULL;
+
+	if(position < manager->transfers->len) {
+		transfer = g_ptr_array_index(manager->transfers, position);
+		g_object_ref(transfer);
+	}
+
+	return transfer;
+}
+
+static void
+pidgin_file_transfer_manager_list_model_iface_init(GListModelInterface *iface) {
+	iface->get_item_type = purple_file_transfer_manager_get_item_type;
+	iface->get_n_items = purple_file_transfer_manager_get_n_items;
+	iface->get_item = purple_file_transfer_manager_get_item;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_FINAL_TYPE_WITH_CODE(PurpleFileTransferManager,
+                              purple_file_transfer_manager,
+                              G_TYPE_OBJECT,
+                              G_IMPLEMENT_INTERFACE(G_TYPE_LIST_MODEL,
+                                                    pidgin_file_transfer_manager_list_model_iface_init))
+
+static void
+purple_file_transfer_manager_finalize(GObject *obj) {
+	PurpleFileTransferManager *manager= PURPLE_FILE_TRANSFER_MANAGER(obj);
+
+	g_clear_pointer(&manager->transfers, g_ptr_array_unref);
+
+	G_OBJECT_CLASS(purple_file_transfer_manager_parent_class)->finalize(obj);
+}
+
+static void
+purple_file_transfer_manager_init(PurpleFileTransferManager *manager) {
+	manager->transfers = g_ptr_array_new();
+}
+
+static void
+purple_file_transfer_manager_class_init(PurpleFileTransferManagerClass *klass)
+{
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = purple_file_transfer_manager_finalize;
+
+	/**
+	 * PurpleFileTransferManager::added:
+	 * @manager: The instance.
+	 * @transfer: The [class@FileTransfer] that was added.
+	 * @data: User data.
+	 *
+	 * Emitted when a file transfer is added to @manager.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_ADDED] = g_signal_new_class_handler(
+		"added",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		PURPLE_TYPE_FILE_TRANSFER);
+
+	/**
+	 * PurpleFileTransferManager::removed:
+	 * @manager: The instance.
+	 * @transfer: The [class@FileTransfer] that was removed.
+	 * @data: User data.
+	 *
+	 * Emitted when a file transfer is removed from @manager.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_REMOVED] = g_signal_new_class_handler(
+		"removed",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		1,
+		PURPLE_TYPE_FILE_TRANSFER);
+
+	/**
+	 * PurpleFileTransferManager::transfer-changed:
+	 * @manager: The manager instance.
+	 * @transfer: The transfer that changed.
+	 * @pspec: The [class@GObject.ParamSpec] for the property that changed.
+	 *
+	 * This is a propagation of the notify signal from @transfer. This means
+	 * that your callback will be called for any file transfer that @manager
+	 * knows about.
+	 *
+	 * This also supports details, so you can specify the signal name as
+	 * something like `transfer-changed::state` and your callback will only
+	 * be called when the state property of @transfer has been changed.
+	 *
+	 * Since: 3.0.0
+	 */
+	signals[SIG_TRANSFER_CHANGED] = g_signal_new_class_handler(
+		"transfer-changed",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED,
+		NULL,
+		NULL,
+		NULL,
+		NULL,
+		G_TYPE_NONE,
+		2,
+		PURPLE_TYPE_FILE_TRANSFER,
+		G_TYPE_PARAM);
+}
+
+/******************************************************************************
+ * Private API
+ *****************************************************************************/
+void
+purple_file_transfer_manager_startup(void) {
+	if(!PURPLE_IS_FILE_TRANSFER_MANAGER(default_manager)) {
+		default_manager = g_object_new(PURPLE_TYPE_FILE_TRANSFER_MANAGER,
+		                               NULL);
+
+		g_object_add_weak_pointer(G_OBJECT(default_manager),
+		                          (gpointer *)&default_manager);
+	}
+}
+
+void
+purple_file_transfer_manager_shutdown(void) {
+	g_clear_object(&default_manager);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurpleFileTransferManager *
+purple_file_transfer_manager_get_default(void) {
+	return default_manager;
+}
+
+GListModel *
+purple_file_transfer_manager_get_default_as_model(void) {
+	if(PURPLE_IS_FILE_TRANSFER_MANAGER(default_manager)) {
+		return G_LIST_MODEL(default_manager);
+	}
+
+	return NULL;
+}
+
+void
+purple_file_transfer_manager_add(PurpleFileTransferManager *manager,
+                                 PurpleFileTransfer *transfer)
+{
+	guint position = 0;
+
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER_MANAGER(manager));
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+
+	/* Store the len as position before appending to avoid math. */
+	position = manager->transfers->len;
+	g_ptr_array_add(manager->transfers, g_object_ref(transfer));
+
+	g_signal_connect_object(transfer, "notify",
+	                        G_CALLBACK(purple_file_transfer_manager_transfer_changed_cb),
+	                        manager, 0);
+
+	g_list_model_items_changed(G_LIST_MODEL(manager), position, 0, 1);
+
+	g_signal_emit(manager, signals[SIG_ADDED], 0, transfer);
+}
+
+gboolean
+purple_file_transfer_manager_remove(PurpleFileTransferManager *manager,
+                                    PurpleFileTransfer *transfer)
+{
+	gboolean found = FALSE;
+	guint position = 0;
+
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER_MANAGER(manager), FALSE);
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), FALSE);
+
+	found = g_ptr_array_find(manager->transfers, transfer, &position);
+	if(!found) {
+		return FALSE;
+	}
+
+	/* Temporarily add a ref to the transfer so we can emit the removed
+	 * signal without issue.
+	 */
+	g_object_ref(transfer);
+
+	g_ptr_array_remove_index(manager->transfers, position);
+
+	g_list_model_items_changed(G_LIST_MODEL(manager), position, 1, 0);
+	g_signal_emit(manager, signals[SIG_REMOVED], 0, transfer);
+
+	g_object_unref(transfer);
+
+	return TRUE;
+}
diff --git a/libpurple/purplefiletransfermanagerprivate.h b/libpurple/purplefiletransfermanagerprivate.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplefiletransfermanagerprivate.h
@@ -0,0 +1,54 @@
+/*
+ * 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 "purplefiletransfermanagerprivate.h may only be include by libpurple"
+#endif
+
+#ifndef PURPLE_FILE_TRANSFER_MANAGER_PRIVATE_H
+#define PURPLE_FILE_TRANSFER_MANAGER_PRIVATE_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+/**
+ * purple_file_transfer_manager_startup: (skip)
+ *
+ * Creates the default [class@FileTransferManager].
+ *
+ * Since: 3.0.0
+ */
+G_GNUC_INTERNAL void purple_file_transfer_manager_startup(void);
+
+/**
+ * purple_file_transfer_manager_shutdown: (skip)
+ *
+ * Cleans up the default [class@FileTransferManager].
+ *
+ * Since: 3.0.0
+ */
+G_GNUC_INTERNAL void purple_file_transfer_manager_shutdown(void);
+
+G_END_DECLS
+
+#endif /* PURPLE_FILE_TRANSFER_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
@@ -13,6 +13,7 @@
     'credential_manager',
     'credential_provider',
     'file_transfer',
+    'file_transfer_manager',
     'history_adapter',
     'history_manager',
     'idle_manager',
diff --git a/libpurple/tests/test_file_transfer_manager.c b/libpurple/tests/test_file_transfer_manager.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_file_transfer_manager.c
@@ -0,0 +1,176 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+/******************************************************************************
+ * Add/Remove Test
+ *****************************************************************************/
+static void
+test_purple_file_transfer_manager_add_remove_cb(PurpleFileTransferManager *manager,
+                                                PurpleFileTransfer *transfer,
+                                                gpointer data)
+{
+	guint *counter = data;
+
+	g_assert_true(PURPLE_IS_FILE_TRANSFER_MANAGER(manager));
+	g_assert_true(PURPLE_IS_FILE_TRANSFER(transfer));
+
+	*counter = *counter + 1;
+}
+
+static void
+test_purple_file_transfer_manager_add_remove(void) {
+	PurpleAccount *account = NULL;
+	PurpleContactInfo *remote = NULL;
+	PurpleFileTransfer *transfer = NULL;
+	PurpleFileTransferManager *manager = NULL;
+	guint counter = 0;
+
+	account = purple_account_new("test", "test");
+	remote = purple_contact_info_new(NULL);
+
+	manager = g_object_new(PURPLE_TYPE_FILE_TRANSFER_MANAGER, NULL);
+	g_signal_connect(manager, "added",
+	                 G_CALLBACK(test_purple_file_transfer_manager_add_remove_cb),
+	                 &counter);
+	g_signal_connect(manager, "removed",
+	                 G_CALLBACK(test_purple_file_transfer_manager_add_remove_cb),
+	                 &counter);
+
+	g_assert_cmpuint(g_list_model_get_n_items(G_LIST_MODEL(manager)), ==, 0);
+
+	/* Add the transfer, verify that it was added, and that the signal was
+	 * emitted.
+	 */
+	transfer = purple_file_transfer_new_receive(account, remote, "foo.bar", 0);
+	purple_file_transfer_manager_add(manager, transfer);
+	g_assert_cmpuint(g_list_model_get_n_items(G_LIST_MODEL(manager)), ==, 1);
+	g_assert_cmpuint(counter, ==, 1);
+
+	counter = 0;
+
+	/* Remove the transfer, verify that it was removed, and that the signal
+	 * was emitted.
+	 */
+	purple_file_transfer_manager_remove(manager, transfer);
+	g_assert_cmpuint(g_list_model_get_n_items(G_LIST_MODEL(manager)), ==, 0);
+	g_assert_cmpuint(counter, ==, 1);
+
+	/* Cleanup */
+	g_clear_object(&transfer);
+	g_clear_object(&manager);
+	g_clear_object(&remote);
+	g_clear_object(&account);
+}
+
+/******************************************************************************
+ * Signal Propagation Tests
+ *****************************************************************************/
+static void
+test_purple_file_transfer_manager_transfer_changed_cb(PurpleFileTransferManager *manager,
+                                                      PurpleFileTransfer *transfer,
+                                                      GParamSpec *pspec,
+                                                      gpointer data)
+{
+	guint *counter = data;
+
+	g_assert_true(PURPLE_IS_FILE_TRANSFER_MANAGER(manager));
+	g_assert_true(PURPLE_IS_FILE_TRANSFER(transfer));
+	g_assert_true(G_IS_PARAM_SPEC(pspec));
+
+	*counter = *counter + 1;
+}
+
+static void
+test_purple_file_transfer_manager_propagates_notify(void) {
+	PurpleAccount *account = NULL;
+	PurpleContactInfo *remote = NULL;
+	PurpleFileTransferManager *manager = NULL;
+	PurpleFileTransfer *transfer = NULL;
+	guint generic_counter = 0;
+	guint specific_counter = 0;
+
+	account = purple_account_new("test", "test");
+	remote = purple_contact_info_new(NULL);
+
+	manager = g_object_new(PURPLE_TYPE_FILE_TRANSFER_MANAGER, NULL);
+	g_signal_connect(manager, "transfer-changed",
+	                 G_CALLBACK(test_purple_file_transfer_manager_transfer_changed_cb),
+	                 &generic_counter);
+	g_signal_connect(manager, "transfer-changed::state",
+	                 G_CALLBACK(test_purple_file_transfer_manager_transfer_changed_cb),
+	                 &specific_counter);
+
+	transfer = purple_file_transfer_new_receive(account, remote, "foo.bar", 0);
+	purple_file_transfer_manager_add(manager, transfer);
+
+	/* Verify our counters are still 0. */
+	g_assert_cmpuint(generic_counter, ==, 0);
+	g_assert_cmpuint(specific_counter, ==, 0);
+
+	/* Change the state property and verify that each counter was incremented
+	 * by 1.
+	 */
+	purple_file_transfer_set_state(transfer,
+	                               PURPLE_FILE_TRANSFER_STATE_NEGOTIATING);
+	g_assert_cmpuint(generic_counter, ==, 1);
+	g_assert_cmpuint(specific_counter, ==, 1);
+
+	/* Now set another property and verify that only the generic counter was
+	 * incremented.
+	 */
+	purple_file_transfer_set_message(transfer, "heyo");
+	g_assert_cmpuint(generic_counter, ==, 2);
+	g_assert_cmpuint(specific_counter, ==, 1);
+
+	g_clear_object(&transfer);
+	g_clear_object(&manager);
+	g_clear_object(&remote);
+	g_clear_object(&account);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	gint ret = 0;
+
+	g_test_init(&argc, &argv, NULL);
+
+	g_test_set_nonfatal_assertions();
+
+	test_ui_purple_init();
+
+	g_test_add_func("/file-transfer-manager/add-remove",
+	                test_purple_file_transfer_manager_add_remove);
+
+	g_test_add_func("/file-transfer-manager/propagates-notify",
+	                test_purple_file_transfer_manager_propagates_notify);
+
+	ret = g_test_run();
+
+	test_ui_purple_uninit();
+
+	return ret;
+}
