[Telepathy-commits] [telepathy-qt4/master] Add 'callable' example CM from telepathy-glib 0.7.27

Simon McVittie simon.mcvittie at collabora.co.uk
Tue Mar 17 15:27:13 PDT 2009


---
 configure.ac                            |    2 +-
 tests/lib/Makefile.am                   |    3 +-
 tests/lib/callable/Makefile.am          |   39 ++
 tests/lib/callable/conn.c               |  402 ++++++++++++
 tests/lib/callable/conn.h               |   78 +++
 tests/lib/callable/connection-manager.c |  130 ++++
 tests/lib/callable/connection-manager.h |   73 +++
 tests/lib/callable/media-channel.c      | 1024 +++++++++++++++++++++++++++++++
 tests/lib/callable/media-channel.h      |   74 +++
 tests/lib/callable/media-manager.c      |  438 +++++++++++++
 tests/lib/callable/media-manager.h      |   71 +++
 tests/lib/callable/media-stream.c       |  506 +++++++++++++++
 tests/lib/callable/media-stream.h       |   83 +++
 13 files changed, 2921 insertions(+), 2 deletions(-)
 create mode 100644 tests/lib/callable/Makefile.am
 create mode 100644 tests/lib/callable/conn.c
 create mode 100644 tests/lib/callable/conn.h
 create mode 100644 tests/lib/callable/connection-manager.c
 create mode 100644 tests/lib/callable/connection-manager.h
 create mode 100644 tests/lib/callable/media-channel.c
 create mode 100644 tests/lib/callable/media-channel.h
 create mode 100644 tests/lib/callable/media-manager.c
 create mode 100644 tests/lib/callable/media-manager.h
 create mode 100644 tests/lib/callable/media-stream.c
 create mode 100644 tests/lib/callable/media-stream.h

diff --git a/configure.ac b/configure.ac
index 59247c7..faf6de1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -182,7 +182,7 @@ AC_SUBST(PROTO_CFLAGS)
 
 dnl Check for telepathy-glib, and for Qt <-> GLib main loop integration:
 dnl if we have both, we can run more tests
-PKG_CHECK_MODULES(TP_GLIB, [telepathy-glib >= 0.7.26], [have_tp_glib=yes],
+PKG_CHECK_MODULES(TP_GLIB, [telepathy-glib >= 0.7.27], [have_tp_glib=yes],
                   [have_tp_glib=no])
 AC_SUBST(TP_GLIB_CFLAGS)
 AC_SUBST(TP_GLIB_LIBS)
diff --git a/tests/lib/Makefile.am b/tests/lib/Makefile.am
index 1d9687e..e770a0f 100644
--- a/tests/lib/Makefile.am
+++ b/tests/lib/Makefile.am
@@ -28,7 +28,7 @@ libtp_qt4_tests_la_LIBADD = $(top_builddir)/TelepathyQt4/libtelepathy-qt4.la
 
 if ENABLE_TP_GLIB_TESTS
 
-SUBDIRS += contactlist echo
+SUBDIRS += callable contactlist echo
 AM_CFLAGS += $(TP_GLIB_CFLAGS)
 AM_CXXFLAGS += $(TP_GLIB_CFLAGS)
 
@@ -44,6 +44,7 @@ libtp_glib_tests_la_SOURCES = \
     simple-manager.h
 libtp_glib_tests_la_LIBADD = \
     $(TP_GLIB_LIBS) \
+    callable/libexample-cm-callable.la \
     echo/libexample-cm-echo.la \
     contactlist/libexample-cm-contactlist.la
 
diff --git a/tests/lib/callable/Makefile.am b/tests/lib/callable/Makefile.am
new file mode 100644
index 0000000..2d7d8a9
--- /dev/null
+++ b/tests/lib/callable/Makefile.am
@@ -0,0 +1,39 @@
+# Taken from telepathy-glib. The only change is to remove main.c and cut down
+# the Makefile.am accordingly.
+#
+# PLEASE DO NOT MODIFY THIS CONNECTION MANAGER. Either subclass it,
+# copy-and-modify (moving it to a better namespace), or make changes in the
+# copy in telepathy-glib first.
+
+noinst_LTLIBRARIES = libexample-cm-callable.la
+
+libexample_cm_callable_la_SOURCES = \
+    conn.c \
+    conn.h \
+    connection-manager.c \
+    connection-manager.h \
+    media-channel.c \
+    media-channel.h \
+    media-manager.c \
+    media-manager.h \
+    media-stream.c \
+    media-stream.h
+
+libexample_cm_callable_la_LIBADD = $(TP_GLIB_LIBS)
+
+AM_CFLAGS = \
+    $(ERROR_CFLAGS) \
+    $(TP_GLIB_CFLAGS)
+
+EXTRA_DIST = manager-file.py
+
+_gen/example_callable.manager _gen/param-spec-struct.h: \
+		manager-file.py $(top_srcdir)/tools/manager-file.py
+	$(mkdir_p) _gen
+	$(PYTHON) $(top_srcdir)/tools/manager-file.py $(srcdir)/manager-file.py _gen
+
+BUILT_SOURCES = _gen/example_callable.manager _gen/param-spec-struct.h
+CLEANFILES = $(BUILT_SOURCES)
+
+clean-local:
+	rm -rf _gen
diff --git a/tests/lib/callable/conn.c b/tests/lib/callable/conn.c
new file mode 100644
index 0000000..34a3530
--- /dev/null
+++ b/tests/lib/callable/conn.c
@@ -0,0 +1,402 @@
+/*
+ * conn.c - an example connection
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * 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.1 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "conn.h"
+
+#include <string.h>
+
+#include <dbus/dbus-glib.h>
+
+#include <telepathy-glib/dbus.h>
+#include <telepathy-glib/errors.h>
+#include <telepathy-glib/handle-repo-dynamic.h>
+#include <telepathy-glib/handle-repo-static.h>
+#include <telepathy-glib/interfaces.h>
+
+#include "media-manager.h"
+
+G_DEFINE_TYPE_WITH_CODE (ExampleCallableConnection,
+    example_callable_connection,
+    TP_TYPE_BASE_CONNECTION,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_CONTACTS,
+      tp_contacts_mixin_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_PRESENCE,
+      tp_presence_mixin_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CONNECTION_INTERFACE_SIMPLE_PRESENCE,
+      tp_presence_mixin_simple_presence_iface_init))
+
+enum
+{
+  PROP_ACCOUNT = 1,
+  PROP_SIMULATION_DELAY,
+  N_PROPS
+};
+
+struct _ExampleCallableConnectionPrivate
+{
+  gchar *account;
+  guint simulation_delay;
+  gboolean away;
+  gchar *presence_message;
+};
+
+static void
+example_callable_connection_init (ExampleCallableConnection *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      EXAMPLE_TYPE_CALLABLE_CONNECTION,
+      ExampleCallableConnectionPrivate);
+  self->priv->away = FALSE;
+  self->priv->presence_message = g_strdup ("");
+}
+
+static void
+get_property (GObject *object,
+              guint property_id,
+              GValue *value,
+              GParamSpec *spec)
+{
+  ExampleCallableConnection *self = EXAMPLE_CALLABLE_CONNECTION (object);
+
+  switch (property_id)
+    {
+    case PROP_ACCOUNT:
+      g_value_set_string (value, self->priv->account);
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      g_value_set_uint (value, self->priv->simulation_delay);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
+    }
+}
+
+static void
+set_property (GObject *object,
+              guint property_id,
+              const GValue *value,
+              GParamSpec *spec)
+{
+  ExampleCallableConnection *self = EXAMPLE_CALLABLE_CONNECTION (object);
+
+  switch (property_id)
+    {
+    case PROP_ACCOUNT:
+      g_free (self->priv->account);
+      self->priv->account = g_value_dup_string (value);
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      self->priv->simulation_delay = g_value_get_uint (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, spec);
+    }
+}
+
+static void
+finalize (GObject *object)
+{
+  ExampleCallableConnection *self = EXAMPLE_CALLABLE_CONNECTION (object);
+
+  tp_contacts_mixin_finalize (object);
+  g_free (self->priv->account);
+  g_free (self->priv->presence_message);
+
+  G_OBJECT_CLASS (example_callable_connection_parent_class)->finalize (object);
+}
+
+static gchar *
+get_unique_connection_name (TpBaseConnection *conn)
+{
+  ExampleCallableConnection *self = EXAMPLE_CALLABLE_CONNECTION (conn);
+
+  return g_strdup_printf ("%s@%p", self->priv->account, self);
+}
+
+gchar *
+example_callable_normalize_contact (TpHandleRepoIface *repo,
+                                    const gchar *id,
+                                    gpointer context,
+                                    GError **error)
+{
+  if (id[0] == '\0')
+    {
+      g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_HANDLE,
+          "Contact ID must not be empty");
+      return NULL;
+    }
+
+  return g_utf8_normalize (id, -1, G_NORMALIZE_ALL_COMPOSE);
+}
+
+static void
+create_handle_repos (TpBaseConnection *conn,
+                     TpHandleRepoIface *repos[NUM_TP_HANDLE_TYPES])
+{
+  repos[TP_HANDLE_TYPE_CONTACT] = tp_dynamic_handle_repo_new
+      (TP_HANDLE_TYPE_CONTACT, example_callable_normalize_contact, NULL);
+}
+
+static GPtrArray *
+create_channel_managers (TpBaseConnection *conn)
+{
+  ExampleCallableConnection *self = EXAMPLE_CALLABLE_CONNECTION (conn);
+  GPtrArray *ret = g_ptr_array_sized_new (1);
+
+  g_ptr_array_add (ret,
+      g_object_new (EXAMPLE_TYPE_CALLABLE_MEDIA_MANAGER,
+        "connection", conn,
+        "simulation-delay", self->priv->simulation_delay,
+        NULL));
+
+  return ret;
+}
+
+static gboolean
+start_connecting (TpBaseConnection *conn,
+                  GError **error)
+{
+  ExampleCallableConnection *self = EXAMPLE_CALLABLE_CONNECTION (conn);
+  TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (conn,
+      TP_HANDLE_TYPE_CONTACT);
+
+  /* In a real connection manager we'd ask the underlying implementation to
+   * start connecting, then go to state CONNECTED when finished, but here
+   * we can do it immediately. */
+
+  conn->self_handle = tp_handle_ensure (contact_repo, self->priv->account,
+      NULL, error);
+
+  if (conn->self_handle == 0)
+    return FALSE;
+
+  tp_base_connection_change_status (conn, TP_CONNECTION_STATUS_CONNECTED,
+      TP_CONNECTION_STATUS_REASON_REQUESTED);
+
+  return TRUE;
+}
+
+static void
+shut_down (TpBaseConnection *conn)
+{
+  /* In a real connection manager we'd ask the underlying implementation to
+   * start shutting down, then call this function when finished, but here
+   * we can do it immediately. */
+  tp_base_connection_finish_shutdown (conn);
+}
+
+static void
+constructed (GObject *object)
+{
+  TpBaseConnection *base = TP_BASE_CONNECTION (object);
+  void (*chain_up) (GObject *) =
+    G_OBJECT_CLASS (example_callable_connection_parent_class)->constructed;
+
+  if (chain_up != NULL)
+    chain_up (object);
+
+  tp_contacts_mixin_init (object,
+      G_STRUCT_OFFSET (ExampleCallableConnection, contacts_mixin));
+  tp_base_connection_register_with_contacts_mixin (base);
+
+  tp_presence_mixin_init (object,
+      G_STRUCT_OFFSET (ExampleCallableConnection, presence_mixin));
+  tp_presence_mixin_simple_presence_register_with_contacts_mixin (object);
+}
+
+static gboolean
+status_available (GObject *object,
+                  guint index_)
+{
+  TpBaseConnection *base = TP_BASE_CONNECTION (object);
+
+  if (base->status != TP_CONNECTION_STATUS_CONNECTED)
+    return FALSE;
+
+  return TRUE;
+}
+
+static GHashTable *
+get_contact_statuses (GObject *object,
+                      const GArray *contacts,
+                      GError **error)
+{
+  ExampleCallableConnection *self =
+    EXAMPLE_CALLABLE_CONNECTION (object);
+  TpBaseConnection *base = TP_BASE_CONNECTION (object);
+  guint i;
+  GHashTable *result = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+      NULL, (GDestroyNotify) tp_presence_status_free);
+
+  for (i = 0; i < contacts->len; i++)
+    {
+      TpHandle contact = g_array_index (contacts, guint, i);
+      ExampleCallablePresence presence;
+      GHashTable *parameters;
+
+      parameters = g_hash_table_new_full (g_str_hash,
+          g_str_equal, NULL, (GDestroyNotify) tp_g_value_slice_free);
+
+      /* we know our own status from the connection; for this example CM,
+       * everyone else's status is assumed to be "available" */
+      if (contact == base->self_handle)
+        {
+          presence = (self->priv->away ? EXAMPLE_CALLABLE_PRESENCE_AWAY
+              : EXAMPLE_CALLABLE_PRESENCE_AVAILABLE);
+
+          if (self->priv->presence_message[0] != '\0')
+            g_hash_table_insert (parameters, "message",
+                tp_g_value_slice_new_string (self->priv->presence_message));
+        }
+      else
+        {
+          presence = EXAMPLE_CALLABLE_PRESENCE_AVAILABLE;
+        }
+
+      g_hash_table_insert (result, GUINT_TO_POINTER (contact),
+          tp_presence_status_new (presence, parameters));
+      g_hash_table_destroy (parameters);
+    }
+
+  return result;
+}
+
+static gboolean
+set_own_status (GObject *object,
+                const TpPresenceStatus *status,
+                GError **error)
+{
+  ExampleCallableConnection *self =
+    EXAMPLE_CALLABLE_CONNECTION (object);
+  TpBaseConnection *base = TP_BASE_CONNECTION (object);
+  GHashTable *presences;
+  const gchar *message = "";
+
+  if (status->optional_arguments != NULL)
+    {
+      GValue *v = g_hash_table_lookup (status->optional_arguments, "message");
+
+      if (v != NULL && G_VALUE_HOLDS_STRING (v))
+        {
+          message = g_value_get_string (v);
+
+          if (message == NULL)
+            message = "";
+        }
+    }
+
+  if (status->index == EXAMPLE_CALLABLE_PRESENCE_AWAY)
+    {
+      if (self->priv->away && !tp_strdiff (message,
+            self->priv->presence_message))
+        return TRUE;
+
+      self->priv->away = TRUE;
+    }
+  else
+    {
+      if (!self->priv->away && !tp_strdiff (message,
+            self->priv->presence_message))
+        return TRUE;
+
+      self->priv->away = FALSE;
+    }
+
+  g_free (self->priv->presence_message);
+  self->priv->presence_message = g_strdup (message);
+
+  presences = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+      NULL, NULL);
+  g_hash_table_insert (presences, GUINT_TO_POINTER (base->self_handle),
+      (gpointer) status);
+  tp_presence_mixin_emit_presence_update (object, presences);
+  g_hash_table_destroy (presences);
+  return TRUE;
+}
+
+static const TpPresenceStatusOptionalArgumentSpec can_have_message[] = {
+      { "message", "s", NULL, NULL },
+      { NULL }
+};
+
+/* Must be kept in sync with ExampleCallablePresence enum in header */
+static const TpPresenceStatusSpec presence_statuses[] = {
+      { "offline", TP_CONNECTION_PRESENCE_TYPE_OFFLINE, FALSE, NULL },
+      { "unknown", TP_CONNECTION_PRESENCE_TYPE_UNKNOWN, FALSE, NULL },
+      { "error", TP_CONNECTION_PRESENCE_TYPE_ERROR, FALSE, NULL },
+      { "away", TP_CONNECTION_PRESENCE_TYPE_AWAY, TRUE, can_have_message },
+      { "available", TP_CONNECTION_PRESENCE_TYPE_AVAILABLE, TRUE,
+        can_have_message },
+      { NULL }
+};
+
+static void
+example_callable_connection_class_init (
+    ExampleCallableConnectionClass *klass)
+{
+  static const gchar *interfaces_always_present[] = {
+      TP_IFACE_CONNECTION_INTERFACE_CONTACTS,
+      TP_IFACE_CONNECTION_INTERFACE_PRESENCE,
+      TP_IFACE_CONNECTION_INTERFACE_REQUESTS,
+      TP_IFACE_CONNECTION_INTERFACE_SIMPLE_PRESENCE,
+      NULL };
+  TpBaseConnectionClass *base_class = (TpBaseConnectionClass *) klass;
+  GObjectClass *object_class = (GObjectClass *) klass;
+  GParamSpec *param_spec;
+
+  object_class->get_property = get_property;
+  object_class->set_property = set_property;
+  object_class->constructed = constructed;
+  object_class->finalize = finalize;
+  g_type_class_add_private (klass,
+      sizeof (ExampleCallableConnectionPrivate));
+
+  base_class->create_handle_repos = create_handle_repos;
+  base_class->get_unique_connection_name = get_unique_connection_name;
+  base_class->create_channel_managers = create_channel_managers;
+  base_class->start_connecting = start_connecting;
+  base_class->shut_down = shut_down;
+  base_class->interfaces_always_present = interfaces_always_present;
+
+  param_spec = g_param_spec_string ("account", "Account name",
+      "The username of this user", NULL,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_ACCOUNT, param_spec);
+
+  param_spec = g_param_spec_uint ("simulation-delay", "Simulation delay",
+      "Delay between simulated network events",
+      0, G_MAXUINT32, 1000,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_SIMULATION_DELAY,
+      param_spec);
+
+  tp_contacts_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (ExampleCallableConnectionClass, contacts_mixin));
+  tp_presence_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (ExampleCallableConnectionClass, presence_mixin),
+      status_available, get_contact_statuses, set_own_status,
+      presence_statuses);
+  tp_presence_mixin_simple_presence_init_dbus_properties (object_class);
+}
diff --git a/tests/lib/callable/conn.h b/tests/lib/callable/conn.h
new file mode 100644
index 0000000..f3d4690
--- /dev/null
+++ b/tests/lib/callable/conn.h
@@ -0,0 +1,78 @@
+/*
+ * conn.h - header for an example connection
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * Copying and distribution of this file, with or without modification,
+ * are permitted in any medium without royalty provided the copyright
+ * notice and this notice are preserved.
+ */
+
+#ifndef __EXAMPLE_CALLABLE_CONN_H__
+#define __EXAMPLE_CALLABLE_CONN_H__
+
+#include <glib-object.h>
+#include <telepathy-glib/base-connection.h>
+#include <telepathy-glib/contacts-mixin.h>
+#include <telepathy-glib/presence-mixin.h>
+
+G_BEGIN_DECLS
+
+typedef struct _ExampleCallableConnection ExampleCallableConnection;
+typedef struct _ExampleCallableConnectionPrivate
+    ExampleCallableConnectionPrivate;
+
+typedef struct _ExampleCallableConnectionClass ExampleCallableConnectionClass;
+typedef struct _ExampleCallableConnectionClassPrivate
+    ExampleCallableConnectionClassPrivate;
+
+struct _ExampleCallableConnectionClass {
+    TpBaseConnectionClass parent_class;
+    TpPresenceMixinClass presence_mixin;
+    TpContactsMixinClass contacts_mixin;
+
+    ExampleCallableConnectionClassPrivate *priv;
+};
+
+struct _ExampleCallableConnection {
+    TpBaseConnection parent;
+    TpPresenceMixin presence_mixin;
+    TpContactsMixin contacts_mixin;
+
+    ExampleCallableConnectionPrivate *priv;
+};
+
+GType example_callable_connection_get_type (void);
+
+#define EXAMPLE_TYPE_CALLABLE_CONNECTION \
+  (example_callable_connection_get_type ())
+#define EXAMPLE_CALLABLE_CONNECTION(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj), EXAMPLE_TYPE_CALLABLE_CONNECTION, \
+                              ExampleCallableConnection))
+#define EXAMPLE_CALLABLE_CONNECTION_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST((klass), EXAMPLE_TYPE_CALLABLE_CONNECTION, \
+                           ExampleCallableConnectionClass))
+#define EXAMPLE_IS_CALLABLE_CONNECTION(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((obj), EXAMPLE_TYPE_CALLABLE_CONNECTION))
+#define EXAMPLE_IS_CALLABLE_CONNECTION_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE((klass), EXAMPLE_TYPE_CALLABLE_CONNECTION))
+#define EXAMPLE_CALLABLE_CONNECTION_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), EXAMPLE_TYPE_CALLABLE_CONNECTION, \
+                              ExampleCallableConnectionClass))
+
+gchar *example_callable_normalize_contact (TpHandleRepoIface *repo,
+    const gchar *id, gpointer context, GError **error);
+
+/* Must be kept in sync with the array presence_statuses in conn.c */
+typedef enum {
+    EXAMPLE_CALLABLE_PRESENCE_OFFLINE = 0,
+    EXAMPLE_CALLABLE_PRESENCE_UNKNOWN,
+    EXAMPLE_CALLABLE_PRESENCE_ERROR,
+    EXAMPLE_CALLABLE_PRESENCE_AWAY,
+    EXAMPLE_CALLABLE_PRESENCE_AVAILABLE
+} ExampleCallablePresence;
+
+G_END_DECLS
+
+#endif
diff --git a/tests/lib/callable/connection-manager.c b/tests/lib/callable/connection-manager.c
new file mode 100644
index 0000000..fa8e0f0
--- /dev/null
+++ b/tests/lib/callable/connection-manager.c
@@ -0,0 +1,130 @@
+/*
+ * manager.c - an example connection manager
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * 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.1 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "connection-manager.h"
+
+#include <dbus/dbus-glib.h>
+
+#include <telepathy-glib/dbus.h>
+#include <telepathy-glib/errors.h>
+
+#include "conn.h"
+
+G_DEFINE_TYPE (ExampleCallableConnectionManager,
+    example_callable_connection_manager,
+    TP_TYPE_BASE_CONNECTION_MANAGER)
+
+struct _ExampleCallableConnectionManagerPrivate
+{
+  int dummy;
+};
+
+static void
+example_callable_connection_manager_init (
+    ExampleCallableConnectionManager *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      EXAMPLE_TYPE_CALLABLE_CONNECTION_MANAGER,
+      ExampleCallableConnectionManagerPrivate);
+}
+
+typedef struct {
+    gchar *account;
+    guint simulation_delay;
+} ExampleParams;
+
+static gboolean
+account_param_filter (const TpCMParamSpec *paramspec,
+                      GValue *value,
+                      GError **error)
+{
+  const gchar *id = g_value_get_string (value);
+
+  g_value_take_string (value,
+      example_callable_normalize_contact (NULL, id, NULL, error));
+
+  if (g_value_get_string (value) == NULL)
+    return FALSE;
+
+  return TRUE;
+}
+
+#include "_gen/param-spec-struct.h"
+
+static gpointer
+alloc_params (void)
+{
+  ExampleParams *params = g_slice_new0 (ExampleParams);
+
+  params->simulation_delay = 1000;
+  return params;
+}
+
+static void
+free_params (gpointer p)
+{
+  ExampleParams *params = p;
+
+  g_free (params->account);
+
+  g_slice_free (ExampleParams, params);
+}
+
+static const TpCMProtocolSpec example_protocols[] = {
+  { "example", example_callable_example_params,
+    alloc_params, free_params },
+  { NULL, NULL }
+};
+
+static TpBaseConnection *
+new_connection (TpBaseConnectionManager *self,
+                const gchar *proto,
+                TpIntSet *params_present,
+                gpointer parsed_params,
+                GError **error)
+{
+  ExampleParams *params = parsed_params;
+  ExampleCallableConnection *conn;
+
+  conn = EXAMPLE_CALLABLE_CONNECTION
+      (g_object_new (EXAMPLE_TYPE_CALLABLE_CONNECTION,
+          "account", params->account,
+          "simulation-delay", params->simulation_delay,
+          "protocol", proto,
+          NULL));
+
+  return (TpBaseConnection *) conn;
+}
+
+static void
+example_callable_connection_manager_class_init (
+    ExampleCallableConnectionManagerClass *klass)
+{
+  TpBaseConnectionManagerClass *base_class =
+      (TpBaseConnectionManagerClass *) klass;
+
+  g_type_class_add_private (klass,
+      sizeof (ExampleCallableConnectionManagerPrivate));
+
+  base_class->new_connection = new_connection;
+  base_class->cm_dbus_name = "example_callable";
+  base_class->protocol_params = example_protocols;
+}
diff --git a/tests/lib/callable/connection-manager.h b/tests/lib/callable/connection-manager.h
new file mode 100644
index 0000000..5d854cb
--- /dev/null
+++ b/tests/lib/callable/connection-manager.h
@@ -0,0 +1,73 @@
+/*
+ * manager.h - header for an example connection manager
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * 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.1 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef __EXAMPLE_CALLABLE_CONNECTION_MANAGER_H__
+#define __EXAMPLE_CALLABLE_CONNECTION_MANAGER_H__
+
+#include <glib-object.h>
+#include <telepathy-glib/base-connection-manager.h>
+
+G_BEGIN_DECLS
+
+typedef struct _ExampleCallableConnectionManager
+    ExampleCallableConnectionManager;
+typedef struct _ExampleCallableConnectionManagerPrivate
+    ExampleCallableConnectionManagerPrivate;
+
+typedef struct _ExampleCallableConnectionManagerClass
+    ExampleCallableConnectionManagerClass;
+typedef struct _ExampleCallableConnectionManagerClassPrivate
+    ExampleCallableConnectionManagerClassPrivate;
+
+struct _ExampleCallableConnectionManagerClass {
+    TpBaseConnectionManagerClass parent_class;
+
+    ExampleCallableConnectionManagerClassPrivate *priv;
+};
+
+struct _ExampleCallableConnectionManager {
+    TpBaseConnectionManager parent;
+
+    ExampleCallableConnectionManagerPrivate *priv;
+};
+
+GType example_callable_connection_manager_get_type (void);
+
+/* TYPE MACROS */
+#define EXAMPLE_TYPE_CALLABLE_CONNECTION_MANAGER \
+  (example_callable_connection_manager_get_type ())
+#define EXAMPLE_CALLABLE_CONNECTION_MANAGER(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj), EXAMPLE_TYPE_CALLABLE_CONNECTION_MANAGER, \
+                              ExampleCallableConnectionManager))
+#define EXAMPLE_CALLABLE_CONNECTION_MANAGER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST((klass), EXAMPLE_TYPE_CALLABLE_CONNECTION_MANAGER, \
+                           ExampleCallableConnectionManagerClass))
+#define EXAMPLE_IS_CALLABLE_CONNECTION_MANAGER(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((obj), EXAMPLE_TYPE_CALLABLE_CONNECTION_MANAGER))
+#define EXAMPLE_IS_CALLABLE_CONNECTION_MANAGER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE((klass), EXAMPLE_TYPE_CALLABLE_CONNECTION_MANAGER))
+#define EXAMPLE_CALLABLE_CONNECTION_MANAGER_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), EXAMPLE_TYPE_CALLABLE_CONNECTION_MANAGER, \
+                              ExampleCallableConnectionManagerClass))
+
+G_END_DECLS
+
+#endif
diff --git a/tests/lib/callable/media-channel.c b/tests/lib/callable/media-channel.c
new file mode 100644
index 0000000..5f305cf
--- /dev/null
+++ b/tests/lib/callable/media-channel.c
@@ -0,0 +1,1024 @@
+/*
+ * media-channel.c - an example 1-1 streamed media call.
+ *
+ * For simplicity, this channel emulates a device with its own
+ * audio/video user interface, like a video-equipped form of the phones
+ * manipulated by telepathy-snom or gnome-phone-manager.
+ *
+ * As a result, this channel does not have the MediaSignalling interface, and
+ * clients should not attempt to do their own streaming using
+ * telepathy-farsight, telepathy-stream-engine or maemo-stream-engine.
+ *
+ * In practice, nearly all connection managers also have the MediaSignalling
+ * interface on their streamed media channels. Usage for those CMs is the
+ * same, except that whichever client is the primary handler for the channel
+ * should also hand the channel over to telepathy-farsight or
+ * telepathy-stream-engine to implement the actual streaming.
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * 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.1 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "media-channel.h"
+
+#include "media-stream.h"
+
+#include <telepathy-glib/base-connection.h>
+#include <telepathy-glib/channel-iface.h>
+#include <telepathy-glib/dbus.h>
+#include <telepathy-glib/interfaces.h>
+#include <telepathy-glib/svc-channel.h>
+#include <telepathy-glib/svc-generic.h>
+
+static void media_iface_init (gpointer iface, gpointer data);
+static void channel_iface_init (gpointer iface, gpointer data);
+
+G_DEFINE_TYPE_WITH_CODE (ExampleCallableMediaChannel,
+    example_callable_media_channel,
+    G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES,
+      tp_dbus_properties_mixin_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL, channel_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_TYPE_STREAMED_MEDIA,
+      media_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_INTERFACE_GROUP,
+      tp_group_mixin_iface_init);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_IFACE, NULL);
+    G_IMPLEMENT_INTERFACE (TP_TYPE_EXPORTABLE_CHANNEL, NULL))
+
+enum
+{
+  PROP_OBJECT_PATH = 1,
+  PROP_CHANNEL_TYPE,
+  PROP_HANDLE_TYPE,
+  PROP_HANDLE,
+  PROP_TARGET_ID,
+  PROP_REQUESTED,
+  PROP_INITIATOR_HANDLE,
+  PROP_INITIATOR_ID,
+  PROP_CONNECTION,
+  PROP_INTERFACES,
+  PROP_CHANNEL_DESTROYED,
+  PROP_CHANNEL_PROPERTIES,
+  PROP_SIMULATION_DELAY,
+  N_PROPS
+};
+
+enum
+{
+  SIGNAL_CALL_TERMINATED,
+  N_SIGNALS
+};
+
+typedef enum {
+    PROGRESS_NONE,
+    PROGRESS_CALLING,
+    PROGRESS_ACTIVE,
+    PROGRESS_ENDED
+} ExampleCallableCallProgress;
+
+static guint signals[N_SIGNALS] = { 0 };
+
+struct _ExampleCallableMediaChannelPrivate
+{
+  TpBaseConnection *conn;
+  gchar *object_path;
+  TpHandle handle;
+  TpHandle initiator;
+  ExampleCallableCallProgress progress;
+
+  guint simulation_delay;
+
+  guint next_stream_id;
+
+  GHashTable *streams;
+
+  gboolean locally_requested;
+  gboolean disposed;
+};
+
+static const char * example_callable_media_channel_interfaces[] = {
+    TP_IFACE_CHANNEL_INTERFACE_GROUP,
+    NULL
+};
+
+static void
+example_callable_media_channel_init (ExampleCallableMediaChannel *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      EXAMPLE_TYPE_CALLABLE_MEDIA_CHANNEL,
+      ExampleCallableMediaChannelPrivate);
+
+  self->priv->next_stream_id = 1;
+  self->priv->streams = g_hash_table_new_full (g_direct_hash, g_direct_equal,
+      NULL, g_object_unref);
+}
+
+static void
+constructed (GObject *object)
+{
+  void (*chain_up) (GObject *) =
+      ((GObjectClass *) example_callable_media_channel_parent_class)->constructed;
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (object);
+  TpHandleRepoIface *contact_repo = tp_base_connection_get_handles
+      (self->priv->conn, TP_HANDLE_TYPE_CONTACT);
+  DBusGConnection *bus;
+  TpIntSet *members;
+  TpIntSet *local_pending;
+
+  if (chain_up != NULL)
+    chain_up (object);
+
+  tp_handle_ref (contact_repo, self->priv->handle);
+  tp_handle_ref (contact_repo, self->priv->initiator);
+
+  bus = tp_get_bus ();
+  dbus_g_connection_register_g_object (bus, self->priv->object_path, object);
+
+  tp_group_mixin_init (object,
+      G_STRUCT_OFFSET (ExampleCallableMediaChannel, group),
+      contact_repo, self->priv->conn->self_handle);
+
+  /* Initially, the channel contains the initiator as a member; they are also
+   * the actor for the change that adds any initial members. */
+
+  members = tp_intset_new_containing (self->priv->initiator);
+
+  if (self->priv->locally_requested)
+    {
+      /* Nobody is locally pending. The remote peer will turn up in
+       * remote-pending state when we actually contact them, which is done
+       * in RequestStreams */
+      self->priv->progress = PROGRESS_NONE;
+      local_pending = NULL;
+    }
+  else
+    {
+      /* This is an incoming call, so the self-handle is locally
+       * pending, to indicate that we need to answer. */
+      self->priv->progress = PROGRESS_CALLING;
+      local_pending = tp_intset_new_containing (self->priv->conn->self_handle);
+    }
+
+  tp_group_mixin_change_members (object, "",
+      members /* added */,
+      NULL /* nobody removed */,
+      local_pending, /* added to local-pending */
+      NULL /* nobody added to remote-pending */,
+      self->priv->initiator /* actor */, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_intset_destroy (members);
+
+  if (local_pending != NULL)
+    tp_intset_destroy (local_pending);
+
+  /* We don't need to allow adding or removing members to this Group in ways
+   * that need flags set, so the only flag we set is to say we support the
+   * Properties interface to the Group.
+   *
+   * It doesn't make sense to add anyone to the Group, since we already know
+   * who we're going to call (or were called by). The only call to AddMembers
+   * we need to support is to move ourselves from local-pending to member in
+   * the incoming call case, and that's always allowed anyway.
+   *
+   * (Connection managers that support the various backwards-compatible
+   * ways to make an outgoing StreamedMedia channel have to support adding the
+   * peer to remote-pending, but that has no actual effect other than to
+   * obscure what's going on; in this one, there's no need to support that
+   * usage.)
+   *
+   * Similarly, it doesn't make sense to remove anyone from this Group apart
+   * from ourselves (to hang up), and removing the SelfHandle is always
+   * allowed anyway.
+   */
+  tp_group_mixin_change_flags (object, TP_CHANNEL_GROUP_FLAG_PROPERTIES, 0);
+
+  if (!self->priv->locally_requested)
+    {
+      /* FIXME: act on any streams that the remote peer has already enabled */
+    }
+}
+
+static void
+get_property (GObject *object,
+              guint property_id,
+              GValue *value,
+              GParamSpec *pspec)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (object);
+
+  switch (property_id)
+    {
+    case PROP_OBJECT_PATH:
+      g_value_set_string (value, self->priv->object_path);
+      break;
+
+    case PROP_CHANNEL_TYPE:
+      g_value_set_static_string (value, TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA);
+      break;
+
+    case PROP_HANDLE_TYPE:
+      g_value_set_uint (value, TP_HANDLE_TYPE_CONTACT);
+      break;
+
+    case PROP_HANDLE:
+      g_value_set_uint (value, self->priv->handle);
+      break;
+
+    case PROP_TARGET_ID:
+        {
+          TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (
+              self->priv->conn, TP_HANDLE_TYPE_CONTACT);
+
+          g_value_set_string (value,
+              tp_handle_inspect (contact_repo, self->priv->handle));
+        }
+      break;
+
+    case PROP_REQUESTED:
+      g_value_set_boolean (value, self->priv->locally_requested);
+      break;
+
+    case PROP_INITIATOR_HANDLE:
+      g_value_set_uint (value, self->priv->initiator);
+      break;
+
+    case PROP_INITIATOR_ID:
+        {
+          TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (
+              self->priv->conn, TP_HANDLE_TYPE_CONTACT);
+
+          g_value_set_string (value,
+              tp_handle_inspect (contact_repo, self->priv->initiator));
+        }
+      break;
+
+    case PROP_CONNECTION:
+      g_value_set_object (value, self->priv->conn);
+      break;
+
+    case PROP_INTERFACES:
+      g_value_set_boxed (value, example_callable_media_channel_interfaces);
+      break;
+
+    case PROP_CHANNEL_DESTROYED:
+      g_value_set_boolean (value, (self->priv->progress == PROGRESS_ENDED));
+      break;
+
+    case PROP_CHANNEL_PROPERTIES:
+      g_value_take_boxed (value,
+          tp_dbus_properties_mixin_make_properties_hash (object,
+              TP_IFACE_CHANNEL, "ChannelType",
+              TP_IFACE_CHANNEL, "TargetHandleType",
+              TP_IFACE_CHANNEL, "TargetHandle",
+              TP_IFACE_CHANNEL, "TargetID",
+              TP_IFACE_CHANNEL, "InitiatorHandle",
+              TP_IFACE_CHANNEL, "InitiatorID",
+              TP_IFACE_CHANNEL, "Requested",
+              TP_IFACE_CHANNEL, "Interfaces",
+              NULL));
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      g_value_set_uint (value, self->priv->simulation_delay);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+set_property (GObject *object,
+              guint property_id,
+              const GValue *value,
+              GParamSpec *pspec)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (object);
+
+  switch (property_id)
+    {
+    case PROP_OBJECT_PATH:
+      g_assert (self->priv->object_path == NULL);
+      self->priv->object_path = g_value_dup_string (value);
+      break;
+
+    case PROP_HANDLE:
+      /* we don't ref it here because we don't necessarily have access to the
+       * contact repo yet - instead we ref it in the constructor.
+       */
+      self->priv->handle = g_value_get_uint (value);
+      break;
+
+    case PROP_INITIATOR_HANDLE:
+      /* likewise */
+      self->priv->initiator = g_value_get_uint (value);
+      break;
+
+    case PROP_REQUESTED:
+      self->priv->locally_requested = g_value_get_boolean (value);
+      break;
+
+    case PROP_HANDLE_TYPE:
+    case PROP_CHANNEL_TYPE:
+      /* these properties are writable in the interface, but not actually
+       * meaningfully changable on this channel, so we do nothing */
+      break;
+
+    case PROP_CONNECTION:
+      self->priv->conn = g_value_get_object (value);
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      self->priv->simulation_delay = g_value_get_uint (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+example_callable_media_channel_close (ExampleCallableMediaChannel *self,
+                                      TpHandle actor,
+                                      TpChannelGroupChangeReason reason)
+{
+  if (self->priv->progress != PROGRESS_ENDED)
+    {
+      TpIntSet *everyone;
+      const gchar *send_reason;
+
+      self->priv->progress = PROGRESS_ENDED;
+
+      /* In a real protocol these would be some sort of real protocol construct,
+       * like an XMPP error stanza or a SIP error code */
+      switch (reason)
+        {
+        case TP_CHANNEL_GROUP_CHANGE_REASON_BUSY:
+          send_reason = "<user-is-busy/>";
+          break;
+
+        case TP_CHANNEL_GROUP_CHANGE_REASON_NO_ANSWER:
+          send_reason = "<no-answer/>";
+          break;
+
+        default:
+          send_reason = "<call-terminated/>";
+        }
+
+      everyone = tp_intset_new_containing (self->priv->handle);
+      tp_intset_add (everyone, self->group.self_handle);
+      tp_group_mixin_change_members ((GObject *) self, "",
+          NULL /* nobody added */,
+          everyone /* removed */,
+          NULL /* nobody locally pending */,
+          NULL /* nobody remotely pending */,
+          actor,
+          reason);
+      tp_intset_destroy (everyone);
+
+      g_message ("SIGNALLING: send: Terminating call: %s", send_reason);
+      g_signal_emit (self, signals[SIGNAL_CALL_TERMINATED], 0);
+      tp_svc_channel_emit_closed (self);
+    }
+}
+
+static void
+dispose (GObject *object)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (object);
+
+  if (self->priv->disposed)
+    return;
+
+  self->priv->disposed = TRUE;
+
+  example_callable_media_channel_close (self, 0,
+      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+
+  ((GObjectClass *) example_callable_media_channel_parent_class)->dispose (object);
+}
+
+static void
+finalize (GObject *object)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (object);
+  TpHandleRepoIface *contact_handles = tp_base_connection_get_handles
+      (self->priv->conn, TP_HANDLE_TYPE_CONTACT);
+
+  tp_handle_unref (contact_handles, self->priv->handle);
+  tp_handle_unref (contact_handles, self->priv->initiator);
+
+  g_free (self->priv->object_path);
+
+  tp_group_mixin_finalize (object);
+
+  ((GObjectClass *) example_callable_media_channel_parent_class)->finalize (object);
+}
+
+static gboolean
+add_member (GObject *object,
+            TpHandle member,
+            const gchar *message,
+            GError **error)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (object);
+  TpHandleRepoIface *contact_repo = tp_base_connection_get_handles
+      (self->priv->conn, TP_HANDLE_TYPE_CONTACT);
+
+  /* In connection managers that supported the RequestChannel method for
+   * streamed media channels, it would be necessary to support adding the
+   * called contact to the members of an outgoing call. However, in this
+   * legacy-free example, we don't support that usage, so the only use for
+   * AddMembers is to accept an incoming call.
+   */
+
+  if (member == self->group.self_handle &&
+      tp_handle_set_is_member (self->group.local_pending, member))
+    {
+      /* We're in local-pending, move to members to accept. */
+      TpIntSet *set = tp_intset_new_containing (member);
+
+      g_message ("SIGNALLING: send: Accepting incoming call from %s",
+          tp_handle_inspect (contact_repo, self->priv->handle));
+
+      tp_group_mixin_change_members (object, "",
+          set /* added */,
+          NULL /* nobody removed */,
+          NULL /* nobody added to local pending */,
+          NULL /* nobody added to remote pending */,
+          member /* actor */, TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+
+      return TRUE;
+    }
+
+  /* Otherwise it's a meaningless request, so reject it. */
+  g_set_error (error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+      "Cannot add handle %u to channel", member);
+  return FALSE;
+}
+
+static gboolean
+remove_member_with_reason (GObject *object,
+                           TpHandle member,
+                           const gchar *message,
+                           guint reason,
+                           GError **error)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (object);
+
+  /* The TpGroupMixin won't call this unless removing the member is allowed
+   * by the group flags, which in this case means it must be our own handle
+   * (because the other user never appears in local-pending).
+   */
+
+  g_assert (member == self->group.self_handle);
+
+  example_callable_media_channel_close (self, self->group.self_handle, reason);
+  return TRUE;
+}
+
+static void
+example_callable_media_channel_class_init (ExampleCallableMediaChannelClass *klass)
+{
+  static TpDBusPropertiesMixinPropImpl channel_props[] = {
+      { "TargetHandleType", "handle-type", NULL },
+      { "TargetHandle", "handle", NULL },
+      { "ChannelType", "channel-type", NULL },
+      { "Interfaces", "interfaces", NULL },
+      { "TargetID", "target-id", NULL },
+      { "Requested", "requested", NULL },
+      { "InitiatorHandle", "initiator-handle", NULL },
+      { "InitiatorID", "initiator-id", NULL },
+      { NULL }
+  };
+  static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = {
+      { TP_IFACE_CHANNEL,
+        tp_dbus_properties_mixin_getter_gobject_properties,
+        NULL,
+        channel_props,
+      },
+      { NULL }
+  };
+  GObjectClass *object_class = (GObjectClass *) klass;
+  GParamSpec *param_spec;
+
+  g_type_class_add_private (klass,
+      sizeof (ExampleCallableMediaChannelPrivate));
+
+  object_class->constructed = constructed;
+  object_class->set_property = set_property;
+  object_class->get_property = get_property;
+  object_class->dispose = dispose;
+  object_class->finalize = finalize;
+
+  g_object_class_override_property (object_class, PROP_OBJECT_PATH,
+      "object-path");
+  g_object_class_override_property (object_class, PROP_CHANNEL_TYPE,
+      "channel-type");
+  g_object_class_override_property (object_class, PROP_HANDLE_TYPE,
+      "handle-type");
+  g_object_class_override_property (object_class, PROP_HANDLE, "handle");
+
+  g_object_class_override_property (object_class, PROP_CHANNEL_DESTROYED,
+      "channel-destroyed");
+  g_object_class_override_property (object_class, PROP_CHANNEL_PROPERTIES,
+      "channel-properties");
+
+  param_spec = g_param_spec_object ("connection", "TpBaseConnection object",
+      "Connection object that owns this channel",
+      TP_TYPE_BASE_CONNECTION,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_CONNECTION, param_spec);
+
+  param_spec = g_param_spec_boxed ("interfaces", "Extra D-Bus interfaces",
+      "Additional Channel.Interface.* interfaces",
+      G_TYPE_STRV,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_INTERFACES, param_spec);
+
+  param_spec = g_param_spec_string ("target-id", "Peer's ID",
+      "The string obtained by inspecting the target handle",
+      NULL,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_TARGET_ID, param_spec);
+
+  param_spec = g_param_spec_uint ("initiator-handle", "Initiator's handle",
+      "The contact who initiated the channel",
+      0, G_MAXUINT32, 0,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_INITIATOR_HANDLE,
+      param_spec);
+
+  param_spec = g_param_spec_string ("initiator-id", "Initiator's ID",
+      "The string obtained by inspecting the initiator-handle",
+      NULL,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_INITIATOR_ID,
+      param_spec);
+
+  param_spec = g_param_spec_boolean ("requested", "Requested?",
+      "True if this channel was requested by the local user",
+      FALSE,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_REQUESTED, param_spec);
+
+  param_spec = g_param_spec_uint ("simulation-delay", "Simulation delay",
+      "Delay between simulated network events",
+      0, G_MAXUINT32, 1000,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_SIMULATION_DELAY,
+      param_spec);
+
+  signals[SIGNAL_CALL_TERMINATED] = g_signal_new ("call-terminated",
+      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+      g_cclosure_marshal_VOID__VOID,
+      G_TYPE_NONE, 0);
+
+  klass->dbus_properties_class.interfaces = prop_interfaces;
+  tp_dbus_properties_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (ExampleCallableMediaChannelClass,
+        dbus_properties_class));
+
+  tp_group_mixin_class_init (object_class,
+      G_STRUCT_OFFSET (ExampleCallableMediaChannelClass, group_class),
+      add_member,
+      NULL);
+  tp_group_mixin_class_allow_self_removal (object_class);
+  tp_group_mixin_class_set_remove_with_reason_func (object_class,
+      remove_member_with_reason);
+  tp_group_mixin_init_dbus_properties (object_class);
+}
+
+static void
+channel_close (TpSvcChannel *iface,
+               DBusGMethodInvocation *context)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (iface);
+
+  example_callable_media_channel_close (self, self->group.self_handle,
+      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_svc_channel_return_from_close (context);
+}
+
+static void
+channel_get_channel_type (TpSvcChannel *iface G_GNUC_UNUSED,
+                          DBusGMethodInvocation *context)
+{
+  tp_svc_channel_return_from_get_channel_type (context,
+      TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA);
+}
+
+static void
+channel_get_handle (TpSvcChannel *iface,
+                    DBusGMethodInvocation *context)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (iface);
+
+  tp_svc_channel_return_from_get_handle (context, TP_HANDLE_TYPE_CONTACT,
+      self->priv->handle);
+}
+
+static void
+channel_get_interfaces (TpSvcChannel *iface G_GNUC_UNUSED,
+                        DBusGMethodInvocation *context)
+{
+  tp_svc_channel_return_from_get_interfaces (context,
+      example_callable_media_channel_interfaces);
+}
+
+static void
+channel_iface_init (gpointer iface,
+                    gpointer data)
+{
+  TpSvcChannelClass *klass = iface;
+
+#define IMPLEMENT(x) tp_svc_channel_implement_##x (klass, channel_##x)
+  IMPLEMENT (close);
+  IMPLEMENT (get_channel_type);
+  IMPLEMENT (get_handle);
+  IMPLEMENT (get_interfaces);
+#undef IMPLEMENT
+}
+
+static void
+media_list_streams (TpSvcChannelTypeStreamedMedia *iface,
+                    DBusGMethodInvocation *context)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (iface);
+  GPtrArray *array = g_ptr_array_sized_new (g_hash_table_size (
+        self->priv->streams));
+  GHashTableIter iter;
+  gpointer v;
+
+  g_hash_table_iter_init (&iter, self->priv->streams);
+
+  while (g_hash_table_iter_next (&iter, NULL, &v))
+    {
+      ExampleCallableMediaStream *stream = v;
+      GValueArray *va;
+
+      g_object_get (stream,
+          "stream-info", &va,
+          NULL);
+
+      g_ptr_array_add (array, va);
+    }
+
+  tp_svc_channel_type_streamed_media_return_from_list_streams (context,
+      array);
+  g_ptr_array_foreach (array, (GFunc) g_value_array_free, NULL);
+  g_ptr_array_free (array, TRUE);
+}
+
+static void
+media_remove_streams (TpSvcChannelTypeStreamedMedia *iface,
+                      const GArray *stream_ids,
+                      DBusGMethodInvocation *context)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (iface);
+  guint i;
+
+  for (i = 0; i < stream_ids->len; i++)
+    {
+      guint id = g_array_index (stream_ids, guint, i);
+
+      if (g_hash_table_lookup (self->priv->streams,
+            GUINT_TO_POINTER (id)) == NULL)
+        {
+          GError *error = g_error_new (TP_ERRORS, TP_ERROR_INVALID_ARGUMENT,
+              "No stream with ID %u in this channel", id);
+
+          dbus_g_method_return_error (context, error);
+          g_error_free (error);
+          return;
+        }
+    }
+
+  for (i = 0; i < stream_ids->len; i++)
+    {
+      guint id = g_array_index (stream_ids, guint, i);
+
+      example_callable_media_stream_close (
+          g_hash_table_lookup (self->priv->streams, GUINT_TO_POINTER (id)));
+    }
+
+  tp_svc_channel_type_streamed_media_return_from_remove_streams (context);
+}
+
+static void
+media_request_stream_direction (TpSvcChannelTypeStreamedMedia *iface,
+                                guint stream_id,
+                                guint stream_direction,
+                                DBusGMethodInvocation *context)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (iface);
+  ExampleCallableMediaStream *stream = g_hash_table_lookup (
+      self->priv->streams, GUINT_TO_POINTER (stream_id));
+  GError *error = NULL;
+
+  if (stream == NULL)
+    {
+      g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT,
+          "No stream with ID %u in this channel", stream_id);
+      goto error;
+    }
+
+  if (stream_direction > TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL)
+    {
+      g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT,
+          "Stream direction %u is not valid", stream_direction);
+      goto error;
+    }
+
+  /* In some protocols, streams cannot be neither sending nor receiving, so
+   * if a stream is set to TP_MEDIA_STREAM_DIRECTION_NONE, this is equivalent
+   * to removing it with RemoveStreams. (This is true in XMPP, for instance.)
+   *
+   * If this was the case, there would be code like this here:
+   *
+   * if (stream_direction == TP_MEDIA_STREAM_DIRECTION_NONE)
+   *   {
+   *     example_callable_media_stream_close (stream);
+   *     tp_svc_channel_type_streamed_media_return_from_request_stream_direction (
+   *        context);
+   *     return;
+   *   }
+   *
+   * However, for this example we'll emulate a protocol where streams can be
+   * directionless.
+   */
+
+  if (!example_callable_media_stream_change_direction (stream,
+        stream_direction, &error))
+    goto error;
+
+  tp_svc_channel_type_streamed_media_return_from_request_stream_direction (
+      context);
+  return;
+
+error:
+  dbus_g_method_return_error (context, error);
+  g_error_free (error);
+}
+
+static void
+stream_removed_cb (ExampleCallableMediaStream *stream,
+                   ExampleCallableMediaChannel *self)
+{
+  guint id;
+
+  g_object_get (stream,
+      "id", &id,
+      NULL);
+
+  g_signal_handlers_disconnect_matched (stream, G_SIGNAL_MATCH_DATA,
+      0, 0, NULL, NULL, self);
+  g_hash_table_remove (self->priv->streams, GUINT_TO_POINTER (id));
+  tp_svc_channel_type_streamed_media_emit_stream_removed (self, id);
+
+  if (g_hash_table_size (self->priv->streams) == 0)
+    {
+      /* no streams left, so the call terminates */
+      example_callable_media_channel_close (self, 0,
+          TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+    }
+}
+
+static void
+stream_direction_changed_cb (ExampleCallableMediaStream *stream,
+                             ExampleCallableMediaChannel *self)
+{
+  guint id, direction, pending;
+
+  g_object_get (stream,
+      "id", &id,
+      "direction", &direction,
+      "pending-send", &pending,
+      NULL);
+
+  tp_svc_channel_type_streamed_media_emit_stream_direction_changed (self, id,
+      direction, pending);
+}
+
+static void
+stream_state_changed_cb (ExampleCallableMediaStream *stream,
+                         GParamSpec *spec G_GNUC_UNUSED,
+                         ExampleCallableMediaChannel *self)
+{
+  guint id, state;
+
+  g_object_get (stream,
+      "id", &id,
+      "state", &state,
+      NULL);
+
+  tp_svc_channel_type_streamed_media_emit_stream_state_changed (self, id,
+      state);
+}
+
+static gboolean
+simulate_contact_answered_cb (gpointer p)
+{
+  ExampleCallableMediaChannel *self = p;
+  TpIntSet *peer_set;
+  GHashTableIter iter;
+  gpointer v;
+
+  /* if the call has been cancelled while we were waiting for the
+   * contact to answer, do nothing */
+  if (self->priv->progress == PROGRESS_ENDED)
+    return FALSE;
+
+  /* otherwise, we're waiting for a response from the contact, which now
+   * arrives */
+  g_assert (self->priv->progress == PROGRESS_CALLING);
+
+  g_message ("SIGNALLING: receive: contact answered our call");
+
+  self->priv->progress = PROGRESS_ACTIVE;
+
+  peer_set = tp_intset_new_containing (self->priv->handle);
+  tp_group_mixin_change_members ((GObject *) self, "",
+      peer_set /* added */,
+      NULL /* nobody removed */,
+      NULL /* nobody added to local-pending */,
+      NULL /* nobody added to remote-pending */,
+      self->priv->handle /* actor */,
+      TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+  tp_intset_destroy (peer_set);
+
+  g_hash_table_iter_init (&iter, self->priv->streams);
+
+  while (g_hash_table_iter_next (&iter, NULL, &v))
+    {
+      /* remote contact accepts our proposed stream direction... */
+      example_callable_media_stream_simulate_contact_agreed_to_send (v);
+      /* ... and the stream tries to connect */
+      example_callable_media_stream_connect (v);
+    }
+
+  return FALSE;
+}
+
+static void
+media_request_streams (TpSvcChannelTypeStreamedMedia *iface,
+                       guint contact_handle,
+                       const GArray *media_types,
+                       DBusGMethodInvocation *context)
+{
+  ExampleCallableMediaChannel *self = EXAMPLE_CALLABLE_MEDIA_CHANNEL (iface);
+  TpHandleRepoIface *contact_repo = tp_base_connection_get_handles
+      (self->priv->conn, TP_HANDLE_TYPE_CONTACT);
+  GPtrArray *array;
+  guint i;
+  GError *error = NULL;
+
+  if (!tp_handle_is_valid (contact_repo, contact_handle, &error))
+    goto error;
+
+  if (contact_handle != self->priv->handle)
+    {
+      g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT,
+          "This channel is for handle #%u, we can't make a stream to #%u",
+          self->priv->handle, contact_handle);
+      goto error;
+    }
+
+  if (self->priv->progress == PROGRESS_ENDED)
+    {
+      g_set_error (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE,
+          "Call has terminated");
+      goto error;
+    }
+
+  for (i = 0; i < media_types->len; i++)
+    {
+      guint media_type = g_array_index (media_types, guint, i);
+
+      switch (media_type)
+        {
+        case TP_MEDIA_STREAM_TYPE_AUDIO:
+        case TP_MEDIA_STREAM_TYPE_VIDEO:
+          break;
+        default:
+          g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT,
+              "%u is not a valid Media_Stream_Type", media_type);
+          goto error;
+        }
+    }
+
+  array = g_ptr_array_sized_new (media_types->len);
+
+  for (i = 0; i < media_types->len; i++)
+    {
+      guint media_type = g_array_index (media_types, guint, i);
+      ExampleCallableMediaStream *stream;
+      GValueArray *info;
+      guint id = self->priv->next_stream_id++;
+
+      if (self->priv->progress < PROGRESS_CALLING)
+        {
+          TpIntSet *peer_set = tp_intset_new_containing (self->priv->handle);
+
+          g_message ("SIGNALLING: send: new streamed media call");
+          self->priv->progress = PROGRESS_CALLING;
+
+          tp_group_mixin_change_members ((GObject *) self, "",
+              NULL /* nobody added */,
+              NULL /* nobody removed */,
+              NULL /* nobody added to local-pending */,
+              peer_set /* added to remote-pending */,
+              self->group.self_handle /* actor */,
+              TP_CHANNEL_GROUP_CHANGE_REASON_NONE);
+
+          tp_intset_destroy (peer_set);
+
+          /* In this example there is no real contact, so just simulate them
+           * answering after a short time */
+          /* FIXME: define a special contact who never answers, and if it's
+           * that contact, don't add this timeout */
+          g_timeout_add_full (G_PRIORITY_DEFAULT, self->priv->simulation_delay,
+              simulate_contact_answered_cb, g_object_ref (self),
+              g_object_unref);
+        }
+
+      g_message ("SIGNALLING: send: new %s stream",
+          media_type == TP_MEDIA_STREAM_TYPE_AUDIO ? "audio" : "video");
+
+      stream = g_object_new (EXAMPLE_TYPE_CALLABLE_MEDIA_STREAM,
+          "channel", self,
+          "id", id,
+          "handle", self->priv->handle,
+          "type", media_type,
+          NULL);
+
+      g_hash_table_insert (self->priv->streams, GUINT_TO_POINTER (id), stream);
+
+      tp_svc_channel_type_streamed_media_emit_stream_added (self, id,
+          self->priv->handle, media_type);
+
+      g_signal_connect (stream, "removed", G_CALLBACK (stream_removed_cb),
+          self);
+      g_signal_connect (stream, "notify::state",
+          G_CALLBACK (stream_state_changed_cb), self);
+      g_signal_connect (stream, "direction-changed",
+          G_CALLBACK (stream_direction_changed_cb), self);
+
+      /* newly requested streams start off in a "we want to be bidirectional"
+       * state */
+      example_callable_media_stream_change_direction (stream,
+          TP_MEDIA_STREAM_DIRECTION_BIDIRECTIONAL, NULL);
+
+      if (self->priv->progress == PROGRESS_ACTIVE)
+        {
+          example_callable_media_stream_connect (stream);
+        }
+
+      g_object_get (stream,
+          "stream-info", &info,
+          NULL);
+
+      g_ptr_array_add (array, info);
+    }
+
+  tp_svc_channel_type_streamed_media_return_from_request_streams (context,
+      array);
+  g_ptr_array_free (array, TRUE);
+
+  return;
+
+error:
+  dbus_g_method_return_error (context, error);
+  g_error_free (error);
+}
+
+static void
+media_iface_init (gpointer iface,
+                  gpointer data)
+{
+  TpSvcChannelTypeStreamedMediaClass *klass = iface;
+
+#define IMPLEMENT(x) \
+  tp_svc_channel_type_streamed_media_implement_##x (klass, media_##x)
+  IMPLEMENT (list_streams);
+  IMPLEMENT (remove_streams);
+  IMPLEMENT (request_stream_direction);
+  IMPLEMENT (request_streams);
+#undef IMPLEMENT
+}
diff --git a/tests/lib/callable/media-channel.h b/tests/lib/callable/media-channel.h
new file mode 100644
index 0000000..428370d
--- /dev/null
+++ b/tests/lib/callable/media-channel.h
@@ -0,0 +1,74 @@
+/*
+ * media-channel.h - header for an example channel
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * 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.1 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef __EXAMPLE_CALLABLE_MEDIA_CHANNEL_H__
+#define __EXAMPLE_CALLABLE_MEDIA_CHANNEL_H__
+
+#include <glib-object.h>
+#include <telepathy-glib/group-mixin.h>
+
+G_BEGIN_DECLS
+
+typedef struct _ExampleCallableMediaChannel ExampleCallableMediaChannel;
+typedef struct _ExampleCallableMediaChannelPrivate
+    ExampleCallableMediaChannelPrivate;
+
+typedef struct _ExampleCallableMediaChannelClass
+    ExampleCallableMediaChannelClass;
+typedef struct _ExampleCallableMediaChannelClassPrivate
+    ExampleCallableMediaChannelClassPrivate;
+
+GType example_callable_media_channel_get_type (void);
+
+#define EXAMPLE_TYPE_CALLABLE_MEDIA_CHANNEL \
+  (example_callable_media_channel_get_type ())
+#define EXAMPLE_CALLABLE_MEDIA_CHANNEL(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), EXAMPLE_TYPE_CALLABLE_MEDIA_CHANNEL, \
+                               ExampleCallableMediaChannel))
+#define EXAMPLE_CALLABLE_MEDIA_CHANNEL_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST ((klass), EXAMPLE_TYPE_CALLABLE_MEDIA_CHANNEL, \
+                            ExampleCallableMediaChannelClass))
+#define EXAMPLE_IS_CALLABLE_MEDIA_CHANNEL(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EXAMPLE_TYPE_CALLABLE_MEDIA_CHANNEL))
+#define EXAMPLE_IS_CALLABLE_MEDIA_CHANNEL_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass), EXAMPLE_TYPE_CALLABLE_MEDIA_CHANNEL))
+#define EXAMPLE_CALLABLE_MEDIA_CHANNEL_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), EXAMPLE_TYPE_CALLABLE_MEDIA_CHANNEL, \
+                              ExampleCallableMediaChannelClass))
+
+struct _ExampleCallableMediaChannelClass {
+    GObjectClass parent_class;
+    TpGroupMixinClass group_class;
+    TpDBusPropertiesMixinClass dbus_properties_class;
+
+    ExampleCallableMediaChannelClassPrivate *priv;
+};
+
+struct _ExampleCallableMediaChannel {
+    GObject parent;
+    TpGroupMixin group;
+
+    ExampleCallableMediaChannelPrivate *priv;
+};
+
+G_END_DECLS
+
+#endif
diff --git a/tests/lib/callable/media-manager.c b/tests/lib/callable/media-manager.c
new file mode 100644
index 0000000..7f9372b
--- /dev/null
+++ b/tests/lib/callable/media-manager.c
@@ -0,0 +1,438 @@
+/*
+ * media-manager.c - an example channel manager for StreamedMedia calls.
+ * This channel manager emulates a protocol like XMPP Jingle, where you can
+ * make several simultaneous calls to the same or different contacts.
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * 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.1 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "media-manager.h"
+
+#include <dbus/dbus-glib.h>
+
+#include <telepathy-glib/base-connection.h>
+#include <telepathy-glib/channel-manager.h>
+#include <telepathy-glib/dbus.h>
+#include <telepathy-glib/errors.h>
+#include <telepathy-glib/interfaces.h>
+
+#include "media-channel.h"
+
+static void channel_manager_iface_init (gpointer, gpointer);
+
+G_DEFINE_TYPE_WITH_CODE (ExampleCallableMediaManager,
+    example_callable_media_manager,
+    G_TYPE_OBJECT,
+    G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_MANAGER,
+      channel_manager_iface_init))
+
+/* type definition stuff */
+
+enum
+{
+  PROP_CONNECTION = 1,
+  PROP_SIMULATION_DELAY,
+  N_PROPS
+};
+
+struct _ExampleCallableMediaManagerPrivate
+{
+  TpBaseConnection *conn;
+  guint simulation_delay;
+
+  /* List of ExampleCallableMediaChannel */
+  GList *channels;
+
+  /* Next channel will be ("MediaChannel%u", next_channel_index) */
+  guint next_channel_index;
+
+  gulong status_changed_id;
+};
+
+static void
+example_callable_media_manager_init (ExampleCallableMediaManager *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      EXAMPLE_TYPE_CALLABLE_MEDIA_MANAGER,
+      ExampleCallableMediaManagerPrivate);
+
+  self->priv->conn = NULL;
+  self->priv->channels = NULL;
+  self->priv->status_changed_id = 0;
+}
+
+static void
+example_callable_media_manager_close_all (ExampleCallableMediaManager *self)
+{
+  if (self->priv->channels != NULL)
+    {
+      GList *tmp = self->priv->channels;
+
+      self->priv->channels = NULL;
+
+      g_list_foreach (tmp, (GFunc) g_object_unref, NULL);
+      g_list_free (tmp);
+    }
+
+  if (self->priv->status_changed_id != 0)
+    {
+      g_signal_handler_disconnect (self->priv->conn,
+          self->priv->status_changed_id);
+      self->priv->status_changed_id = 0;
+    }
+}
+
+static void
+dispose (GObject *object)
+{
+  ExampleCallableMediaManager *self = EXAMPLE_CALLABLE_MEDIA_MANAGER (object);
+
+  example_callable_media_manager_close_all (self);
+  g_assert (self->priv->channels == NULL);
+
+  ((GObjectClass *) example_callable_media_manager_parent_class)->dispose (
+    object);
+}
+
+static void
+get_property (GObject *object,
+              guint property_id,
+              GValue *value,
+              GParamSpec *pspec)
+{
+  ExampleCallableMediaManager *self = EXAMPLE_CALLABLE_MEDIA_MANAGER (object);
+
+  switch (property_id)
+    {
+    case PROP_CONNECTION:
+      g_value_set_object (value, self->priv->conn);
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      g_value_set_uint (value, self->priv->simulation_delay);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void
+set_property (GObject *object,
+              guint property_id,
+              const GValue *value,
+              GParamSpec *pspec)
+{
+  ExampleCallableMediaManager *self = EXAMPLE_CALLABLE_MEDIA_MANAGER (object);
+
+  switch (property_id)
+    {
+    case PROP_CONNECTION:
+      /* We don't ref the connection, because it owns a reference to the
+       * channel manager, and it guarantees that the manager's lifetime is
+       * less than its lifetime */
+      self->priv->conn = g_value_get_object (value);
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      self->priv->simulation_delay = g_value_get_uint (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    }
+}
+
+static void
+status_changed_cb (TpBaseConnection *conn,
+                   guint status,
+                   guint reason,
+                   ExampleCallableMediaManager *self)
+{
+  switch (status)
+    {
+    case TP_CONNECTION_STATUS_DISCONNECTED:
+        {
+          example_callable_media_manager_close_all (self);
+        }
+      break;
+
+    default:
+      break;
+    }
+}
+
+static void
+constructed (GObject *object)
+{
+  ExampleCallableMediaManager *self = EXAMPLE_CALLABLE_MEDIA_MANAGER (object);
+  void (*chain_up) (GObject *) =
+      ((GObjectClass *) example_callable_media_manager_parent_class)->constructed;
+
+  if (chain_up != NULL)
+    {
+      chain_up (object);
+    }
+
+  self->priv->status_changed_id = g_signal_connect (self->priv->conn,
+      "status-changed", (GCallback) status_changed_cb, self);
+}
+
+static void
+example_callable_media_manager_class_init (
+    ExampleCallableMediaManagerClass *klass)
+{
+  GParamSpec *param_spec;
+  GObjectClass *object_class = (GObjectClass *) klass;
+
+  object_class->constructed = constructed;
+  object_class->dispose = dispose;
+  object_class->get_property = get_property;
+  object_class->set_property = set_property;
+
+  param_spec = g_param_spec_object ("connection", "Connection object",
+      "The connection that owns this channel manager",
+      TP_TYPE_BASE_CONNECTION,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_CONNECTION, param_spec);
+
+  param_spec = g_param_spec_uint ("simulation-delay", "Simulation delay",
+      "Delay between simulated network events",
+      0, G_MAXUINT32, 1000,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_SIMULATION_DELAY,
+      param_spec);
+
+  g_type_class_add_private (klass,
+      sizeof (ExampleCallableMediaManagerPrivate));
+}
+
+static void
+example_callable_media_manager_foreach_channel (
+    TpChannelManager *iface,
+    TpExportableChannelFunc callback,
+    gpointer user_data)
+{
+  ExampleCallableMediaManager *self = EXAMPLE_CALLABLE_MEDIA_MANAGER (iface);
+
+  g_list_foreach (self->priv->channels, (GFunc) callback, user_data);
+}
+
+static void
+channel_closed_cb (ExampleCallableMediaChannel *chan,
+                   ExampleCallableMediaManager *self)
+{
+  tp_channel_manager_emit_channel_closed_for_object (self,
+      TP_EXPORTABLE_CHANNEL (chan));
+
+  if (self->priv->channels != NULL)
+    {
+      self->priv->channels = g_list_remove_all (self->priv->channels, chan);
+    }
+}
+
+static ExampleCallableMediaChannel *
+new_channel (ExampleCallableMediaManager *self,
+             TpHandle handle,
+             TpHandle initiator,
+             gpointer request_token)
+{
+  ExampleCallableMediaChannel *chan;
+  gchar *object_path;
+  GSList *requests = NULL;
+
+  /* FIXME: This could potentially wrap around, but only after 4 billion
+   * calls, which is probably plenty. */
+  object_path = g_strdup_printf ("%s/MediaChannel%u",
+      self->priv->conn->object_path, self->priv->next_channel_index++);
+
+  chan = g_object_new (EXAMPLE_TYPE_CALLABLE_MEDIA_CHANNEL,
+      "connection", self->priv->conn,
+      "object-path", object_path,
+      "handle", handle,
+      "initiator-handle", initiator,
+      "requested", (self->priv->conn->self_handle == initiator),
+      "simulation-delay", self->priv->simulation_delay,
+      NULL);
+
+  g_free (object_path);
+
+  g_signal_connect (chan, "closed", G_CALLBACK (channel_closed_cb), self);
+
+  self->priv->channels = g_list_prepend (self->priv->channels, chan);
+
+  if (request_token != NULL)
+    requests = g_slist_prepend (requests, request_token);
+
+  tp_channel_manager_emit_new_channel (self, TP_EXPORTABLE_CHANNEL (chan),
+      requests);
+  g_slist_free (requests);
+
+  return chan;
+}
+
+static const gchar * const fixed_properties[] = {
+    TP_IFACE_CHANNEL ".ChannelType",
+    TP_IFACE_CHANNEL ".TargetHandleType",
+    NULL
+};
+
+static const gchar * const allowed_properties[] = {
+    TP_IFACE_CHANNEL ".TargetHandle",
+    TP_IFACE_CHANNEL ".TargetID",
+    NULL
+};
+
+static void
+example_callable_media_manager_foreach_channel_class (
+    TpChannelManager *manager,
+    TpChannelManagerChannelClassFunc func,
+    gpointer user_data)
+{
+  GHashTable *table = g_hash_table_new_full (g_str_hash, g_str_equal,
+      NULL, (GDestroyNotify) tp_g_value_slice_free);
+
+  g_hash_table_insert (table, TP_IFACE_CHANNEL ".ChannelType",
+      tp_g_value_slice_new_static_string (
+        TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA));
+
+  g_hash_table_insert (table, TP_IFACE_CHANNEL ".TargetHandleType",
+      tp_g_value_slice_new_uint (TP_HANDLE_TYPE_CONTACT));
+
+  func (manager, table, allowed_properties, user_data);
+
+  g_hash_table_destroy (table);
+}
+
+static gboolean
+example_callable_media_manager_request (ExampleCallableMediaManager *self,
+                                        gpointer request_token,
+                                        GHashTable *request_properties,
+                                        gboolean require_new)
+{
+  TpHandle handle;
+  GError *error = NULL;
+
+  if (tp_strdiff (tp_asv_get_string (request_properties,
+          TP_IFACE_CHANNEL ".ChannelType"),
+      TP_IFACE_CHANNEL_TYPE_STREAMED_MEDIA))
+    {
+      return FALSE;
+    }
+
+  if (tp_asv_get_uint32 (request_properties,
+      TP_IFACE_CHANNEL ".TargetHandleType", NULL) != TP_HANDLE_TYPE_CONTACT)
+    {
+      return FALSE;
+    }
+
+  handle = tp_asv_get_uint32 (request_properties,
+      TP_IFACE_CHANNEL ".TargetHandle", NULL);
+  g_assert (handle != 0);
+
+  if (tp_channel_manager_asv_has_unknown_properties (request_properties,
+        fixed_properties, allowed_properties, &error))
+    {
+      goto error;
+    }
+
+  if (handle == self->priv->conn->self_handle)
+    {
+      /* In protocols with a concept of multiple "resources" signed in to
+       * one account (XMPP, and possibly MSN) it is technically possible to
+       * call yourself - e.g. if you're signed in on two PCs, you can call one
+       * from the other. For simplicity, this example simulates a protocol
+       * where this is not the case.
+       */
+      g_set_error (&error, TP_ERRORS, TP_ERROR_NOT_IMPLEMENTED,
+          "In this protocol, you can't call yourself");
+      goto error;
+    }
+
+  if (!require_new)
+    {
+      /* see if we're already calling that handle */
+      const GList *link;
+
+      for (link = self->priv->channels; link != NULL; link = link->next)
+        {
+          guint its_handle;
+
+          g_object_get (link->data,
+              "handle", &its_handle,
+              NULL);
+
+          if (its_handle == handle)
+            {
+              tp_channel_manager_emit_request_already_satisfied (self,
+                  request_token, TP_EXPORTABLE_CHANNEL (link->data));
+              return TRUE;
+            }
+        }
+    }
+
+  new_channel (self, handle, self->priv->conn->self_handle,
+      request_token);
+  return TRUE;
+
+error:
+  tp_channel_manager_emit_request_failed (self, request_token,
+      error->domain, error->code, error->message);
+  g_error_free (error);
+  return TRUE;
+}
+
+static gboolean
+example_callable_media_manager_create_channel (TpChannelManager *manager,
+                                               gpointer request_token,
+                                               GHashTable *request_properties)
+{
+    return example_callable_media_manager_request (
+        EXAMPLE_CALLABLE_MEDIA_MANAGER (manager),
+        request_token, request_properties, TRUE);
+}
+
+static gboolean
+example_callable_media_manager_ensure_channel (TpChannelManager *manager,
+                                               gpointer request_token,
+                                               GHashTable *request_properties)
+{
+    return example_callable_media_manager_request (
+        EXAMPLE_CALLABLE_MEDIA_MANAGER (manager),
+        request_token, request_properties, FALSE);
+}
+
+static void
+channel_manager_iface_init (gpointer g_iface,
+                            gpointer iface_data G_GNUC_UNUSED)
+{
+  TpChannelManagerIface *iface = g_iface;
+
+  iface->foreach_channel = example_callable_media_manager_foreach_channel;
+  iface->foreach_channel_class =
+    example_callable_media_manager_foreach_channel_class;
+  iface->create_channel = example_callable_media_manager_create_channel;
+  iface->ensure_channel = example_callable_media_manager_ensure_channel;
+  /* In this channel manager, RequestChannel is not supported (it's new
+   * code so there's no reason to be backwards compatible). The requirements
+   * for RequestChannel are somewhat complicated for backwards compatibility
+   * reasons: see telepathy-gabble or
+   * http://telepathy.freedesktop.org/wiki/Requesting%20StreamedMedia%20channels
+   * for the gory details. */
+  iface->request_channel = NULL;
+}
diff --git a/tests/lib/callable/media-manager.h b/tests/lib/callable/media-manager.h
new file mode 100644
index 0000000..5be239e
--- /dev/null
+++ b/tests/lib/callable/media-manager.h
@@ -0,0 +1,71 @@
+/*
+ * media-manager.h - header for an example channel manager
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * 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.1 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef __EXAMPLE_CALLABLE_MEDIA_MANAGER_H__
+#define __EXAMPLE_CALLABLE_MEDIA_MANAGER_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef struct _ExampleCallableMediaManager ExampleCallableMediaManager;
+typedef struct _ExampleCallableMediaManagerPrivate
+    ExampleCallableMediaManagerPrivate;
+
+typedef struct _ExampleCallableMediaManagerClass
+    ExampleCallableMediaManagerClass;
+typedef struct _ExampleCallableMediaManagerClassPrivate
+    ExampleCallableMediaManagerClassPrivate;
+
+struct _ExampleCallableMediaManagerClass {
+    GObjectClass parent_class;
+
+    ExampleCallableMediaManagerClassPrivate *priv;
+};
+
+struct _ExampleCallableMediaManager {
+    GObject parent;
+
+    ExampleCallableMediaManagerPrivate *priv;
+};
+
+GType example_callable_media_manager_get_type (void);
+
+/* TYPE MACROS */
+#define EXAMPLE_TYPE_CALLABLE_MEDIA_MANAGER \
+  (example_callable_media_manager_get_type ())
+#define EXAMPLE_CALLABLE_MEDIA_MANAGER(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST((obj), EXAMPLE_TYPE_CALLABLE_MEDIA_MANAGER, \
+                              ExampleCallableMediaManager))
+#define EXAMPLE_CALLABLE_MEDIA_MANAGER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST((klass), EXAMPLE_TYPE_CALLABLE_MEDIA_MANAGER, \
+                           ExampleCallableMediaManagerClass))
+#define EXAMPLE_IS_CALLABLE_MEDIA_MANAGER(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE((obj), EXAMPLE_TYPE_CALLABLE_MEDIA_MANAGER))
+#define EXAMPLE_IS_CALLABLE_MEDIA_MANAGER_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE((klass), EXAMPLE_TYPE_CALLABLE_MEDIA_MANAGER))
+#define EXAMPLE_CALLABLE_MEDIA_MANAGER_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), EXAMPLE_TYPE_CALLABLE_MEDIA_MANAGER, \
+                              ExampleCallableMediaManagerClass))
+
+G_END_DECLS
+
+#endif
diff --git a/tests/lib/callable/media-stream.c b/tests/lib/callable/media-stream.c
new file mode 100644
index 0000000..43f9ccd
--- /dev/null
+++ b/tests/lib/callable/media-stream.c
@@ -0,0 +1,506 @@
+/*
+ * media-stream.c - a stream in a streamed media call.
+ *
+ * In connection managers with MediaSignalling, this object would be a D-Bus
+ * object in its own right. In this CM, MediaSignalling is not used, and this
+ * object just represents internal state of the MediaChannel.
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * 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.1 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#include "media-stream.h"
+
+#include <telepathy-glib/base-connection.h>
+#include <telepathy-glib/gtypes.h>
+
+#include "media-channel.h"
+
+G_DEFINE_TYPE (ExampleCallableMediaStream,
+    example_callable_media_stream,
+    G_TYPE_OBJECT)
+
+enum
+{
+  PROP_CHANNEL = 1,
+  PROP_ID,
+  PROP_HANDLE,
+  PROP_TYPE,
+  PROP_STATE,
+  PROP_PENDING_SEND,
+  PROP_DIRECTION,
+  PROP_STREAM_INFO,
+  PROP_SIMULATION_DELAY,
+  N_PROPS
+};
+
+enum
+{
+  SIGNAL_REMOVED,
+  SIGNAL_DIRECTION_CHANGED,
+  N_SIGNALS
+};
+
+static guint signals[N_SIGNALS] = { 0 };
+
+struct _ExampleCallableMediaStreamPrivate
+{
+  TpBaseConnection *conn;
+  ExampleCallableMediaChannel *channel;
+  guint id;
+  TpHandle handle;
+  TpMediaStreamType type;
+  TpMediaStreamState state;
+  TpMediaStreamDirection direction;
+  TpMediaStreamPendingSend pending_send;
+
+  guint simulation_delay;
+
+  gulong call_terminated_id;
+
+  guint connected_event_id;
+
+  gboolean removed;
+};
+
+static void
+example_callable_media_stream_init (ExampleCallableMediaStream *self)
+{
+  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
+      EXAMPLE_TYPE_CALLABLE_MEDIA_STREAM,
+      ExampleCallableMediaStreamPrivate);
+
+  /* FIXME: no particular "implicit" direction is currently mandated by
+   * telepathy-spec */
+  self->priv->direction = TP_MEDIA_STREAM_DIRECTION_NONE;
+  self->priv->pending_send = 0;
+}
+
+static void
+call_terminated_cb (ExampleCallableMediaChannel *channel,
+                    ExampleCallableMediaStream *self)
+{
+  g_signal_handler_disconnect (channel, self->priv->call_terminated_id);
+  self->priv->call_terminated_id = 0;
+  example_callable_media_stream_close (self);
+}
+
+static void
+constructed (GObject *object)
+{
+  ExampleCallableMediaStream *self = EXAMPLE_CALLABLE_MEDIA_STREAM (object);
+  void (*chain_up) (GObject *) =
+      ((GObjectClass *) example_callable_media_stream_parent_class)->constructed;
+
+  if (chain_up != NULL)
+    chain_up (object);
+
+  g_object_get (self->priv->channel,
+      "connection", &self->priv->conn,
+      NULL);
+  self->priv->call_terminated_id = g_signal_connect (self->priv->channel,
+      "call-terminated", G_CALLBACK (call_terminated_cb), self);
+
+  if (self->priv->handle != 0)
+    {
+      TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (
+          self->priv->conn, TP_HANDLE_TYPE_CONTACT);
+
+      tp_handle_ref (contact_repo, self->priv->handle);
+    }
+}
+
+static void
+get_property (GObject *object,
+              guint property_id,
+              GValue *value,
+              GParamSpec *pspec)
+{
+  ExampleCallableMediaStream *self = EXAMPLE_CALLABLE_MEDIA_STREAM (object);
+
+  switch (property_id)
+    {
+    case PROP_ID:
+      g_value_set_uint (value, self->priv->id);
+      break;
+
+    case PROP_HANDLE:
+      g_value_set_uint (value, self->priv->handle);
+      break;
+
+    case PROP_TYPE:
+      g_value_set_uint (value, self->priv->type);
+      break;
+
+    case PROP_STATE:
+      g_value_set_uint (value, self->priv->state);
+      break;
+
+    case PROP_PENDING_SEND:
+      g_value_set_uint (value, self->priv->pending_send);
+      break;
+
+    case PROP_DIRECTION:
+      g_value_set_uint (value, self->priv->direction);
+      break;
+
+    case PROP_CHANNEL:
+      g_value_set_object (value, self->priv->channel);
+      break;
+
+    case PROP_STREAM_INFO:
+        {
+          GValueArray *va = g_value_array_new (6);
+          guint i;
+
+          for (i = 0; i < 6; i++)
+            {
+              g_value_array_append (va, NULL);
+              g_value_init (va->values + i, G_TYPE_UINT);
+            }
+
+          g_value_set_uint (va->values + 0, self->priv->id);
+          g_value_set_uint (va->values + 1, self->priv->handle);
+          g_value_set_uint (va->values + 2, self->priv->type);
+          g_value_set_uint (va->values + 3, self->priv->state);
+          g_value_set_uint (va->values + 4, self->priv->direction);
+          g_value_set_uint (va->values + 5, self->priv->pending_send);
+
+          g_value_take_boxed (value, va);
+        }
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      g_value_set_uint (value, self->priv->simulation_delay);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+set_property (GObject *object,
+              guint property_id,
+              const GValue *value,
+              GParamSpec *pspec)
+{
+  ExampleCallableMediaStream *self = EXAMPLE_CALLABLE_MEDIA_STREAM (object);
+
+  switch (property_id)
+    {
+    case PROP_ID:
+      self->priv->id = g_value_get_uint (value);
+      break;
+
+    case PROP_HANDLE:
+      self->priv->handle = g_value_get_uint (value);
+      break;
+
+    case PROP_TYPE:
+      self->priv->type = g_value_get_uint (value);
+      break;
+
+    case PROP_CHANNEL:
+      g_assert (self->priv->channel == NULL);
+      self->priv->channel = g_value_dup_object (value);
+      break;
+
+    case PROP_SIMULATION_DELAY:
+      self->priv->simulation_delay = g_value_get_uint (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+      break;
+    }
+}
+
+static void
+dispose (GObject *object)
+{
+  ExampleCallableMediaStream *self = EXAMPLE_CALLABLE_MEDIA_STREAM (object);
+  TpHandleRepoIface *contact_repo = tp_base_connection_get_handles (
+      self->priv->conn, TP_HANDLE_TYPE_CONTACT);
+
+  example_callable_media_stream_close (self);
+
+  if (self->priv->handle != 0)
+    {
+      tp_handle_unref (contact_repo, self->priv->handle);
+      self->priv->handle = 0;
+    }
+
+  if (self->priv->channel != NULL)
+    {
+      if (self->priv->call_terminated_id != 0)
+        {
+          g_signal_handler_disconnect (self->priv->channel,
+              self->priv->call_terminated_id);
+          self->priv->call_terminated_id = 0;
+        }
+
+      g_object_unref (self->priv->channel);
+      self->priv->channel = NULL;
+    }
+
+  if (self->priv->conn != NULL)
+    {
+      g_object_unref (self->priv->conn);
+      self->priv->conn = NULL;
+    }
+
+  ((GObjectClass *) example_callable_media_stream_parent_class)->dispose (object);
+}
+
+static void
+example_callable_media_stream_class_init (ExampleCallableMediaStreamClass *klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+  GParamSpec *param_spec;
+
+  g_type_class_add_private (klass,
+      sizeof (ExampleCallableMediaStreamPrivate));
+
+  object_class->constructed = constructed;
+  object_class->set_property = set_property;
+  object_class->get_property = get_property;
+  object_class->dispose = dispose;
+
+  param_spec = g_param_spec_object ("channel", "ExampleCallableMediaChannel",
+      "Media channel that owns this stream",
+      EXAMPLE_TYPE_CALLABLE_MEDIA_CHANNEL,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_CHANNEL, param_spec);
+
+  param_spec = g_param_spec_uint ("id", "Stream ID",
+      "ID of this stream",
+      0, G_MAXUINT32, 0,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_ID, param_spec);
+
+  param_spec = g_param_spec_uint ("handle", "Peer's TpHandle",
+      "The handle with which this stream communicates or 0 if not applicable",
+      0, G_MAXUINT32, 0,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_HANDLE, param_spec);
+
+  param_spec = g_param_spec_uint ("type", "TpMediaStreamType",
+      "Media stream type",
+      0, NUM_TP_MEDIA_STREAM_TYPES - 1, 0,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_TYPE, param_spec);
+
+  param_spec = g_param_spec_uint ("state", "TpMediaStreamState",
+      "Media stream connection state",
+      0, NUM_TP_MEDIA_STREAM_STATES - 1, 0,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_STATE, param_spec);
+
+  param_spec = g_param_spec_uint ("direction", "TpMediaStreamDirection",
+      "Media stream direction",
+      0, NUM_TP_MEDIA_STREAM_DIRECTIONS - 1, 0,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_DIRECTION, param_spec);
+
+  param_spec = g_param_spec_uint ("pending-send", "TpMediaStreamPendingSend",
+      "Requested media stream directions pending approval",
+      0,
+      TP_MEDIA_STREAM_PENDING_LOCAL_SEND | TP_MEDIA_STREAM_PENDING_REMOTE_SEND,
+      0,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_PENDING_SEND, param_spec);
+
+  param_spec = g_param_spec_boxed ("stream-info", "Stream info",
+      "6-entry GValueArray as returned by ListStreams and RequestStreams",
+      TP_STRUCT_TYPE_MEDIA_STREAM_INFO,
+      G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_STREAM_INFO, param_spec);
+
+  param_spec = g_param_spec_uint ("simulation-delay", "Simulation delay",
+      "Delay between simulated network events",
+      0, G_MAXUINT32, 1000,
+      G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+  g_object_class_install_property (object_class, PROP_SIMULATION_DELAY,
+      param_spec);
+
+  signals[SIGNAL_REMOVED] = g_signal_new ("removed",
+      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+      g_cclosure_marshal_VOID__VOID,
+      G_TYPE_NONE, 0);
+
+  signals[SIGNAL_DIRECTION_CHANGED] = g_signal_new ("direction-changed",
+      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
+      g_cclosure_marshal_VOID__VOID,
+      G_TYPE_NONE, 0);
+}
+
+void
+example_callable_media_stream_close (ExampleCallableMediaStream *self)
+{
+  if (!self->priv->removed)
+    {
+      self->priv->removed = TRUE;
+
+      g_message ("Sending to server: Closing stream %u",
+          self->priv->id);
+
+      g_signal_emit (self, signals[SIGNAL_REMOVED], 0);
+
+      if (self->priv->connected_event_id != 0)
+        {
+          g_source_remove (self->priv->connected_event_id);
+        }
+    }
+}
+
+void
+example_callable_media_stream_simulate_contact_agreed_to_send (
+    ExampleCallableMediaStream *self)
+{
+  if (self->priv->removed ||
+      !(self->priv->pending_send & TP_MEDIA_STREAM_PENDING_REMOTE_SEND))
+    return;
+
+  g_message ("SIGNALLING: receive: OK, I'll send you media on stream %u",
+      self->priv->id);
+
+  self->priv->direction |= TP_MEDIA_STREAM_DIRECTION_RECEIVE;
+  self->priv->pending_send &= ~TP_MEDIA_STREAM_PENDING_REMOTE_SEND;
+
+  g_signal_emit (self, signals[SIGNAL_DIRECTION_CHANGED], 0);
+}
+
+static gboolean
+simulate_contact_agreed_to_send_cb (gpointer p)
+{
+  example_callable_media_stream_simulate_contact_agreed_to_send (p);
+  return FALSE;
+}
+
+gboolean
+example_callable_media_stream_change_direction (
+    ExampleCallableMediaStream *self,
+    TpMediaStreamDirection direction,
+    GError **error)
+{
+  gboolean sending =
+    ((self->priv->direction & TP_MEDIA_STREAM_DIRECTION_SEND) != 0);
+  gboolean receiving =
+    ((self->priv->direction & TP_MEDIA_STREAM_DIRECTION_RECEIVE) != 0);
+  gboolean want_to_send =
+    ((direction & TP_MEDIA_STREAM_DIRECTION_RECEIVE) != 0);
+  gboolean want_to_receive =
+    ((direction & TP_MEDIA_STREAM_DIRECTION_RECEIVE) != 0);
+  gboolean pending_remote_send =
+    ((self->priv->pending_send & TP_MEDIA_STREAM_PENDING_REMOTE_SEND) != 0);
+  gboolean pending_local_send =
+    ((self->priv->pending_send & TP_MEDIA_STREAM_PENDING_LOCAL_SEND) != 0);
+  gboolean changed = FALSE;
+
+  if (want_to_send)
+    {
+      if (!sending)
+        {
+          if (pending_local_send)
+            {
+              g_message ("SIGNALLING: send: I will now send you media on "
+                  "stream %u", self->priv->id);
+            }
+
+          g_message ("MEDIA: Sending media to peer for stream %u",
+              self->priv->id);
+          changed = TRUE;
+          self->priv->direction |= TP_MEDIA_STREAM_DIRECTION_SEND;
+        }
+    }
+  else
+    {
+      if (sending)
+        {
+          g_message ("SIGNALLING: send: I will no longer send you media on "
+              "stream %u", self->priv->id);
+          g_message ("MEDIA: No longer sending media to peer for stream %u",
+              self->priv->id);
+          changed = TRUE;
+          self->priv->direction &= ~TP_MEDIA_STREAM_DIRECTION_SEND;
+        }
+      else if (pending_local_send)
+        {
+          g_message ("SIGNALLING: send: No, I refuse to send you media on "
+              "stream %u", self->priv->id);
+          changed = TRUE;
+          self->priv->pending_send &= ~TP_MEDIA_STREAM_PENDING_LOCAL_SEND;
+        }
+    }
+
+  if (want_to_receive)
+    {
+      if (!receiving && !pending_remote_send)
+        {
+          g_message ("SIGNALLING: send: Please start sending me stream %u",
+              self->priv->id);
+          changed = TRUE;
+          self->priv->pending_send |= TP_MEDIA_STREAM_PENDING_REMOTE_SEND;
+          g_timeout_add_full (G_PRIORITY_DEFAULT, self->priv->simulation_delay,
+              simulate_contact_agreed_to_send_cb, g_object_ref (self),
+              g_object_unref);
+        }
+    }
+  else
+    {
+      if (receiving)
+        {
+          g_message ("SIGNALLING: send: Please stop sending me stream %u",
+              self->priv->id);
+          g_message ("MEDIA: Suppressing output of stream %u",
+              self->priv->id);
+          changed = TRUE;
+          self->priv->direction &= ~TP_MEDIA_STREAM_DIRECTION_RECEIVE;
+        }
+    }
+
+  if (changed)
+    g_signal_emit (self, signals[SIGNAL_DIRECTION_CHANGED], 0);
+
+  return TRUE;
+}
+
+static gboolean
+simulate_stream_connected_cb (gpointer p)
+{
+  ExampleCallableMediaStream *self = EXAMPLE_CALLABLE_MEDIA_STREAM (p);
+
+  g_message ("MEDIA: stream connected");
+  self->priv->state = TP_MEDIA_STREAM_STATE_CONNECTED;
+  g_object_notify ((GObject *) self, "state");
+
+  return FALSE;
+}
+
+void
+example_callable_media_stream_connect (ExampleCallableMediaStream *self)
+{
+  /* if already trying to connect, do nothing */
+  if (self->priv->connected_event_id != 0)
+    return;
+
+  /* simulate it taking a short time to connect */
+  self->priv->connected_event_id = g_timeout_add (self->priv->simulation_delay,
+      simulate_stream_connected_cb, self);
+}
diff --git a/tests/lib/callable/media-stream.h b/tests/lib/callable/media-stream.h
new file mode 100644
index 0000000..5f5d916
--- /dev/null
+++ b/tests/lib/callable/media-stream.h
@@ -0,0 +1,83 @@
+/*
+ * media-stream.h - header for an example stream
+ *
+ * Copyright © 2007-2009 Collabora Ltd. <http://www.collabora.co.uk/>
+ * Copyright © 2007-2009 Nokia Corporation
+ *
+ * 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.1 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+
+#ifndef __EXAMPLE_CALLABLE_MEDIA_STREAM_H__
+#define __EXAMPLE_CALLABLE_MEDIA_STREAM_H__
+
+#include <glib-object.h>
+
+#include <telepathy-glib/enums.h>
+
+G_BEGIN_DECLS
+
+typedef struct _ExampleCallableMediaStream ExampleCallableMediaStream;
+typedef struct _ExampleCallableMediaStreamPrivate
+    ExampleCallableMediaStreamPrivate;
+
+typedef struct _ExampleCallableMediaStreamClass
+    ExampleCallableMediaStreamClass;
+typedef struct _ExampleCallableMediaStreamClassPrivate
+    ExampleCallableMediaStreamClassPrivate;
+
+GType example_callable_media_stream_get_type (void);
+
+#define EXAMPLE_TYPE_CALLABLE_MEDIA_STREAM \
+  (example_callable_media_stream_get_type ())
+#define EXAMPLE_CALLABLE_MEDIA_STREAM(obj) \
+  (G_TYPE_CHECK_INSTANCE_CAST ((obj), EXAMPLE_TYPE_CALLABLE_MEDIA_STREAM, \
+                               ExampleCallableMediaStream))
+#define EXAMPLE_CALLABLE_MEDIA_STREAM_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_CAST ((klass), EXAMPLE_TYPE_CALLABLE_MEDIA_STREAM, \
+                            ExampleCallableMediaStreamClass))
+#define EXAMPLE_IS_CALLABLE_MEDIA_STREAM(obj) \
+  (G_TYPE_CHECK_INSTANCE_TYPE ((obj), EXAMPLE_TYPE_CALLABLE_MEDIA_STREAM))
+#define EXAMPLE_IS_CALLABLE_MEDIA_STREAM_CLASS(klass) \
+  (G_TYPE_CHECK_CLASS_TYPE ((klass), EXAMPLE_TYPE_CALLABLE_MEDIA_STREAM))
+#define EXAMPLE_CALLABLE_MEDIA_STREAM_GET_CLASS(obj) \
+  (G_TYPE_INSTANCE_GET_CLASS ((obj), EXAMPLE_TYPE_CALLABLE_MEDIA_STREAM, \
+                              ExampleCallableMediaStreamClass))
+
+struct _ExampleCallableMediaStreamClass {
+    GObjectClass parent_class;
+
+    ExampleCallableMediaStreamClassPrivate *priv;
+};
+
+struct _ExampleCallableMediaStream {
+    GObject parent;
+
+    ExampleCallableMediaStreamPrivate *priv;
+};
+
+void example_callable_media_stream_close (ExampleCallableMediaStream *self);
+gboolean example_callable_media_stream_change_direction (
+    ExampleCallableMediaStream *self, TpMediaStreamDirection direction,
+    GError **error);
+void example_callable_media_stream_connect (ExampleCallableMediaStream *self);
+
+/* This controls receiving emulated network events, so it wouldn't exist in
+ * a real connection manager */
+void example_callable_media_stream_simulate_contact_agreed_to_send (
+    ExampleCallableMediaStream *self);
+
+G_END_DECLS
+
+#endif
-- 
1.5.6.5




More information about the telepathy-commits mailing list