summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--configure.ac5
-rw-r--r--extensions/Channel_Type_FileTransfer_Future.xml67
-rw-r--r--extensions/Makefile.am1
-rw-r--r--extensions/all.xml1
-rw-r--r--src/Makefile.am8
-rw-r--r--src/capabilities.c21
-rw-r--r--src/capabilities.h2
-rw-r--r--src/connection.c10
-rw-r--r--src/debug.c1
-rw-r--r--src/debug.h3
-rw-r--r--src/ft-channel.c602
-rw-r--r--src/ft-channel.h30
-rw-r--r--src/ft-manager.c183
-rw-r--r--src/gtalk-file-collection.c1786
-rw-r--r--src/gtalk-file-collection.h100
-rw-r--r--src/jingle-content.c187
-rw-r--r--src/jingle-content.h11
-rw-r--r--src/jingle-factory.c5
-rw-r--r--src/jingle-factory.h3
-rw-r--r--src/jingle-media-rtp.c32
-rw-r--r--src/jingle-session.c80
-rw-r--r--src/jingle-session.h2
-rw-r--r--src/jingle-share.c528
-rw-r--r--src/jingle-share.h87
-rw-r--r--src/jingle-transport-google.c124
-rw-r--r--src/jingle-transport-google.h4
-rw-r--r--src/namespaces.h3
-rw-r--r--src/types.h1
-rw-r--r--tests/twisted/Makefile.am11
-rw-r--r--tests/twisted/constants.py1
-rw-r--r--tests/twisted/jingle-share/file_transfer_helper.py578
-rw-r--r--tests/twisted/jingle-share/jingleshareutils.py103
-rw-r--r--tests/twisted/jingle-share/test-caps-file-transfer.py156
-rw-r--r--tests/twisted/jingle-share/test-multift.py159
-rw-r--r--tests/twisted/jingle-share/test-receive-file-and-close-socket-while-receiving.py19
-rw-r--r--tests/twisted/jingle-share/test-receive-file-and-disconnect.py16
-rw-r--r--tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-pending.py52
-rw-r--r--tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-transfering.py45
-rw-r--r--tests/twisted/jingle-share/test-receive-file-decline.py79
-rw-r--r--tests/twisted/jingle-share/test-send-file-and-cancel-immediately.py74
-rw-r--r--tests/twisted/jingle-share/test-send-file-send-before-accept.py28
-rw-r--r--tests/twisted/jingle-share/test-send-file-wait-to-provide.py37
-rw-r--r--tests/twisted/jingle-share/test-send-file.py9
-rw-r--r--tests/twisted/ns.py2
44 files changed, 5008 insertions, 248 deletions
diff --git a/configure.ac b/configure.ac
index 98d0901b2..538b731c1 100644
--- a/configure.ac
+++ b/configure.ac
@@ -256,6 +256,11 @@ PKG_CHECK_MODULES(SOUP, libsoup-2.4)
AC_SUBST(SOUP_CFLAGS)
AC_SUBST(SOUP_LIBS)
+dnl Check for libnice
+PKG_CHECK_MODULES(NICE, nice >= 0.0.11)
+AC_SUBST(NICE_CFLAGS)
+AC_SUBST(NICE_LIBS)
+
PKG_CHECK_MODULES([UUID], [uuid])
AC_SUBST([UUID_CFLAGS])
AC_SUBST([UUID_LIBS])
diff --git a/extensions/Channel_Type_FileTransfer_Future.xml b/extensions/Channel_Type_FileTransfer_Future.xml
new file mode 100644
index 000000000..b155136e0
--- /dev/null
+++ b/extensions/Channel_Type_FileTransfer_Future.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" ?>
+<node name="/Channel_Type_FileTransfer_Future"
+ xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
+ <tp:copyright>Copyright (C) 2010 Collabora Ltd.</tp:copyright>
+ <tp:license xmlns="http://www.w3.org/1999/xhtml">
+ <p>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.</p>
+
+<p>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.</p>
+
+<p>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 Street, Fifth Floor, Boston, MA 02110-1301, USA.</p>
+ </tp:license>
+ <interface name="org.freedesktop.Telepathy.Channel.Type.FileTransfer.FUTURE"
+ tp:causes-havoc="a staging area for future File Transfer Channel functionality">
+
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>This interface contains functionality which we intend to incorporate
+ into the File Transfer Channel interface in future.
+ It should be considered to be conceptually part of the core
+ File Transfer Channel interface, but without API or ABI guarantees.</p>
+
+ <tp:rationale>
+ <p>If we add new functionality to the Channel interface, libraries
+ that use generated code (notably telepathy-glib) will have it as
+ part of their ABI forever, meaning we can't make incompatible
+ changes. By using this interface as a staging area for future
+ Channel functionality, we can try out new properties, signals
+ and methods as application-specific extensions, then merge them
+ into the core Channel interface when we have enough implementation
+ experience to declare them to be stable.</p>
+
+ <p>The name is by analogy to Python's <code>__future__</code>
+ pseudo-module.</p>
+ </tp:rationale>
+ </tp:docstring>
+
+ <property name="FileCollection" tp:name-for-bindings="FileCollection"
+ type="s" access="read">
+ <tp:added version="0.19.2">(in Channel.Type.FileTransfer.FUTURE
+ pseudo-interface)</tp:added>
+ <tp:docstring xmlns="http://www.w3.org/1999/xhtml">
+ <p>The FileCollection to which this channel belongs.</p>
+
+ <p>A channel's FileCollection property can never change.</p>
+
+ <p>At least on GTalk and apparently also on iChat the user can
+ send a set of files to a contact and that contact can then
+ pick and choose which files to actually receive.
+
+ The CM should emit all new FT channels belonging to one collection
+ at the same time, UIs supporting this feature can then
+ bundle all these channels together in some way and show a
+ nice UI. UIs not supporting it will treat them as seperate
+ transfers, which is not great but a reasonable fallback</p>
+ </tp:docstring>
+ </property>
+
+ </interface>
+</node>
+<!-- vim:set sw=2 sts=2 et ft=xml: -->
diff --git a/extensions/Makefile.am b/extensions/Makefile.am
index 76bbc84d3..ba369af96 100644
--- a/extensions/Makefile.am
+++ b/extensions/Makefile.am
@@ -12,6 +12,7 @@ EXTRA_DIST = \
Channel_Future.xml \
Channel_Interface_Conference.xml \
Channel_Type_Call.xml \
+ Channel_Type_FileTransfer_Future.xml \
Connection_Future.xml \
Connection_Interface_Gabble_Decloak.xml \
Connection_Interface_Mail_Notification.xml \
diff --git a/extensions/all.xml b/extensions/all.xml
index 9d33bfe7a..22827090f 100644
--- a/extensions/all.xml
+++ b/extensions/all.xml
@@ -43,6 +43,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA</p>
<xi:include href="OLPC_Channel_Type_ActivityView.xml"/>
<xi:include href="Channel_Type_Contact_Search.xml"/>
+<xi:include href="Channel_Type_FileTransfer_Future.xml"/>
<xi:include href="Connection_Interface_Gabble_Decloak.xml"/>
<xi:include href="Channel_Interface_Conference.xml"/>
<xi:include href="Connection_Interface_Mail_Notification.xml"/>
diff --git a/src/Makefile.am b/src/Makefile.am
index e690d4532..755ee01e7 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -93,6 +93,8 @@ libgabble_convenience_la_SOURCES = \
ft-manager.h \
gabble.c \
gabble.h \
+ gtalk-file-collection.h \
+ gtalk-file-collection.c \
im-channel.h \
im-channel.c \
im-factory.h \
@@ -101,6 +103,8 @@ libgabble_convenience_la_SOURCES = \
jingle-content.c \
jingle-factory.h \
jingle-factory.c \
+ jingle-share.h \
+ jingle-share.c \
jingle-media-rtp.h \
jingle-media-rtp.c \
jingle-session.h \
@@ -216,14 +220,14 @@ noinst_LTLIBRARIES = libgabble-convenience.la
AM_CFLAGS = $(ERROR_CFLAGS) -I$(top_srcdir) -I$(top_builddir) \
@DBUS_CFLAGS@ @GLIB_CFLAGS@ @WOCKY_CFLAGS@ \
@HANDLE_LEAK_DEBUG_CFLAGS@ @TP_GLIB_CFLAGS@ \
- @SOUP_CFLAGS@ @UUID_CFLAGS@ @GMODULE_CFLAGS@ \
+ @SOUP_CFLAGS@ @NICE_CFLAGS@ @UUID_CFLAGS@ @GMODULE_CFLAGS@ \
@SQLITE_CFLAGS@ \
-I $(top_srcdir)/lib -I $(top_builddir)/lib \
-DG_LOG_DOMAIN=\"gabble\" \
-DPLUGIN_DIR=\"$(libdir)/telepathy/gabble-0\"
ALL_LIBS = @DBUS_LIBS@ @GLIB_LIBS@ @WOCKY_LIBS@ @TP_GLIB_LIBS@ \
- @SOUP_LIBS@ @UUID_LIBS@ @GMODULE_LIBS@ @SQLITE_LIBS@
+ @SOUP_LIBS@ @NICE_LIBS@ @UUID_LIBS@ @GMODULE_LIBS@ @SQLITE_LIBS@
# build gibber first
all: gibber
diff --git a/src/capabilities.c b/src/capabilities.c
index 2c6f3bcdb..671953b28 100644
--- a/src/capabilities.c
+++ b/src/capabilities.c
@@ -68,6 +68,7 @@ static const Feature self_advertised_features[] =
{ FEATURE_OPTIONAL, NS_GOOGLE_TRANSPORT_P2P },
{ FEATURE_OPTIONAL, NS_JINGLE_TRANSPORT_ICEUDP },
+ { FEATURE_OPTIONAL, NS_GOOGLE_FEAT_SHARE },
{ FEATURE_OPTIONAL, NS_GOOGLE_FEAT_VOICE },
{ FEATURE_OPTIONAL, NS_GOOGLE_FEAT_VIDEO },
{ FEATURE_OPTIONAL, NS_JINGLE_DESCRIPTION_AUDIO },
@@ -92,6 +93,7 @@ static const Feature quirks[] = {
};
static GabbleCapabilitySet *legacy_caps = NULL;
+static GabbleCapabilitySet *share_v1_caps = NULL;
static GabbleCapabilitySet *voice_v1_caps = NULL;
static GabbleCapabilitySet *video_v1_caps = NULL;
static GabbleCapabilitySet *any_audio_caps = NULL;
@@ -111,6 +113,12 @@ gabble_capabilities_get_legacy (void)
}
const GabbleCapabilitySet *
+gabble_capabilities_get_bundle_share_v1 (void)
+{
+ return share_v1_caps;
+}
+
+const GabbleCapabilitySet *
gabble_capabilities_get_bundle_voice_v1 (void)
{
return voice_v1_caps;
@@ -255,6 +263,9 @@ gabble_capabilities_init (GabbleConnection *conn)
gabble_capability_set_add (legacy_caps, feat->ns);
}
+ share_v1_caps = gabble_capability_set_new ();
+ gabble_capability_set_add (share_v1_caps, NS_GOOGLE_FEAT_SHARE);
+
voice_v1_caps = gabble_capability_set_new ();
gabble_capability_set_add (voice_v1_caps, NS_GOOGLE_FEAT_VOICE);
@@ -320,6 +331,7 @@ gabble_capabilities_finalize (GabbleConnection *conn)
if (--feature_handles_refcount == 0)
{
gabble_capability_set_free (legacy_caps);
+ gabble_capability_set_free (share_v1_caps);
gabble_capability_set_free (voice_v1_caps);
gabble_capability_set_free (video_v1_caps);
gabble_capability_set_free (any_audio_caps);
@@ -333,6 +345,7 @@ gabble_capabilities_finalize (GabbleConnection *conn)
gabble_capability_set_free (olpc_caps);
legacy_caps = NULL;
+ share_v1_caps = NULL;
voice_v1_caps = NULL;
video_v1_caps = NULL;
any_audio_caps = NULL;
@@ -373,8 +386,10 @@ capabilities_fill_cache (GabblePresenceCache *cache)
GOOGLE_BUNDLE ("voice-v1", NS_GOOGLE_FEAT_VOICE);
GOOGLE_BUNDLE ("video-v1", NS_GOOGLE_FEAT_VIDEO);
- /* Not really sure what these ones are. */
- GOOGLE_BUNDLE ("share-v1", NULL);
+ /* File transfer support */
+ GOOGLE_BUNDLE ("share-v1", NS_GOOGLE_FEAT_SHARE);
+
+ /* Not really sure what this ones is. */
GOOGLE_BUNDLE ("sms-v1", NULL);
/* TODO: remove this when we fix fd.o#22768. */
@@ -396,6 +411,8 @@ capabilities_fill_cache (GabblePresenceCache *cache)
NS_GABBLE_CAPS "#" BUNDLE_VOICE_V1, NS_GOOGLE_FEAT_VOICE);
gabble_presence_cache_add_bundle_caps (cache,
NS_GABBLE_CAPS "#" BUNDLE_VIDEO_V1, NS_GOOGLE_FEAT_VIDEO);
+ gabble_presence_cache_add_bundle_caps (cache,
+ NS_GABBLE_CAPS "#" BUNDLE_SHARE_V1, NS_GOOGLE_FEAT_SHARE);
}
const CapabilityConversionData capabilities_conversions[] =
diff --git a/src/capabilities.h b/src/capabilities.h
index ed33ed411..9f78bf3f1 100644
--- a/src/capabilities.h
+++ b/src/capabilities.h
@@ -59,10 +59,12 @@ const GabbleCapabilitySet *gabble_capabilities_get_olpc_notify (void);
* clients require the bundle names "voice-v1" and "video-v1". We keep these
* names for compatibility.
*/
+#define BUNDLE_SHARE_V1 "share-v1"
#define BUNDLE_VOICE_V1 "voice-v1"
#define BUNDLE_VIDEO_V1 "video-v1"
#define BUNDLE_PMUC_V1 "pmuc-v1"
+const GabbleCapabilitySet *gabble_capabilities_get_bundle_share_v1 (void);
const GabbleCapabilitySet *gabble_capabilities_get_bundle_voice_v1 (void);
const GabbleCapabilitySet *gabble_capabilities_get_bundle_video_v1 (void);
diff --git a/src/connection.c b/src/connection.c
index 8a27f7eac..09164fc4c 100644
--- a/src/connection.c
+++ b/src/connection.c
@@ -2183,7 +2183,7 @@ gabble_connection_fill_in_caps (GabbleConnection *self,
GabblePresence *presence = self->self_presence;
LmMessageNode *node = lm_message_get_node (presence_message);
gchar *caps_hash;
- gboolean voice_v1, video_v1;
+ gboolean share_v1, voice_v1, video_v1;
GString *ext = g_string_new ("");
/* XEP-0115 version 1.5 uses a verification string in the 'ver' attribute */
@@ -2206,9 +2206,13 @@ gabble_connection_fill_in_caps (GabbleConnection *self,
g_string_append (ext, BUNDLE_PMUC_V1);
+ share_v1 = gabble_presence_has_cap (presence, NS_GOOGLE_FEAT_SHARE);
voice_v1 = gabble_presence_has_cap (presence, NS_GOOGLE_FEAT_VOICE);
video_v1 = gabble_presence_has_cap (presence, NS_GOOGLE_FEAT_VIDEO);
+ if (share_v1)
+ g_string_append (ext, " " BUNDLE_SHARE_V1);
+
if (voice_v1)
g_string_append (ext, " " BUNDLE_VOICE_V1);
@@ -2542,6 +2546,10 @@ connection_iq_disco_cb (LmMessageHandler *handler,
* because capabilities_get_features() always includes a few bonus
* features...
*/
+
+ if (!tp_strdiff (suffix, BUNDLE_SHARE_V1))
+ features = gabble_capabilities_get_bundle_share_v1 ();
+
if (!tp_strdiff (suffix, BUNDLE_VOICE_V1))
features = gabble_capabilities_get_bundle_voice_v1 ();
diff --git a/src/debug.c b/src/debug.c
index 701e9c9f0..014c80193 100644
--- a/src/debug.c
+++ b/src/debug.c
@@ -45,6 +45,7 @@ static GDebugKey keys[] = {
{ "plugins", GABBLE_DEBUG_PLUGINS },
{ "mail", GABBLE_DEBUG_MAIL_NOTIF },
{ "authentication", GABBLE_DEBUG_AUTH },
+ { "share", GABBLE_DEBUG_SHARE },
{ 0, },
};
diff --git a/src/debug.h b/src/debug.h
index d79372681..85d5971a1 100644
--- a/src/debug.h
+++ b/src/debug.h
@@ -32,7 +32,8 @@ typedef enum
GABBLE_DEBUG_PLUGINS = 1 << 21,
GABBLE_DEBUG_MAIL_NOTIF = 1 << 22,
GABBLE_DEBUG_AUTH = 1 << 23,
- GABBLE_DEBUG_SLACKER = 1 << 24
+ GABBLE_DEBUG_SLACKER = 1 << 24,
+ GABBLE_DEBUG_SHARE = 1 << 25
} GabbleDebugFlags;
void gabble_debug_set_flags_from_env (void);
diff --git a/src/ft-channel.c b/src/ft-channel.c
index a50608c08..12bbb1442 100644
--- a/src/ft-channel.c
+++ b/src/ft-channel.c
@@ -1,6 +1,6 @@
/*
* ft-channel.c - Source for GabbleFileTransferChannel
- * Copyright (C) 2009 Collabora Ltd.
+ * Copyright (C) 2009-2010 Collabora Ltd.
* @author: Guillaume Desmottes <guillaume.desmottes@collabora.co.uk>
*
* This library is free software; you can redistribute it and/or
@@ -18,7 +18,7 @@
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
-#include <config.h>
+#include "config.h"
#include <glib/gstdio.h>
#include <dbus/dbus-glib.h>
@@ -43,7 +43,6 @@
#include <gibber/gibber-transport.h>
#include <gibber/gibber-unix-transport.h> /* just for the feature-test */
-#include "bytestream-factory.h"
#include "connection.h"
#include "ft-channel.h"
#include "gabble-signals-marshal.h"
@@ -59,8 +58,16 @@
#include <telepathy-glib/svc-generic.h>
#include <telepathy-glib/svc-channel.h>
+
static void channel_iface_init (gpointer g_iface, gpointer iface_data);
static void file_transfer_iface_init (gpointer g_iface, gpointer iface_data);
+static void transferred_chunk (GabbleFileTransferChannel *self, guint64 count);
+static gboolean set_bytestream (GabbleFileTransferChannel *self,
+ GabbleBytestreamIface *bytestream);
+static gboolean set_gtalk_file_collection (GabbleFileTransferChannel *self,
+ GTalkFileCollection *gtalk_file_collection);
+
+
G_DEFINE_TYPE_WITH_CODE (GabbleFileTransferChannel, gabble_file_transfer_channel,
G_TYPE_OBJECT,
@@ -71,6 +78,8 @@ G_DEFINE_TYPE_WITH_CODE (GabbleFileTransferChannel, gabble_file_transfer_channel
G_IMPLEMENT_INTERFACE (TP_TYPE_CHANNEL_IFACE, NULL);
G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_CHANNEL_TYPE_FILE_TRANSFER,
file_transfer_iface_init);
+ G_IMPLEMENT_INTERFACE (GABBLE_TYPE_SVC_CHANNEL_TYPE_FILETRANSFER_FUTURE,
+ NULL);
);
#define GABBLE_UNDEFINED_FILE_SIZE G_MAXUINT64
@@ -108,9 +117,11 @@ enum
PROP_TRANSFERRED_BYTES,
PROP_INITIAL_OFFSET,
PROP_RESUME_SUPPORTED,
+ PROP_FILE_COLLECTION,
PROP_CONNECTION,
PROP_BYTESTREAM,
+ PROP_GTALK_FILE_COLLECTION,
LAST_PROPERTY
};
@@ -126,9 +137,10 @@ struct _GabbleFileTransferChannelPrivate {
TpSocketAddressType socket_type;
GValue *socket_address;
TpHandle initiator;
- gboolean remote_accepted;
gboolean resume_supported;
+ GTalkFileCollection *gtalk_file_collection;
+
GabbleBytestreamIface *bytestream;
GibberListener *listener;
GibberTransport *transport;
@@ -145,20 +157,20 @@ struct _GabbleFileTransferChannelPrivate {
guint64 transferred_bytes;
guint64 initial_offset;
guint64 date;
+ gchar *file_collection;
+ gboolean channel_opened;
};
-static void set_bytestream (GabbleFileTransferChannel *self,
- GabbleBytestreamIface *bytestream);
-static void
+void
gabble_file_transfer_channel_do_close (GabbleFileTransferChannel *self)
{
if (self->priv->closed)
return;
DEBUG ("Emitting closed signal for %s", self->priv->object_path);
- tp_svc_channel_emit_closed (self);
self->priv->closed = TRUE;
+ tp_svc_channel_emit_closed (self);
}
static void
@@ -265,6 +277,9 @@ gabble_file_transfer_channel_get_property (GObject *object,
case PROP_DATE:
g_value_set_uint64 (value, self->priv->date);
break;
+ case PROP_FILE_COLLECTION:
+ g_value_set_string (value, self->priv->file_collection);
+ break;
case PROP_CHANNEL_DESTROYED:
g_value_set_boolean (value, self->priv->closed);
break;
@@ -293,11 +308,15 @@ gabble_file_transfer_channel_get_property (GObject *object,
TP_IFACE_CHANNEL_TYPE_FILE_TRANSFER, "AvailableSocketTypes",
TP_IFACE_CHANNEL_TYPE_FILE_TRANSFER, "TransferredBytes",
TP_IFACE_CHANNEL_TYPE_FILE_TRANSFER, "InitialOffset",
+ GABBLE_IFACE_CHANNEL_TYPE_FILETRANSFER_FUTURE, "FileCollection",
NULL));
break;
case PROP_BYTESTREAM:
g_value_set_object (value, self->priv->bytestream);
break;
+ case PROP_GTALK_FILE_COLLECTION:
+ g_value_set_object (value, self->priv->gtalk_file_collection);
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
@@ -370,12 +389,20 @@ gabble_file_transfer_channel_set_property (GObject *object,
case PROP_INITIAL_OFFSET:
self->priv->initial_offset = g_value_get_uint64 (value);
break;
+ case PROP_FILE_COLLECTION:
+ g_free (self->priv->file_collection);
+ self->priv->file_collection = g_value_dup_string (value);
+ break;
+ case PROP_RESUME_SUPPORTED:
+ self->priv->resume_supported = g_value_get_boolean (value);
+ break;
case PROP_BYTESTREAM:
set_bytestream (self,
GABBLE_BYTESTREAM_IFACE (g_value_get_object (value)));
break;
- case PROP_RESUME_SUPPORTED:
- self->priv->resume_supported = g_value_get_boolean (value);
+ case PROP_GTALK_FILE_COLLECTION:
+ set_gtalk_file_collection (self,
+ GTALK_FILE_COLLECTION (g_value_get_object (value)));
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
@@ -500,16 +527,10 @@ gabble_file_transfer_channel_constructor (GType type,
tp_handle_inspect (contact_repo, self->priv->initiator),
self->priv->filename, self->priv->size);
- if (self->priv->initiator == base_conn->self_handle)
- {
- /* Outgoing FT , we'll need SOCK5 proxies when we'll offer the file */
- gabble_bytestream_factory_query_socks5_proxies (
- self->priv->connection->bytestream_factory);
- }
-
return obj;
}
+static void close_session_and_transport (GabbleFileTransferChannel *self);
static void gabble_file_transfer_channel_dispose (GObject *object);
static void gabble_file_transfer_channel_finalize (GObject *object);
@@ -548,6 +569,11 @@ gabble_file_transfer_channel_class_init (
{ NULL }
};
+ static TpDBusPropertiesMixinPropImpl file_future_props[] = {
+ { "FileCollection", "file-collection", NULL },
+ { NULL }
+ };
+
static TpDBusPropertiesMixinIfaceImpl prop_interfaces[] = {
{ TP_IFACE_CHANNEL,
tp_dbus_properties_mixin_getter_gobject_properties,
@@ -559,6 +585,11 @@ gabble_file_transfer_channel_class_init (
NULL,
file_props
},
+ { GABBLE_IFACE_CHANNEL_TYPE_FILETRANSFER_FUTURE,
+ tp_dbus_properties_mixin_getter_gobject_properties,
+ NULL,
+ file_future_props
+ },
{ NULL }
};
@@ -750,6 +781,15 @@ gabble_file_transfer_channel_class_init (
g_object_class_install_property (object_class, PROP_BYTESTREAM,
param_spec);
+ param_spec = g_param_spec_object (
+ "gtalk-file-collection",
+ "GTalkFileCollection object for gtalk-compatible file transfer",
+ "GTalk compatible file transfer collection",
+ G_TYPE_OBJECT,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ g_object_class_install_property (object_class, PROP_GTALK_FILE_COLLECTION,
+ param_spec);
+
param_spec = g_param_spec_boolean (
"resume-supported",
"resume is supported",
@@ -759,6 +799,16 @@ gabble_file_transfer_channel_class_init (
g_object_class_install_property (object_class, PROP_RESUME_SUPPORTED,
param_spec);
+ param_spec = g_param_spec_string (
+ "file-collection",
+ "gchar *file_colletion",
+ "Token identifying a collection of files",
+ "",
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ g_object_class_install_property (object_class, PROP_FILE_COLLECTION,
+ param_spec);
+
gabble_file_transfer_channel_class->dbus_props_class.interfaces =
prop_interfaces;
tp_dbus_properties_mixin_class_init (object_class,
@@ -776,12 +826,13 @@ gabble_file_transfer_channel_dispose (GObject *object)
if (self->priv->dispose_has_run)
return;
+ DEBUG ("dispose called");
self->priv->dispose_has_run = TRUE;
+ gabble_file_transfer_channel_do_close (self);
tp_handle_unref (handle_repo, self->priv->handle);
tp_handle_unref (handle_repo, self->priv->initiator);
- gabble_file_transfer_channel_do_close (self);
if (self->priv->progress_timer != 0)
{
@@ -789,23 +840,8 @@ gabble_file_transfer_channel_dispose (GObject *object)
self->priv->progress_timer = 0;
}
- if (self->priv->bytestream != NULL)
- {
- g_object_unref (self->priv->bytestream);
- self->priv->bytestream = NULL;
- }
+ close_session_and_transport (self);
- if (self->priv->listener != NULL)
- {
- g_object_unref (self->priv->listener);
- self->priv->listener = NULL;
- }
-
- if (self->priv->transport != NULL)
- {
- g_object_unref (self->priv->transport);
- self->priv->transport = NULL;
- }
/* release any references held by the object here */
@@ -847,13 +883,27 @@ gabble_file_transfer_channel_finalize (GObject *object)
g_free (self->priv->content_hash);
g_free (self->priv->description);
g_hash_table_destroy (self->priv->available_socket_types);
+ g_free (self->priv->file_collection);
G_OBJECT_CLASS (gabble_file_transfer_channel_parent_class)->finalize (object);
}
static void
-close_bytestream_and_transport (GabbleFileTransferChannel *self)
+close_session_and_transport (GabbleFileTransferChannel *self)
{
+
+ DEBUG ("Closing session and transport");
+ if (self->priv->gtalk_file_collection != NULL)
+ {
+ gtalk_file_collection_terminate (self->priv->gtalk_file_collection, self);
+ /* the terminate could synchronously unref it and set it to NULL */
+ if (self->priv->gtalk_file_collection != NULL)
+ {
+ g_object_unref (self->priv->gtalk_file_collection);
+ self->priv->gtalk_file_collection = NULL;
+ }
+ }
+
if (self->priv->bytestream != NULL)
{
gabble_bytestream_iface_close (self->priv->bytestream, NULL);
@@ -861,6 +911,12 @@ close_bytestream_and_transport (GabbleFileTransferChannel *self)
self->priv->bytestream = NULL;
}
+ if (self->priv->listener != NULL)
+ {
+ g_object_unref (self->priv->listener);
+ self->priv->listener = NULL;
+ }
+
if (self->priv->transport != NULL)
{
g_object_unref (self->priv->transport);
@@ -888,7 +944,7 @@ gabble_file_transfer_channel_close (TpSvcChannel *iface,
TP_FILE_TRANSFER_STATE_CANCELLED,
TP_FILE_TRANSFER_STATE_CHANGE_REASON_LOCAL_STOPPED);
- close_bytestream_and_transport (self);
+ close_session_and_transport (self);
}
gabble_file_transfer_channel_do_close (GABBLE_FILE_TRANSFER_CHANNEL (iface));
@@ -1011,8 +1067,14 @@ check_address_and_access_control (GabbleFileTransferChannel *self,
}
static void
-bytestream_open (GabbleFileTransferChannel *self)
+channel_open (GabbleFileTransferChannel *self)
{
+ DEBUG ("Channel open");
+
+ /* This is needed in case the ProvideFile wasn't called yet, to know if we
+ should go into OPEN state when ProvideFile gets called. */
+ self->priv->channel_opened = TRUE;
+
if (self->priv->socket_address != NULL)
{
/* ProvideFile has already been called. Channel is Open */
@@ -1024,7 +1086,7 @@ bytestream_open (GabbleFileTransferChannel *self)
TP_FILE_TRANSFER_STATE_OPEN,
TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE);
- if (self->priv->transport)
+ if (self->priv->transport != NULL)
gibber_transport_block_receiving (self->priv->transport, FALSE);
}
else
@@ -1040,7 +1102,8 @@ bytestream_open (GabbleFileTransferChannel *self)
static void
bytestream_closed (GabbleFileTransferChannel *self)
{
- if (self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED)
+ if (self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED &&
+ self->priv->state != TP_FILE_TRANSFER_STATE_CANCELLED)
{
TpBaseConnection *base_conn = (TpBaseConnection *)
self->priv->connection;
@@ -1058,6 +1121,7 @@ bytestream_closed (GabbleFileTransferChannel *self)
}
}
+
static void
bytestream_state_changed_cb (GabbleBytestreamIface *bytestream,
GabbleBytestreamState state,
@@ -1067,7 +1131,7 @@ bytestream_state_changed_cb (GabbleBytestreamIface *bytestream,
if (state == GABBLE_BYTESTREAM_STATE_OPEN)
{
- bytestream_open (self);
+ channel_open (self);
}
else if (state == GABBLE_BYTESTREAM_STATE_CLOSED)
{
@@ -1078,19 +1142,45 @@ bytestream_state_changed_cb (GabbleBytestreamIface *bytestream,
static void bytestream_write_blocked_cb (GabbleBytestreamIface *bytestream,
gboolean blocked,
GabbleFileTransferChannel *self);
-static void
+static gboolean
set_bytestream (GabbleFileTransferChannel *self,
- GabbleBytestreamIface *bytestream)
+ GabbleBytestreamIface *bytestream)
+
{
if (bytestream == NULL)
- return;
+ return FALSE;
+
+ g_return_val_if_fail (self->priv->bytestream == NULL, FALSE);
+ g_return_val_if_fail (self->priv->gtalk_file_collection == NULL, FALSE);
+
+ DEBUG ("Setting bytestream to %p", bytestream);
self->priv->bytestream = g_object_ref (bytestream);
gabble_signal_connect_weak (bytestream, "state-changed",
G_CALLBACK (bytestream_state_changed_cb), G_OBJECT (self));
- gabble_signal_connect_weak (self->priv->bytestream, "write-blocked",
+ gabble_signal_connect_weak (bytestream, "write-blocked",
G_CALLBACK (bytestream_write_blocked_cb), G_OBJECT (self));
+
+ return TRUE;
+}
+
+static gboolean
+set_gtalk_file_collection (
+ GabbleFileTransferChannel *self, GTalkFileCollection *gtalk_file_collection)
+{
+ if (gtalk_file_collection == NULL)
+ return FALSE;
+
+ g_return_val_if_fail (self->priv->bytestream == NULL, FALSE);
+ g_return_val_if_fail (self->priv->gtalk_file_collection == NULL, FALSE);
+
+ self->priv->gtalk_file_collection = g_object_ref (gtalk_file_collection);
+
+ /* No need to listen to any signals, the GTalkFileCollection will call our callbacks
+ on his own */
+
+ return TRUE;
}
static void
@@ -1137,70 +1227,28 @@ bytestream_negotiate_cb (GabbleBytestreamIface *bytestream,
set_bytestream (self, bytestream);
- self->priv->remote_accepted = TRUE;
}
-gboolean
-gabble_file_transfer_channel_offer_file (GabbleFileTransferChannel *self,
- GError **error)
+static gboolean
+offer_bytestream (GabbleFileTransferChannel *self, const gchar *jid,
+ const gchar *resource, GError **error)
{
- GabblePresence *presence;
gboolean result;
LmMessage *msg;
- TpHandleRepoIface *contact_repo, *room_repo;
LmMessageNode *si_node, *file_node;
- const gchar *jid;
- gchar *full_jid, *stream_id, *size_str;
-
- g_assert (!CHECK_STR_EMPTY (self->priv->filename));
- g_assert (self->priv->size != GABBLE_UNDEFINED_FILE_SIZE);
- g_assert (self->priv->bytestream == NULL);
-
- presence = gabble_presence_cache_get (self->priv->connection->presence_cache,
- self->priv->handle);
- if (presence == NULL)
- {
- DEBUG ("can't find contact's presence");
- g_set_error (error, TP_ERRORS, TP_ERROR_OFFLINE,
- "can't find contact's presence");
- return FALSE;
- }
+ gchar *stream_id, *size_str, *full_jid;
- contact_repo = tp_base_connection_get_handles (
- (TpBaseConnection *) self->priv->connection, TP_HANDLE_TYPE_CONTACT);
- room_repo = tp_base_connection_get_handles (
- (TpBaseConnection *) self->priv->connection, TP_HANDLE_TYPE_ROOM);
+ if (resource)
+ full_jid = g_strdup_printf ("%s/%s", jid, resource);
+ else
+ full_jid = g_strdup (jid);
- jid = tp_handle_inspect (contact_repo, self->priv->handle);
+ DEBUG ("Offering SI Bytestream file transfer to %s", full_jid);
- if (gabble_get_room_handle_from_jid (room_repo, jid) == 0)
- {
- /* Not a MUC jid, need to get a resource */
- const gchar *resource;
+ /* Outgoing FT , we'll need SOCK5 proxies */
+ gabble_bytestream_factory_query_socks5_proxies (
+ self->priv->connection->bytestream_factory);
- /* FIXME: should we check for SI, bytestreams and/or IBB too?
- * http://bugs.freedesktop.org/show_bug.cgi?id=23777 */
- resource = gabble_presence_pick_resource_by_caps (presence,
- DEVICE_AGNOSTIC,
- gabble_capability_set_predicate_has, NS_FILE_TRANSFER);
-
- if (resource == NULL)
- {
- DEBUG ("contact doesn't have file transfer capabilities");
- g_set_error (error, TP_ERRORS, TP_ERROR_NOT_CAPABLE,
- "contact doesn't have file transfer capabilities");
- return FALSE;
- }
-
- full_jid = g_strdup_printf ("%s/%s", jid, resource);
- }
- else
- {
- /* MUC jid, we already have the full jid */
- full_jid = g_strdup (jid);
- }
-
- DEBUG ("Offering file transfer to %s", full_jid);
stream_id = gabble_bytestream_factory_generate_stream_id ();
@@ -1249,8 +1297,183 @@ gabble_file_transfer_channel_offer_file (GabbleFileTransferChannel *self,
lm_message_unref (msg);
g_free (stream_id);
- g_free (full_jid);
g_free (size_str);
+ g_free (full_jid);
+
+ return result;
+}
+
+
+void
+gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ GabbleFileTransferChannel *self,
+ GTalkFileCollectionState state, gboolean local_terminator)
+{
+ DEBUG ("gtalk ft state changed to %d", state);
+ switch (state)
+ {
+ case GTALK_FILE_COLLECTION_STATE_PENDING:
+ gabble_file_transfer_channel_set_state (
+ TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self),
+ TP_FILE_TRANSFER_STATE_PENDING,
+ TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE);
+ break;
+ case GTALK_FILE_COLLECTION_STATE_ACCEPTED:
+ if (self->priv->state == TP_FILE_TRANSFER_STATE_PENDING)
+ {
+ gabble_file_transfer_channel_set_state (
+ TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self),
+ TP_FILE_TRANSFER_STATE_ACCEPTED,
+ TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE);
+ }
+ break;
+ case GTALK_FILE_COLLECTION_STATE_OPEN:
+ channel_open (self);
+ break;
+ case GTALK_FILE_COLLECTION_STATE_TERMINATED:
+ if (self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED &&
+ self->priv->state != TP_FILE_TRANSFER_STATE_CANCELLED)
+ {
+ gabble_file_transfer_channel_set_state (
+ TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self),
+ TP_FILE_TRANSFER_STATE_CANCELLED,
+ local_terminator ?
+ TP_FILE_TRANSFER_STATE_CHANGE_REASON_LOCAL_STOPPED:
+ TP_FILE_TRANSFER_STATE_CHANGE_REASON_REMOTE_STOPPED);
+ }
+ close_session_and_transport (self);
+ break;
+ case GTALK_FILE_COLLECTION_STATE_ERROR:
+ case GTALK_FILE_COLLECTION_STATE_CONNECTION_FAILED:
+ gabble_file_transfer_channel_set_state (
+ TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self),
+ TP_FILE_TRANSFER_STATE_CANCELLED,
+ TP_FILE_TRANSFER_STATE_CHANGE_REASON_LOCAL_ERROR);
+
+ close_session_and_transport (self);
+ break;
+ case GTALK_FILE_COLLECTION_STATE_COMPLETED:
+ gabble_file_transfer_channel_set_state (
+ TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self),
+ TP_FILE_TRANSFER_STATE_COMPLETED,
+ TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE);
+
+ if (self->priv->transport &&
+ gibber_transport_buffer_is_empty (self->priv->transport))
+ gibber_transport_disconnect (self->priv->transport);
+ break;
+ }
+}
+
+static gboolean
+offer_gtalk_file_transfer (GabbleFileTransferChannel *self,
+ const gchar *full_jid, GError **error)
+{
+
+ GTalkFileCollection *gtalk_file_collection;
+
+ DEBUG ("Offering Gtalk file transfer to %s", full_jid);
+
+ gtalk_file_collection = gtalk_file_collection_new (self,
+ self->priv->connection->jingle_factory, self->priv->handle, full_jid);
+
+ g_return_val_if_fail (gtalk_file_collection != NULL, FALSE);
+
+ set_gtalk_file_collection (self, gtalk_file_collection);
+
+ gtalk_file_collection_initiate (self->priv->gtalk_file_collection, self);
+
+ /* We would have gotten a set_gtalk_file_collection so we already hold an
+ additional reference to the object, so we can drop the reference we got
+ from the gtalk_file_collection_new. If we didn't get our
+ set_gtalk_file_collection called, then the ft manager doesn't handle us,
+ so it's best to just destroy it anyways */
+ g_object_unref (gtalk_file_collection);
+
+ return TRUE;
+}
+
+gboolean
+gabble_file_transfer_channel_offer_file (GabbleFileTransferChannel *self,
+ GError **error)
+{
+ GabblePresence *presence;
+ gboolean result;
+ TpHandleRepoIface *contact_repo, *room_repo;
+ const gchar *jid;
+ gboolean si = FALSE;
+ gboolean jingle_share = FALSE;
+ const gchar *si_resource = NULL;
+ const gchar *share_resource = NULL;
+ g_assert (!CHECK_STR_EMPTY (self->priv->filename));
+ g_assert (self->priv->size != GABBLE_UNDEFINED_FILE_SIZE);
+ g_return_val_if_fail (self->priv->bytestream == NULL, FALSE);
+ g_return_val_if_fail (self->priv->gtalk_file_collection == NULL, FALSE);
+
+ presence = gabble_presence_cache_get (self->priv->connection->presence_cache,
+ self->priv->handle);
+
+ if (presence == NULL)
+ {
+ DEBUG ("can't find contact's presence");
+ g_set_error (error, TP_ERRORS, TP_ERROR_OFFLINE,
+ "can't find contact's presence");
+
+ return FALSE;
+ }
+
+ contact_repo = tp_base_connection_get_handles (
+ (TpBaseConnection *) self->priv->connection, TP_HANDLE_TYPE_CONTACT);
+ room_repo = tp_base_connection_get_handles (
+ (TpBaseConnection *) self->priv->connection, TP_HANDLE_TYPE_ROOM);
+
+ jid = tp_handle_inspect (contact_repo, self->priv->handle);
+ if (gabble_get_room_handle_from_jid (room_repo, jid) == 0)
+ {
+ /* Not a MUC jid, need to get a resource */
+
+ /* FIXME: should we check for SI, bytestreams and/or IBB too?
+ * http://bugs.freedesktop.org/show_bug.cgi?id=23777 */
+ si_resource = gabble_presence_pick_resource_by_caps (presence,
+ DEVICE_AGNOSTIC, gabble_capability_set_predicate_has,
+ NS_FILE_TRANSFER);
+ si = (si_resource != NULL);
+
+ share_resource = gabble_presence_pick_resource_by_caps (presence,
+ DEVICE_AGNOSTIC, gabble_capability_set_predicate_has,
+ NS_GOOGLE_FEAT_SHARE);
+ jingle_share = (share_resource != NULL);
+ }
+ else
+ {
+ /* MUC jid, we already have the full jid */
+ si = gabble_presence_has_cap (presence, NS_FILE_TRANSFER);
+ jingle_share = gabble_presence_has_cap (presence, NS_GOOGLE_FEAT_SHARE);
+ }
+
+ /* Use bytestream if we have SI, but no jingle-share or if we have SI and
+ jingle-share but we have no google relay token */
+ if (si &&
+ (!jingle_share ||
+ gabble_jingle_factory_get_google_relay_token (
+ self->priv->connection->jingle_factory) == NULL))
+ {
+ result = offer_bytestream (self, jid, si_resource, error);
+ }
+ else if (jingle_share)
+ {
+ gchar *full_jid = gabble_peer_to_jid (self->priv->connection,
+ self->priv->handle, share_resource);
+ result = offer_gtalk_file_transfer (self, full_jid, error);
+ g_free (full_jid);
+ }
+ else
+ {
+ DEBUG ("contact doesn't have file transfer capabilities");
+ g_set_error (error, TP_ERRORS, TP_ERROR_NOT_CAPABLE,
+ "contact doesn't have file transfer capabilities");
+ result = FALSE;
+ }
return result;
}
@@ -1339,20 +1562,14 @@ transferred_chunk (GabbleFileTransferChannel *self,
emit_progress_update_cb, self);
}
-
static void
-data_received_cb (GabbleBytestreamIface *stream,
- TpHandle sender,
- GString *data,
- gpointer user_data)
+data_received_cb (GabbleFileTransferChannel *self, const guint8 *data, guint len)
{
- GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data);
GError *error = NULL;
g_assert (self->priv->transport != NULL);
- if (!gibber_transport_send (self->priv->transport, (const guint8 *) data->str,
- data->len, &error))
+ if (!gibber_transport_send (self->priv->transport, data, len, &error))
{
DEBUG ("sending to transport failed: %s", error->message);
g_error_free (error);
@@ -1364,9 +1581,10 @@ data_received_cb (GabbleBytestreamIface *stream,
return;
}
- transferred_chunk (self, (guint64) data->len);
+ transferred_chunk (self, (guint64) len);
- if (self->priv->transferred_bytes + self->priv->initial_offset >=
+ if (self->priv->bytestream != NULL &&
+ self->priv->transferred_bytes + self->priv->initial_offset >=
self->priv->size)
{
DEBUG ("Received all the file. Transfer is complete");
@@ -1384,10 +1602,33 @@ data_received_cb (GabbleBytestreamIface *stream,
if (!gibber_transport_buffer_is_empty (self->priv->transport))
{
/* We don't want to send more data while the buffer isn't empty */
- gabble_bytestream_iface_block_reading (self->priv->bytestream, TRUE);
+ if (self->priv->bytestream != NULL)
+ gabble_bytestream_iface_block_reading (self->priv->bytestream, TRUE);
+ else if (self->priv->gtalk_file_collection != NULL)
+ gtalk_file_collection_block_reading (self->priv->gtalk_file_collection,
+ self, TRUE);
}
}
+
+void
+gabble_file_transfer_channel_gtalk_file_collection_data_received (
+ GabbleFileTransferChannel *self, const gchar *data, guint len)
+{
+ data_received_cb (self, (const guint8 *) data, len);
+}
+
+
+static void
+bytestream_data_received_cb (GabbleBytestreamIface *stream,
+ TpHandle sender,
+ GString *data,
+ gpointer user_data)
+{
+ GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data);
+ data_received_cb (self, (const guint8 *) data->str, data->len);
+}
+
static void
augment_si_reply (LmMessageNode *si,
gpointer user_data)
@@ -1487,17 +1728,31 @@ gabble_file_transfer_channel_accept_file (TpSvcChannelTypeFileTransfer *iface,
self->priv->initial_offset = 0;
}
- g_assert (self->priv->bytestream != NULL);
- gabble_signal_connect_weak (self->priv->bytestream, "data-received",
- G_CALLBACK (data_received_cb), G_OBJECT (self));
+ if (self->priv->bytestream != NULL)
+ {
+ gabble_signal_connect_weak (self->priv->bytestream, "data-received",
+ G_CALLBACK (bytestream_data_received_cb), G_OBJECT (self));
- /* Block the bytestream while the user is not connected to the socket */
- gabble_bytestream_iface_block_reading (self->priv->bytestream, TRUE);
+ /* Block the bytestream while the user is not connected to the socket */
+ gabble_bytestream_iface_block_reading (self->priv->bytestream, TRUE);
- /* channel state will change to open once the bytestream is open */
- gabble_bytestream_iface_accept (self->priv->bytestream, augment_si_reply,
- self);
+ /* channel state will change to open once the bytestream is open */
+ gabble_bytestream_iface_accept (self->priv->bytestream, augment_si_reply,
+ self);
+ }
+ else if (self->priv->gtalk_file_collection != NULL)
+ {
+ /* Block the gtalk ft stream while the user is not connected
+ to the socket */
+ gtalk_file_collection_block_reading (self->priv->gtalk_file_collection,
+ self, TRUE);
+ gtalk_file_collection_accept (self->priv->gtalk_file_collection, self);
+ }
+ else
+ {
+ g_assert_not_reached ();
+ }
}
/**
@@ -1565,7 +1820,7 @@ gabble_file_transfer_channel_provide_file (
return;
}
- if (self->priv->remote_accepted)
+ if (self->priv->channel_opened)
{
/* Remote already accepted the file. Channel is Open.
* If not channel stay Pending. */
@@ -1633,12 +1888,25 @@ transport_handler (GibberTransport *transport,
{
GabbleFileTransferChannel *self = GABBLE_FILE_TRANSFER_CHANNEL (user_data);
- if (!gabble_bytestream_iface_send (self->priv->bytestream, data->length,
- (const gchar *) data->data))
+ if (self->priv->bytestream != NULL)
{
- DEBUG ("Sending failed. Closing the bytestream");
- close_bytestream_and_transport (self);
- return;
+ if (!gabble_bytestream_iface_send (self->priv->bytestream, data->length,
+ (const gchar *) data->data))
+ {
+ DEBUG ("Sending failed. Closing the bytestream");
+ close_session_and_transport (self);
+ return;
+ }
+ }
+ else if (self->priv->gtalk_file_collection != NULL)
+ {
+ if (!gtalk_file_collection_send_data (self->priv->gtalk_file_collection,
+ self, (const gchar *) data->data, data->length))
+ {
+ DEBUG ("Sending failed. Closing the jingle session");
+ close_session_and_transport (self);
+ return;
+ }
}
transferred_chunk (self, (guint64) data->length);
@@ -1646,15 +1914,21 @@ transport_handler (GibberTransport *transport,
if (self->priv->transferred_bytes + self->priv->initial_offset >=
self->priv->size)
{
- DEBUG ("All the file has been sent. Closing the bytestream");
-
- gabble_file_transfer_channel_set_state (
- TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self),
- TP_FILE_TRANSFER_STATE_COMPLETED,
- TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE);
-
- gabble_bytestream_iface_close (self->priv->bytestream, NULL);
- return;
+ if (self->priv->bytestream != NULL)
+ {
+ DEBUG ("All the file has been sent. Closing the bytestream");
+ gabble_file_transfer_channel_set_state (
+ TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self),
+ TP_FILE_TRANSFER_STATE_COMPLETED,
+ TP_FILE_TRANSFER_STATE_CHANGE_REASON_NONE);
+ gabble_bytestream_iface_close (self->priv->bytestream, NULL);
+ }
+ else if (self->priv->gtalk_file_collection != NULL)
+ {
+ DEBUG ("All the file has been sent.");
+ gtalk_file_collection_completed (self->priv->gtalk_file_collection,
+ self);
+ }
}
}
@@ -1663,44 +1937,71 @@ bytestream_write_blocked_cb (GabbleBytestreamIface *bytestream,
gboolean blocked,
GabbleFileTransferChannel *self)
{
- if (self->priv->transport)
+ if (self->priv->transport != NULL)
gibber_transport_block_receiving (self->priv->transport, blocked);
}
+void
+gabble_file_transfer_channel_gtalk_file_collection_write_blocked (
+ GabbleFileTransferChannel *self, gboolean blocked)
+{
+ if (self->priv->transport != NULL)
+ gibber_transport_block_receiving (self->priv->transport, blocked);
+}
+
+
static void
file_transfer_send (GabbleFileTransferChannel *self)
{
- gibber_transport_set_handler (self->priv->transport, transport_handler, self);
- /* We shouldn't receive data if the bytestream isn't open otherwise it will
- error out */
+ /* We shouldn't receive data if the bytestream isn't open otherwise it
+ will error out */
if (self->priv->state == TP_FILE_TRANSFER_STATE_OPEN)
gibber_transport_block_receiving (self->priv->transport, FALSE);
else
gibber_transport_block_receiving (self->priv->transport, TRUE);
+
+ gibber_transport_set_handler (self->priv->transport, transport_handler,
+ self);
}
static void
file_transfer_receive (GabbleFileTransferChannel *self)
{
/* Client is connected, we can now receive data. Unblock the bytestream */
- g_assert (self->priv->bytestream != NULL);
- gabble_bytestream_iface_block_reading (self->priv->bytestream, FALSE);
+ if (self->priv->bytestream != NULL)
+ gabble_bytestream_iface_block_reading (self->priv->bytestream, FALSE);
+ else if (self->priv->gtalk_file_collection != NULL)
+ gtalk_file_collection_block_reading (self->priv->gtalk_file_collection,
+ self, FALSE);
}
static void
transport_disconnected_cb (GibberTransport *transport,
GabbleFileTransferChannel *self)
{
+ TpBaseConnection *base_conn = (TpBaseConnection *) self->priv->connection;
+ gboolean requested = (self->priv->initiator == base_conn->self_handle);
+
DEBUG ("transport to local socket has been disconnected");
- if (self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED)
+ /* If we are sending the file, we can expect the transport to be closed as
+ soon as we received all the data. Otherwise, it should only get closed once
+ the channel has gone to state COMPLETED.
+ This allows to make sure we detect an error if the channel is closed while
+ receiving a gtalk-ft folder where the size is an approximation of the real
+ size to be received */
+ if ((requested &&
+ self->priv->transferred_bytes + self->priv->initial_offset <
+ self->priv->size) ||
+ (!requested && self->priv->state != TP_FILE_TRANSFER_STATE_COMPLETED))
{
- close_bytestream_and_transport (self);
gabble_file_transfer_channel_set_state (
TP_SVC_CHANNEL_TYPE_FILE_TRANSFER (self),
TP_FILE_TRANSFER_STATE_CANCELLED,
TP_FILE_TRANSFER_STATE_CHANGE_REASON_LOCAL_ERROR);
+
+ close_session_and_transport (self);
}
}
@@ -1709,7 +2010,12 @@ transport_buffer_empty_cb (GibberTransport *transport,
GabbleFileTransferChannel *self)
{
/* Buffer is empty so we can unblock the buffer if it was blocked */
- gabble_bytestream_iface_block_reading (self->priv->bytestream, FALSE);
+ if (self->priv->bytestream != NULL)
+ gabble_bytestream_iface_block_reading (self->priv->bytestream, FALSE);
+
+ if (self->priv->gtalk_file_collection != NULL)
+ gtalk_file_collection_block_reading (self->priv->gtalk_file_collection,
+ self, FALSE);
if (self->priv->state > TP_FILE_TRANSFER_STATE_OPEN)
gibber_transport_disconnect (transport);
@@ -1858,8 +2164,8 @@ setup_local_socket (GabbleFileTransferChannel *self,
self->priv->socket_type = address_type;
- g_signal_connect (self->priv->listener, "new-connection",
- G_CALLBACK (new_connection_cb), self);
+ gabble_signal_connect_weak (self->priv->listener, "new-connection",
+ G_CALLBACK (new_connection_cb), G_OBJECT (self));
return TRUE;
}
@@ -1877,8 +2183,10 @@ gabble_file_transfer_channel_new (GabbleConnection *conn,
const gchar *description,
guint64 date,
guint64 initial_offset,
+ gboolean resume_supported,
GabbleBytestreamIface *bytestream,
- gboolean resume_supported)
+ GTalkFileCollection *gtalk_file_collection,
+ const gchar *file_collection)
{
return g_object_new (GABBLE_TYPE_FILE_TRANSFER_CHANNEL,
@@ -1894,7 +2202,9 @@ gabble_file_transfer_channel_new (GabbleConnection *conn,
"description", description,
"date", date,
"initial-offset", initial_offset,
- "bytestream", bytestream,
"resume-supported", resume_supported,
+ "file-collection", file_collection,
+ "bytestream", bytestream,
+ "gtalk-file-collection", gtalk_file_collection,
NULL);
}
diff --git a/src/ft-channel.h b/src/ft-channel.h
index 2bd339d4b..9763f08ef 100644
--- a/src/ft-channel.h
+++ b/src/ft-channel.h
@@ -27,9 +27,14 @@
#include <extensions/_gen/interfaces.h>
#include <extensions/_gen/enums.h>
+typedef struct _GabbleFileTransferChannel GabbleFileTransferChannel;
+
+#include "gtalk-file-collection.h"
+
+#include "bytestream-factory.h"
+
G_BEGIN_DECLS
-typedef struct _GabbleFileTransferChannel GabbleFileTransferChannel;
typedef struct _GabbleFileTransferChannelClass GabbleFileTransferChannelClass;
typedef struct _GabbleFileTransferChannelPrivate GabbleFileTransferChannelPrivate;
@@ -68,11 +73,32 @@ gabble_file_transfer_channel_new (GabbleConnection *conn,
const gchar *content_type, const gchar *filename, guint64 size,
TpFileHashType content_hash_type, const gchar *content_hash,
const gchar *description, guint64 date, guint64 initial_offset,
- GabbleBytestreamIface *bytestream, gboolean resume_supported);
+ gboolean resume_supported, GabbleBytestreamIface *bytestream,
+ GTalkFileCollection *gtalk_fc, const gchar *file_collection);
gboolean gabble_file_transfer_channel_offer_file (
GabbleFileTransferChannel *self, GError **error);
+/* The following methods are a hack, they are 'signal-like' callbacks for the
+ GTalkFileCollection. They have to be made this way because the FileCollection
+ can't send out signals since it needs its signals to be sent to a specific
+ channel only. So instead it calls these callbacks directly on the channel it
+ needs to notify. This is a known layering violation and accepted as the lesser
+ of any other evil [hack]. */
+void gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ GabbleFileTransferChannel *self, GTalkFileCollectionState gtalk_fc_state,
+ gboolean local_terminator);
+
+void gabble_file_transfer_channel_gtalk_file_collection_write_blocked (
+ GabbleFileTransferChannel *self, gboolean blocked);
+
+void gabble_file_transfer_channel_gtalk_file_collection_data_received (
+ GabbleFileTransferChannel *self, const gchar *data, guint len);
+
+void
+gabble_file_transfer_channel_do_close (GabbleFileTransferChannel *self);
+
+
G_END_DECLS
#endif /* #ifndef __GABBLE_FILE_TRANSFER_CHANNEL_H__*/
diff --git a/src/ft-manager.c b/src/ft-manager.c
index e746cb5af..206c1c391 100644
--- a/src/ft-manager.c
+++ b/src/ft-manager.c
@@ -1,6 +1,6 @@
/*
* ft-manager.c - Source for GabbleFtManager
- * Copyright (C) 2009 Collabora Ltd.
+ * Copyright (C) 2009-2010 Collabora Ltd.
* @author: Guillaume Desmottes <guillaume.desmottes@collabora.co.uk>
*
* This library is free software; you can redistribute it and/or
@@ -28,6 +28,8 @@
#include <string.h>
#include <glib/gstdio.h>
+#include "jingle-session.h"
+#include "jingle-share.h"
#include "caps-channel-manager.h"
#include "connection.h"
#include "ft-manager.h"
@@ -56,6 +58,7 @@ channel_manager_iface_init (gpointer, gpointer);
static void gabble_ft_manager_channel_created (GabbleFtManager *mgr,
GabbleFileTransferChannel *chan, gpointer request_token);
+
static void caps_channel_manager_iface_init (gpointer g_iface,
gpointer iface_data);
@@ -80,6 +83,7 @@ struct _GabbleFtManagerPrivate
GList *channels;
/* path of the temporary directory used to store UNIX sockets */
gchar *tmp_dir;
+ gulong status_changed_id;
};
static void
@@ -93,8 +97,13 @@ gabble_ft_manager_init (GabbleFtManager *obj)
obj->priv->channels = NULL;
}
+static void gabble_ft_manager_constructed (GObject *object);
static void gabble_ft_manager_dispose (GObject *object);
static void gabble_ft_manager_finalize (GObject *object);
+static void connection_status_changed_cb (GabbleConnection *conn,
+ guint status,
+ guint reason,
+ GabbleFtManager *self);
static void
gabble_ft_manager_get_property (GObject *object,
@@ -143,6 +152,7 @@ gabble_ft_manager_class_init (GabbleFtManagerClass *gabble_ft_manager_class)
g_type_class_add_private (gabble_ft_manager_class,
sizeof (GabbleFtManagerPrivate));
+ object_class->constructed = gabble_ft_manager_constructed;
object_class->get_property = gabble_ft_manager_get_property;
object_class->set_property = gabble_ft_manager_set_property;
@@ -158,26 +168,46 @@ gabble_ft_manager_class_init (GabbleFtManagerClass *gabble_ft_manager_class)
g_object_class_install_property (object_class, PROP_CONNECTION, param_spec);
}
+
+static void
+gabble_ft_manager_constructed (GObject *object)
+{
+ void (*chain_up) (GObject *) =
+ G_OBJECT_CLASS (gabble_ft_manager_parent_class)->constructed;
+ GabbleFtManager *self = GABBLE_FT_MANAGER (object);
+
+ if (chain_up != NULL)
+ chain_up (object);
+
+ self->priv->status_changed_id = g_signal_connect (self->priv->connection,
+ "status-changed", (GCallback) connection_status_changed_cb, object);
+}
+
+static void
+ft_manager_close_all (GabbleFtManager *self)
+{
+ GList *l;
+
+ while ((l = self->priv->channels) != NULL)
+ {
+ gabble_file_transfer_channel_do_close (l->data);
+ /* Channels should have closed and disappeared from the list */
+ g_assert (l != self->priv->channels);
+ }
+}
+
+
void
gabble_ft_manager_dispose (GObject *object)
{
GabbleFtManager *self = GABBLE_FT_MANAGER (object);
- GList *tmp, *l;
if (self->priv->dispose_has_run)
return;
self->priv->dispose_has_run = TRUE;
- tmp = self->priv->channels;
- self->priv->channels = NULL;
-
- for (l = tmp; l != NULL; l = g_list_next (l))
- {
- g_object_unref (l->data);
- }
-
- g_list_free (tmp);
+ g_assert (self->priv->channels == NULL);
if (G_OBJECT_CLASS (gabble_ft_manager_parent_class)->dispose)
G_OBJECT_CLASS (gabble_ft_manager_parent_class)->dispose (object);
@@ -257,6 +287,31 @@ file_channel_closed_cb (GabbleFileTransferChannel *chan,
}
static void
+gabble_ft_manager_channels_created (GabbleFtManager *self, GList *channels)
+{
+ GList *i;
+ GHashTable *new_channels = g_hash_table_new_full (g_direct_hash,
+ g_direct_equal, NULL, NULL);
+
+ for (i = channels; i ; i = i->next)
+ {
+ GabbleFileTransferChannel *chan = i->data;
+
+ gabble_signal_connect_weak (chan, "closed",
+ G_CALLBACK (file_channel_closed_cb), G_OBJECT (self));
+
+ self->priv->channels = g_list_append (self->priv->channels, chan);
+ /* The channels can't satisfy a request because this will always be called
+ when we receive an incoming jingle-share session */
+ g_hash_table_insert (new_channels, chan, NULL);
+ }
+
+ tp_channel_manager_emit_new_channels (self, new_channels);
+
+ g_hash_table_destroy (new_channels);
+}
+
+static void
gabble_ft_manager_channel_created (GabbleFtManager *self,
GabbleFileTransferChannel *chan,
gpointer request_token)
@@ -277,6 +332,104 @@ gabble_ft_manager_channel_created (GabbleFtManager *self,
g_slist_free (requests);
}
+
+static void
+new_jingle_session_cb (GabbleJingleFactory *jf,
+ GabbleJingleSession *sess,
+ gpointer data)
+{
+ GabbleFtManager *self = GABBLE_FT_MANAGER (data);
+ GTalkFileCollection *gtalk_fc = NULL;
+ GabbleJingleContent *content = NULL;
+ GabbleJingleShareManifest *manifest = NULL;
+ GList *channels = NULL;
+ GList *cs, *i;
+
+ if (gabble_jingle_session_get_content_type (sess) ==
+ GABBLE_TYPE_JINGLE_SHARE)
+ {
+ cs = gabble_jingle_session_get_contents (sess);
+
+ if (cs != NULL)
+ {
+ content = GABBLE_JINGLE_CONTENT (cs->data);
+ g_list_free (cs);
+ }
+
+ if (content == NULL)
+ return;
+
+ gtalk_fc = gtalk_file_collection_new_from_session (jf, sess);
+
+ if (gtalk_fc)
+ {
+ gchar *token = NULL;
+
+ g_object_get (gtalk_fc,
+ "token", &token,
+ NULL);
+
+ manifest = gabble_jingle_share_get_manifest (
+ GABBLE_JINGLE_SHARE (content));
+ for (i = manifest->entries; i; i = i->next)
+ {
+ GabbleJingleShareManifestEntry *entry = i->data;
+ GabbleFileTransferChannel *channel = NULL;
+ gchar *filename = NULL;
+
+ filename = g_strdup_printf ("%s%s",
+ entry->name, entry->folder? ".tar":"");
+ channel = gabble_file_transfer_channel_new (self->priv->connection,
+ sess->peer, sess->peer, TP_FILE_TRANSFER_STATE_PENDING,
+ NULL, filename, entry->size, TP_FILE_HASH_TYPE_NONE, NULL,
+ NULL, 0, 0, FALSE, NULL, gtalk_fc, token);
+ g_free (filename);
+
+ gtalk_file_collection_add_channel (gtalk_fc, channel);
+ channels = g_list_prepend (channels, channel);
+ }
+
+ if (channels != NULL)
+ gabble_ft_manager_channels_created (self, channels);
+
+ g_list_free (channels);
+
+ /* Channels will hold the reference to the gtalk file collection,
+ so we can drop ours already. If no channels were created,
+ then we need to destroy it anyways */
+ g_object_unref (gtalk_fc);
+ }
+ }
+}
+
+
+static void
+connection_status_changed_cb (GabbleConnection *conn,
+ guint status,
+ guint reason,
+ GabbleFtManager *self)
+{
+
+ switch (status)
+ {
+ case TP_CONNECTION_STATUS_CONNECTING:
+ g_signal_connect (self->priv->connection->jingle_factory,
+ "new-session",
+ G_CALLBACK (new_jingle_session_cb), self);
+ break;
+
+ case TP_CONNECTION_STATUS_DISCONNECTED:
+ ft_manager_close_all (self);
+ if (self->priv->status_changed_id != 0)
+ {
+ g_signal_handler_disconnect (self->priv->connection,
+ self->priv->status_changed_id);
+ self->priv->status_changed_id = 0;
+ }
+ break;
+ }
+}
+
static gboolean
gabble_ft_manager_handle_request (TpChannelManager *manager,
gpointer request_token,
@@ -399,7 +552,7 @@ gabble_ft_manager_handle_request (TpChannelManager *manager,
chan = gabble_file_transfer_channel_new (self->priv->connection,
handle, base_connection->self_handle, TP_FILE_TRANSFER_STATE_PENDING,
content_type, filename, size, content_hash_type, content_hash,
- description, date, initial_offset, NULL, TRUE);
+ description, date, initial_offset, TRUE, NULL, NULL, NULL);
if (!gabble_file_transfer_channel_offer_file (chan, &error))
{
@@ -563,7 +716,7 @@ void gabble_ft_manager_handle_si_request (GabbleFtManager *self,
chan = gabble_file_transfer_channel_new (self->priv->connection,
handle, handle, TP_FILE_TRANSFER_STATE_PENDING,
content_type, filename, size, content_hash_type, content_hash,
- description, date, 0, bytestream, resume_supported);
+ description, date, 0, resume_supported, bytestream, NULL, NULL);
gabble_ft_manager_channel_created (self, chan, NULL);
}
@@ -653,7 +806,8 @@ gabble_ft_manager_get_contact_caps (
const GabbleCapabilitySet *caps,
GPtrArray *arr)
{
- if (gabble_capability_set_has (caps, NS_FILE_TRANSFER))
+ if (gabble_capability_set_has (caps, NS_FILE_TRANSFER) ||
+ gabble_capability_set_has (caps, NS_GOOGLE_FEAT_SHARE))
add_file_transfer_channel_class (arr);
}
@@ -683,6 +837,7 @@ gabble_ft_manager_represent_client (
DEBUG ("client %s supports file transfer", client_name);
gabble_capability_set_add (cap_set, NS_FILE_TRANSFER);
+ gabble_capability_set_add (cap_set, NS_GOOGLE_FEAT_SHARE);
/* there's no point in looking at the subsequent filters if we've
* already added the FT capability */
break;
diff --git a/src/gtalk-file-collection.c b/src/gtalk-file-collection.c
new file mode 100644
index 000000000..4275b23dd
--- /dev/null
+++ b/src/gtalk-file-collection.c
@@ -0,0 +1,1786 @@
+/*
+ * gtalk-file-collection.c - Source for GTalkFileCollection
+ *
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * 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 "gtalk-file-collection.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <glib.h>
+
+#define DEBUG_FLAG GABBLE_DEBUG_SHARE
+
+#include "debug.h"
+#include "jingle-factory.h"
+#include "jingle-session.h"
+#include "jingle-share.h"
+#include "namespaces.h"
+#include "util.h"
+
+#include <nice/agent.h>
+
+/*
+ * This GTalk compatible file transfer protocol is a bit complicated, so here
+ * is an explanation on how it works :
+ *
+ * A pseudo-good initial source of information is available here :
+ * http://code.google.com/apis/talk/libjingle/file_share.html
+ *
+ * The current object interaction is like this :
+ *
+ * GabbleFileTransferChannelManager
+ * |
+ * |
+ * |
+ * |
+ * * ref
+ * GabbleFileTransferChannel
+ * weakref * *
+ * / \
+ * / \
+ * / \
+ * / \
+ * / \
+ * / \
+ * / \
+ * ref / \
+ * GTalkFileCollection \
+ * | \
+ * | \
+ * ref | \
+ * GabbleJingleSession \
+ * | \ (one at a time)
+ * | \
+ * ref | \
+ * GabbleJingleShare ------------------*- ShareChannel -------- NiceAgent
+ * | |
+ * | |
+ * ref | ref |
+ * GabbleTransportGoogle ----------------*- JingleCandidate PseudoTCP
+ *
+ * The protocol works like this :
+ * Once you receive an invitation, the manifest will contain a number of files
+ * and folders. Some files might have image attributes (width/height) that help
+ * specify that they are images.
+ * If there are images in the invitation, a new ShareChannel gets created and
+ * connectivity must be established. Then on that stream, an HTTP GET request
+ * is sent for each image being transfered with the URL as :
+ * GET <preview-path>/filename?width=X&height=Y
+ * where X and Y are the thumbnail's requested width and height.
+ * The peer should at this point scale down the image to the requested width and
+ * height and send the thumbnail for showing the preview of the image in the FT
+ * UI.
+ * Once the invitation is accepted, a new ShareChannel is created, which will
+ * cause a new NiceAgent to be created and connectivity to be established on that
+ * ShareChannel. The resulting stream is then used as an HTTP server/client to
+ * request files on it using the <source-path> as prefix to the URL.
+ * Simple files are being transferred normally, while directories will be
+ * transferred as a tarball with 'chuncked' Transfer-Encoding since the resulting
+ * size of the tarball isn't known in advance.
+ * Once a file is completely transferred, then the next file is requested on the
+ * same ShareChannel. If all files are transferred, then the <complete> info
+ * action is being sent through the jingle signaling, and the session can then
+ * be terminated safely.
+ *
+ * Since telepathy doesn't currently support image previews, so the 'preview'
+ * ShareChannel is never created with gabble.
+ * Also note that we only create one ShareChannel and we serialize the file
+ * transfers one after the other, they do not each get one ShareChannel and they
+ * cannot be downloaded in parallel.
+ *
+ */
+
+G_DEFINE_TYPE (GTalkFileCollection, gtalk_file_collection, G_TYPE_OBJECT);
+
+/* properties */
+enum
+{
+ PROP_TOKEN = 1,
+ LAST_PROPERTY
+};
+
+typedef enum
+ {
+ HTTP_SERVER_IDLE,
+ HTTP_SERVER_HEADERS,
+ HTTP_SERVER_SEND,
+ HTTP_CLIENT_IDLE,
+ HTTP_CLIENT_RECEIVE,
+ HTTP_CLIENT_HEADERS,
+ HTTP_CLIENT_CHUNK_SIZE,
+ HTTP_CLIENT_CHUNK_END,
+ HTTP_CLIENT_CHUNK_FINAL,
+ HTTP_CLIENT_BODY,
+ } HttpStatus;
+
+
+typedef struct
+{
+ NiceAgent *agent;
+ guint stream_id;
+ guint component_id;
+ gboolean agent_attached;
+ GabbleJingleShare *content;
+ guint share_channel_id;
+ HttpStatus http_status;
+ gchar *status_line;
+ gboolean is_chunked;
+ guint64 content_length;
+ gchar *write_buffer;
+ guint write_len;
+ gchar *read_buffer;
+ guint read_len;
+} ShareChannel;
+
+
+typedef enum
+{
+ GTALK_FT_STATUS_PENDING,
+ GTALK_FT_STATUS_INITIATED,
+ GTALK_FT_STATUS_ACCEPTED,
+ GTALK_FT_STATUS_TRANSFERRING,
+ GTALK_FT_STATUS_WAITING,
+ GTALK_FT_STATUS_TERMINATED
+} GtalkFtStatus;
+
+struct _GTalkFileCollectionPrivate
+{
+ gboolean dispose_has_run;
+
+ GtalkFtStatus status;
+ /* GList of weakreffed GabbleFileTransferChannel */
+ GList *channels;
+ /* GHashTable of GabbleFileTransferChannel => GINT_TO_POINTER (gboolean) */
+ /* the weakref to the channel here is held through the GList *channels */
+ GHashTable *channels_reading;
+ /* GHashTable of GabbleFileTransferChannel => GINT_TO_POINTER (gboolean) */
+ /* the weakref to the channel here is held through the GList *channels */
+ GHashTable *channels_usable;
+ GabbleFileTransferChannel *current_channel;
+ GabbleJingleFactory *jingle_factory;
+ GabbleJingleSession *jingle;
+ /* ICE component id to jingle share channel association
+ GINT_TO_POINTER (candidate->component) => g_slice_new (ShareChannel) */
+ GHashTable *share_channels;
+ gboolean requested;
+ gchar *token;
+};
+
+static void free_share_channel (gpointer data);
+static void nice_data_received_cb (NiceAgent *agent,
+ guint stream_id, guint component_id, guint len, gchar *buffer,
+ gpointer user_data);
+static void set_current_channel (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel);
+static void channel_disposed (gpointer data, GObject *where_the_object_was);
+
+static void
+gtalk_file_collection_init (GTalkFileCollection *self)
+{
+ GTalkFileCollectionPrivate *priv =
+ G_TYPE_INSTANCE_GET_PRIVATE (self, GTALK_TYPE_FILE_COLLECTION,
+ GTalkFileCollectionPrivate);
+ gchar buf[16];
+ guint32 *uint_buf = (guint32 *) buf;
+ guint i;
+
+
+ DEBUG ("GTalk file collection init called");
+ self->priv = priv;
+
+ self->priv->status = GTALK_FT_STATUS_PENDING;
+
+ self->priv->channels_reading = g_hash_table_new_full (NULL, NULL, NULL, NULL);
+ self->priv->channels_usable = g_hash_table_new_full (NULL, NULL, NULL, NULL);
+
+ self->priv->share_channels = g_hash_table_new_full (NULL, NULL,
+ NULL, free_share_channel);
+
+ for (i = 0; i < sizeof (buf); i++)
+ buf[i] = g_random_int_range (0, 256);
+
+ self->priv->token = g_strdup_printf ("%x%x%x%x",
+ uint_buf[0], uint_buf[1], uint_buf[2], uint_buf[3]);
+
+ /* FIXME: we should start creating a nice agent already and have it start
+ the candidate gathering.. but we don't know which jingle-share transport
+ channel name to assign it to... */
+
+ priv->dispose_has_run = FALSE;
+}
+
+
+static void
+gtalk_file_collection_dispose (GObject *object)
+{
+ GTalkFileCollection *self = GTALK_FILE_COLLECTION (object);
+ GList *i;
+
+ if (self->priv->dispose_has_run)
+ return;
+
+ DEBUG ("dispose called");
+ self->priv->dispose_has_run = TRUE;
+
+ if (self->priv->jingle != NULL)
+ {
+ gabble_jingle_session_terminate (self->priv->jingle,
+ TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL);
+
+ /* the terminate could synchronously unref it and set it to NULL */
+ if (self->priv->jingle != NULL)
+ {
+ g_object_unref (self->priv->jingle);
+ self->priv->jingle = NULL;
+ }
+ }
+
+ set_current_channel (self, NULL);
+
+ if (self->priv->channels_reading != NULL)
+ {
+ g_hash_table_destroy (self->priv->channels_reading);
+ self->priv->channels_reading = NULL;
+ }
+
+ if (self->priv->channels_usable != NULL)
+ {
+ g_hash_table_destroy (self->priv->channels_usable);
+ self->priv->channels_usable = NULL;
+ }
+
+ if (self->priv->share_channels != NULL)
+ {
+ g_hash_table_destroy (self->priv->share_channels);
+ self->priv->share_channels = NULL;
+ }
+
+ for (i = self->priv->channels; i; i = i->next)
+ {
+ GabbleFileTransferChannel *channel = i->data;
+ g_object_weak_unref (G_OBJECT (channel), channel_disposed, self);
+ }
+ g_list_free (self->priv->channels);
+
+ g_free (self->priv->token);
+
+ if (G_OBJECT_CLASS (gtalk_file_collection_parent_class)->dispose)
+ G_OBJECT_CLASS (gtalk_file_collection_parent_class)->dispose (object);
+}
+
+static void
+gtalk_file_collection_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GTalkFileCollection *self = GTALK_FILE_COLLECTION (object);
+
+ switch (property_id)
+ {
+ case PROP_TOKEN:
+ g_value_set_string (value, self->priv->token);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+
+static void
+gtalk_file_collection_class_init (GTalkFileCollectionClass *cls)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (cls);
+
+ g_type_class_add_private (cls, sizeof (GTalkFileCollectionPrivate));
+
+ object_class->get_property = gtalk_file_collection_get_property;
+ object_class->dispose = gtalk_file_collection_dispose;
+
+ g_object_class_install_property (object_class, PROP_TOKEN,
+ g_param_spec_string (
+ "token",
+ "Unique token identifiying the FileCollection",
+ "Token identifying a collection of files",
+ "",
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+}
+
+
+static ShareChannel *
+get_share_channel (GTalkFileCollection *self, NiceAgent *agent)
+{
+ GHashTableIter iter;
+ gpointer key, value;
+ ShareChannel *ret = NULL;
+
+ g_hash_table_iter_init (&iter, self->priv->share_channels);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ ShareChannel *share_channel = (ShareChannel *) value;
+ if (share_channel->agent == agent)
+ {
+ ret = share_channel;
+ break;
+ }
+ }
+
+ return ret;
+}
+
+
+static GabbleFileTransferChannel *
+get_channel_by_filename (GTalkFileCollection *self, gchar *filename)
+{
+ GList *i;
+
+ for (i = self->priv->channels; i; i = i->next)
+ {
+ GabbleFileTransferChannel *channel = i->data;
+ gchar *file = NULL;
+
+ g_object_get (channel,
+ "filename", &file,
+ NULL);
+
+ if (strcmp (file, filename) == 0)
+ return channel;
+ }
+
+ return NULL;
+}
+
+static void
+set_current_channel (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel)
+{
+ self->priv->current_channel = channel;
+
+ if (channel != NULL)
+ {
+ gboolean reading = FALSE;
+
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_OPEN, FALSE);
+ reading = GPOINTER_TO_INT (g_hash_table_lookup (
+ self->priv->channels_reading, channel));
+ gtalk_file_collection_block_reading (self, channel, !reading);
+ }
+}
+
+static gboolean
+channel_exists (GTalkFileCollection * self, GabbleFileTransferChannel *channel)
+{
+ GList *i;
+
+ for (i = self->priv->channels; i; i = i->next)
+ {
+ if (channel == i->data)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+add_channel (GTalkFileCollection * self, GabbleFileTransferChannel *channel)
+{
+ self->priv->channels = g_list_append (self->priv->channels, channel);
+ g_hash_table_replace (self->priv->channels_reading, channel,
+ GINT_TO_POINTER (FALSE));
+ g_object_weak_ref (G_OBJECT (channel), channel_disposed, self);
+}
+
+static void
+del_channel (GTalkFileCollection * self, GabbleFileTransferChannel *channel)
+{
+ g_return_if_fail (channel_exists (self, channel));
+
+ self->priv->channels = g_list_remove (self->priv->channels, channel);
+ g_hash_table_remove (self->priv->channels_reading, channel);
+ g_hash_table_remove (self->priv->channels_usable, channel);
+ g_object_weak_unref (G_OBJECT (channel), channel_disposed, self);
+ if (self->priv->current_channel == channel)
+ set_current_channel (self, NULL);
+}
+
+static void
+jingle_session_state_changed_cb (GabbleJingleSession *session,
+ GParamSpec *arg1,
+ GTalkFileCollection *self)
+{
+ JingleSessionState state;
+ GList *i;
+
+ DEBUG ("called");
+
+ g_object_get (session,
+ "state", &state,
+ NULL);
+
+ switch (state)
+ {
+ case JS_STATE_INVALID:
+ case JS_STATE_PENDING_CREATED:
+ break;
+ case JS_STATE_PENDING_INITIATE_SENT:
+ case JS_STATE_PENDING_INITIATED:
+ for (i = self->priv->channels; i;)
+ {
+ GabbleFileTransferChannel *channel = i->data;
+
+ i = i->next;
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_PENDING, FALSE);
+ }
+ break;
+ case JS_STATE_PENDING_ACCEPT_SENT:
+ case JS_STATE_ACTIVE:
+ /* Do not set the channels to OPEN unless we're ready to send/receive
+ data from them */
+ if (self->priv->status == GTALK_FT_STATUS_INITIATED)
+ self->priv->status = GTALK_FT_STATUS_ACCEPTED;
+ for (i = self->priv->channels; i;)
+ {
+ GabbleFileTransferChannel *channel = i->data;
+ gboolean usable;
+
+ i = i->next;
+
+ usable = GPOINTER_TO_INT (g_hash_table_lookup (
+ self->priv->channels_usable, channel));
+ if (usable)
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_ACCEPTED, FALSE);
+ }
+ break;
+ case JS_STATE_ENDED:
+ /* Do nothing, let the terminated signal set the correct state
+ depending on the termination reason */
+ default:
+ break;
+ }
+}
+
+static void
+jingle_session_terminated_cb (GabbleJingleSession *session,
+ gboolean local_terminator,
+ TpChannelGroupChangeReason reason,
+ const gchar *text,
+ gpointer user_data)
+{
+ GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data);
+ GList *i;
+
+ g_assert (session == self->priv->jingle);
+
+ self->priv->status = GTALK_FT_STATUS_TERMINATED;
+
+ for (i = self->priv->channels; i;)
+ {
+ GabbleFileTransferChannel *channel = i->data;
+
+ i = i->next;
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_TERMINATED, local_terminator);
+ }
+}
+
+static void
+content_new_remote_candidates_cb (GabbleJingleContent *content,
+ GList *clist, gpointer user_data)
+{
+ GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data);
+ GList *li;
+
+ DEBUG ("Got new remote candidates : %d", g_list_length (clist));
+
+ for (li = clist; li; li = li->next)
+ {
+ JingleCandidate *candidate = li->data;
+ NiceCandidate *cand = NULL;
+ ShareChannel *share_channel = NULL;
+ GSList *candidates = NULL;
+
+ if (candidate->protocol != JINGLE_TRANSPORT_PROTOCOL_UDP)
+ {
+ DEBUG ("Ignoring candidate %s because of non-UDP protocol : %d",
+ candidate->username, candidate->protocol);
+ continue;
+ }
+
+ share_channel = g_hash_table_lookup (self->priv->share_channels,
+ GINT_TO_POINTER (candidate->component));
+ if (share_channel == NULL)
+ {
+ DEBUG ("Ignoring candidate %s because of unknown component id %d",
+ candidate->id, candidate->component);
+ continue;
+ }
+
+ cand = nice_candidate_new (
+ candidate->type == JINGLE_CANDIDATE_TYPE_LOCAL?
+ NICE_CANDIDATE_TYPE_HOST:
+ candidate->type == JINGLE_CANDIDATE_TYPE_STUN?
+ NICE_CANDIDATE_TYPE_SERVER_REFLEXIVE:
+ NICE_CANDIDATE_TYPE_RELAYED);
+
+
+ cand->transport = JINGLE_TRANSPORT_PROTOCOL_UDP;
+ nice_address_init (&cand->addr);
+ nice_address_set_from_string (&cand->addr, candidate->address);
+ nice_address_set_port (&cand->addr, candidate->port);
+ cand->priority = candidate->preference * 1000;
+ cand->stream_id = share_channel->stream_id;
+ cand->component_id = share_channel->component_id;
+ /*
+ if (c->id == NULL)
+ candidate_id = g_strdup_printf ("R%d", ++priv->remote_candidate_count);
+ else
+ candidate_id = c->id;*/
+ if (candidate->id != NULL)
+ strncpy (cand->foundation, candidate->id,
+ NICE_CANDIDATE_MAX_FOUNDATION - 1);
+ else if (candidate->username != NULL)
+ strncpy (cand->foundation, candidate->username,
+ NICE_CANDIDATE_MAX_FOUNDATION - 1);
+
+ cand->username = g_strdup (candidate->username?candidate->username:"");
+ cand->password = g_strdup (candidate->password?candidate->password:"");
+
+ candidates = g_slist_append (candidates, cand);
+ nice_agent_set_remote_candidates (share_channel->agent,
+ share_channel->stream_id, share_channel->component_id, candidates);
+ g_slist_foreach (candidates, (GFunc)nice_candidate_free, NULL);
+ g_slist_free (candidates);
+ }
+}
+
+static void
+nice_candidate_gathering_done (NiceAgent *agent, guint stream_id,
+ gpointer user_data)
+{
+ GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data);
+ ShareChannel *share_channel = get_share_channel (self, agent);
+ GabbleJingleContent *content = GABBLE_JINGLE_CONTENT (share_channel->content);
+ GList *candidates = NULL;
+ GList *remote_candidates = NULL;
+ GSList *local_candidates;
+ GSList *li;
+
+ DEBUG ("libnice candidate gathering done!!!!");
+
+ /* Send remote candidates to libnice and listen to new signal */
+ remote_candidates = gabble_jingle_content_get_remote_candidates (content);
+ content_new_remote_candidates_cb (content, remote_candidates, self);
+
+ gabble_signal_connect_weak (content, "new-candidates",
+ (GCallback) content_new_remote_candidates_cb, G_OBJECT (self));
+
+ /* Send gathered local candidates to the content */
+ local_candidates = nice_agent_get_local_candidates (agent, stream_id,
+ share_channel->component_id);
+
+ for (li = local_candidates; li; li = li->next)
+ {
+ NiceCandidate *cand = li->data;
+ JingleCandidate *candidate;
+ gchar ip[NICE_ADDRESS_STRING_LEN];
+
+ nice_address_to_string (&cand->addr, ip);
+
+ candidate = jingle_candidate_new (
+ /* protocol */
+ cand->transport == NICE_CANDIDATE_TRANSPORT_UDP?
+ JINGLE_TRANSPORT_PROTOCOL_UDP:
+ JINGLE_TRANSPORT_PROTOCOL_TCP,
+ /* candidate type */
+ cand->type == NICE_CANDIDATE_TYPE_HOST?
+ JINGLE_CANDIDATE_TYPE_LOCAL:
+ cand->type == NICE_CANDIDATE_TYPE_RELAYED?
+ JINGLE_CANDIDATE_TYPE_RELAY:
+ JINGLE_CANDIDATE_TYPE_STUN,
+ /* id */
+ cand->foundation,
+ /* component */
+ share_channel->share_channel_id,
+ /* address */
+ ip,
+ /* port */
+ nice_address_get_port (&cand->addr),
+ /* generation */
+ 0,
+ /* preference */
+ (gfloat) cand->priority / 1000.0,
+ /* username */
+ cand->username?cand->username:"",
+ /* password */
+ cand->password?cand->password:"",
+ /* network */
+ 0);
+
+ candidates = g_list_prepend (candidates, candidate);
+ }
+
+ gabble_jingle_content_add_candidates (content, candidates);
+}
+
+static void
+nice_component_state_changed (NiceAgent *agent, guint stream_id,
+ guint component_id, guint state, gpointer user_data)
+{
+ GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data);
+ ShareChannel *share_channel = get_share_channel (self, agent);
+ GabbleJingleContent *content = GABBLE_JINGLE_CONTENT (share_channel->content);
+ JingleTransportState ts = JINGLE_TRANSPORT_STATE_DISCONNECTED;
+
+ DEBUG ("libnice component state changed %d!!!!", state);
+
+ switch (state)
+ {
+ case NICE_COMPONENT_STATE_DISCONNECTED:
+ case NICE_COMPONENT_STATE_GATHERING:
+ ts = JINGLE_TRANSPORT_STATE_DISCONNECTED;
+ break;
+ case NICE_COMPONENT_STATE_CONNECTING:
+ ts = JINGLE_TRANSPORT_STATE_CONNECTING;
+ break;
+ case NICE_COMPONENT_STATE_CONNECTED:
+ case NICE_COMPONENT_STATE_READY:
+ ts = JINGLE_TRANSPORT_STATE_CONNECTED;
+ break;
+ case NICE_COMPONENT_STATE_FAILED:
+ {
+ GList *i;
+
+ for (i = self->priv->channels; i;)
+ {
+ GabbleFileTransferChannel *channel = i->data;
+
+ i = i->next;
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_CONNECTION_FAILED,
+ TRUE);
+ }
+ /* return because we don't want to use the content after it
+ has been destroyed.. */
+ return;
+ }
+ }
+ gabble_jingle_content_set_transport_state (content, ts);
+}
+
+static void
+get_next_manifest_entry (GTalkFileCollection *self,
+ ShareChannel *share_channel, gboolean error)
+{
+ GabbleJingleShareManifest *manifest = NULL;
+ GabbleJingleShareManifestEntry *entry = NULL;
+ GabbleFileTransferChannel *channel = NULL;
+ GList *i;
+
+ DEBUG ("called");
+
+ if (self->priv->current_channel != NULL)
+ {
+ if (g_list_length (self->priv->channels) == 1)
+ {
+ GabbleJingleContent *content = \
+ GABBLE_JINGLE_CONTENT (share_channel->content);
+
+ DEBUG ("Received all the files. Transfer is complete");
+ gabble_jingle_content_send_complete (content);
+ }
+
+ g_hash_table_replace (self->priv->channels_usable,
+ self->priv->current_channel, GINT_TO_POINTER (FALSE));
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ self->priv->current_channel,
+ error ? GTALK_FILE_COLLECTION_STATE_ERROR:
+ GTALK_FILE_COLLECTION_STATE_COMPLETED, FALSE);
+
+ set_current_channel (self, NULL);
+ }
+
+ manifest = gabble_jingle_share_get_manifest (share_channel->content);
+ for (i = manifest->entries; i; i = i->next)
+ {
+ gchar *filename = NULL;
+ gboolean usable;
+
+ entry = i->data;
+
+ filename = g_strdup_printf ("%s%s", entry->name,
+ (entry->folder ? ".tar" : ""));
+ channel = get_channel_by_filename (self, filename);
+ g_free (filename);
+ if (channel != NULL)
+ {
+ usable = GPOINTER_TO_INT (g_hash_table_lookup (
+ self->priv->channels_usable, channel));
+ if (usable)
+ break;
+ }
+ entry = NULL;
+ }
+
+ self->priv->status = GTALK_FT_STATUS_WAITING;
+
+
+ if (entry != NULL)
+ {
+ gchar *buffer = NULL;
+ gchar *source_url = manifest->source_url;
+ guint url_len = (source_url != NULL? strlen (source_url) : 0);
+ gchar *separator = "";
+ gchar *filename = NULL;
+
+ if (source_url != NULL && source_url[url_len -1] != '/')
+ separator = "/";
+
+ self->priv->status = GTALK_FT_STATUS_TRANSFERRING;
+
+ filename = g_uri_escape_string (entry->name, NULL, TRUE);
+
+ /* The session initiator will always be the full JID of the peer */
+ buffer = g_strdup_printf ("GET %s%s%s HTTP/1.1\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Length: 0\r\n"
+ "Host: %s:0\r\n" /* e.g. alice@example.com/Empathy:0 */
+ "User-Agent: %s\r\n\r\n",
+ (source_url != NULL ? source_url : ""),
+ separator, filename,
+ gabble_jingle_session_get_initiator (self->priv->jingle),
+ PACKAGE_STRING);
+ g_free (filename);
+
+ /* FIXME: check for success */
+ nice_agent_send (share_channel->agent, share_channel->stream_id,
+ share_channel->component_id, strlen (buffer), buffer);
+ g_free (buffer);
+
+ share_channel->http_status = HTTP_CLIENT_RECEIVE;
+ /* Block or unblock accordingly */
+ set_current_channel (self, channel);
+ }
+}
+
+static void
+nice_component_writable (NiceAgent *agent, guint stream_id, guint component_id,
+ gpointer user_data)
+{
+ GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data);
+ ShareChannel *share_channel = get_share_channel (self, agent);
+
+ if (share_channel->http_status == HTTP_CLIENT_IDLE)
+ {
+ get_next_manifest_entry (self, share_channel, FALSE);
+ }
+ else if (share_channel->http_status == HTTP_SERVER_SEND)
+ {
+ if (self->priv->current_channel == NULL)
+ {
+ GList *i;
+
+ DEBUG ("Unexpected current_channel == NULL!");
+ for (i = self->priv->channels; i;)
+ {
+ GabbleFileTransferChannel *channel = i->data;
+
+ i = i->next;
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_ERROR, FALSE);
+ }
+ return;
+ }
+ gabble_file_transfer_channel_gtalk_file_collection_write_blocked (
+ self->priv->current_channel, FALSE);
+ if (share_channel->write_buffer != NULL)
+ {
+ gint ret = nice_agent_send (agent, stream_id, component_id,
+ share_channel->write_len, share_channel->write_buffer);
+
+ if (ret < 0 || (guint) ret < share_channel->write_len)
+ {
+ gchar *to_free = share_channel->write_buffer;
+
+ if (ret < 0)
+ ret = 0;
+
+ share_channel->write_buffer = g_memdup (
+ share_channel->write_buffer + ret,
+ share_channel->write_len - ret);
+ share_channel->write_len = share_channel->write_len - ret;
+ g_free (to_free);
+
+ gabble_file_transfer_channel_gtalk_file_collection_write_blocked (
+ self->priv->current_channel, TRUE);
+ }
+ else
+ {
+ g_free (share_channel->write_buffer);
+ share_channel->write_buffer = NULL;
+ share_channel->write_len = 0;
+ }
+ }
+ }
+
+}
+
+typedef struct
+{
+ union {
+ gpointer ptr;
+ GTalkFileCollection *self;
+ } u;
+ ShareChannel *share_channel;
+} GoogleRelaySessionData;
+
+static void
+set_relay_info (gpointer item, gpointer user_data)
+{
+ GoogleRelaySessionData *data = user_data;
+ GHashTable *relay = item;
+ const gchar *server_ip = NULL;
+ const gchar *username = NULL;
+ const gchar *password = NULL;
+ const gchar *type_str = NULL;
+ guint server_port;
+ NiceRelayType type;
+ GValue *value;
+
+ value = g_hash_table_lookup (relay, "ip");
+ if (value != NULL)
+ server_ip = g_value_get_string (value);
+ else
+ return;
+
+ value = g_hash_table_lookup (relay, "port");
+ if (value != NULL)
+ server_port = g_value_get_uint (value);
+ else
+ return;
+
+ value = g_hash_table_lookup (relay, "username");
+ if (value != NULL)
+ username = g_value_get_string (value);
+ else
+ return;
+
+ value = g_hash_table_lookup (relay, "password");
+ if (value != NULL)
+ password = g_value_get_string (value);
+ else
+ return;
+
+ value = g_hash_table_lookup (relay, "type");
+ if (value != NULL)
+ type_str = g_value_get_string (value);
+ else
+ return;
+
+ if (!strcmp (type_str, "udp"))
+ type = NICE_RELAY_TYPE_TURN_UDP;
+ else if (!strcmp (type_str, "tcp"))
+ type = NICE_RELAY_TYPE_TURN_TCP;
+ else if (!strcmp (type_str, "tls"))
+ type = NICE_RELAY_TYPE_TURN_TLS;
+ else
+ return;
+
+ nice_agent_set_relay_info (data->share_channel->agent,
+ data->share_channel->stream_id, data->share_channel->component_id,
+ server_ip, server_port,
+ username, password, type);
+
+}
+
+static void
+google_relay_session_cb (GPtrArray *relays, gpointer user_data)
+{
+ GoogleRelaySessionData *data = user_data;
+
+ if (data->u.self == NULL)
+ {
+ DEBUG ("Received relay session callback but self got destroyed");
+ g_slice_free (GoogleRelaySessionData, data);
+ return;
+ }
+
+ if (relays != NULL)
+ g_ptr_array_foreach (relays, set_relay_info, user_data);
+
+ nice_agent_gather_candidates (data->share_channel->agent,
+ data->share_channel->stream_id);
+
+ g_object_remove_weak_pointer (G_OBJECT (data->u.self), &data->u.ptr);
+ g_slice_free (GoogleRelaySessionData, data);
+}
+
+
+static void
+content_new_share_channel_cb (GabbleJingleContent *content, const gchar *name,
+ guint share_channel_id, gpointer user_data)
+{
+ GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data);
+ ShareChannel *share_channel = g_slice_new0 (ShareChannel);
+ NiceAgent *agent = nice_agent_new_reliable (g_main_context_default (),
+ NICE_COMPATIBILITY_GOOGLE);
+ guint stream_id = nice_agent_add_stream (agent, 1);
+ gchar *stun_server;
+ guint stun_port;
+ GoogleRelaySessionData *relay_data = NULL;
+
+ DEBUG ("New Share channel %s was created and linked to id %d", name,
+ share_channel_id);
+
+ share_channel->agent = agent;
+ share_channel->stream_id = stream_id;
+ share_channel->component_id = NICE_COMPONENT_TYPE_RTP;
+ share_channel->content = GABBLE_JINGLE_SHARE (content);
+ share_channel->share_channel_id = share_channel_id;
+
+ if (self->priv->requested)
+ share_channel->http_status = HTTP_SERVER_IDLE;
+ else
+ share_channel->http_status = HTTP_CLIENT_IDLE;
+
+ gabble_signal_connect_weak (agent, "candidate-gathering-done",
+ G_CALLBACK (nice_candidate_gathering_done), G_OBJECT (self));
+
+ gabble_signal_connect_weak (agent, "component-state-changed",
+ G_CALLBACK (nice_component_state_changed), G_OBJECT (self));
+
+ gabble_signal_connect_weak (agent, "reliable-transport-writable",
+ G_CALLBACK (nice_component_writable), G_OBJECT (self));
+
+
+ /* Add the agent to the hash table before gathering candidates in case the
+ gathering finishes synchronously, and the callback tries to add local
+ candidates to the content, it needs to find the share channel id.. */
+ g_hash_table_insert (self->priv->share_channels,
+ GINT_TO_POINTER (share_channel_id), share_channel);
+
+ share_channel->agent_attached = TRUE;
+ nice_agent_attach_recv (agent, stream_id, share_channel->component_id,
+ g_main_context_default (), nice_data_received_cb, self);
+
+ if (gabble_jingle_factory_get_stun_server (
+ self->priv->jingle_factory, &stun_server, &stun_port))
+ {
+ g_object_set (agent,
+ "stun-server", stun_server,
+ "stun-server-port", stun_port,
+ NULL);
+ g_free (stun_server);
+ }
+
+ relay_data = g_slice_new0 (GoogleRelaySessionData);
+ relay_data->u.self = self;
+ relay_data->share_channel = share_channel;
+ g_object_add_weak_pointer (G_OBJECT (relay_data->u.self),
+ &relay_data->u.ptr);
+ gabble_jingle_factory_create_google_relay_session (
+ self->priv->jingle_factory, 1,
+ google_relay_session_cb, relay_data);
+}
+
+static void
+content_completed (GabbleJingleContent *content, gpointer user_data)
+{
+ GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data);
+ GList *i;
+
+ DEBUG ("Received content completed");
+
+ for (i = self->priv->channels; i;)
+ {
+ GabbleFileTransferChannel *channel = i->data;
+
+ i = i->next;
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_COMPLETED, FALSE);
+ }
+}
+
+static void
+free_share_channel (gpointer data)
+{
+ ShareChannel *share_channel = (ShareChannel *) data;
+
+ DEBUG ("Freeing jingle Share channel");
+
+ if (share_channel->write_buffer != NULL)
+ {
+ g_free (share_channel->write_buffer);
+ share_channel->write_buffer = NULL;
+ }
+ if (share_channel->read_buffer != NULL)
+ {
+ g_free (share_channel->read_buffer);
+ share_channel->read_buffer = NULL;
+ }
+ g_object_unref (share_channel->agent);
+ g_slice_free (ShareChannel, share_channel);
+}
+
+
+/* If buffer contains a line ending, 0-terminate the first line and
+ * return a pointer to the beginning of the next line. Otherwise
+ * return NULL. */
+static gchar *
+http_read_line (gchar *buffer, guint len)
+{
+ gchar *p = memchr (buffer, '\n', len);
+
+ if (p != NULL)
+ {
+ *p = 0;
+ if (p > buffer && *(p-1) == '\r')
+ *(p-1) = '\0';
+ p++;
+ }
+
+ return p;
+}
+
+static guint
+http_data_received (GTalkFileCollection *self, ShareChannel *share_channel,
+ gchar *buffer, guint len)
+{
+
+ switch (share_channel->http_status)
+ {
+ case HTTP_SERVER_IDLE:
+ {
+ gchar *headers = http_read_line (buffer, len);
+
+ if (headers == NULL)
+ return 0;
+
+ share_channel->http_status = HTTP_SERVER_HEADERS;
+ share_channel->status_line = g_strdup (buffer);
+
+ if (self->priv->current_channel != NULL)
+ {
+ DEBUG ("Received status line with current channel set");
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ self->priv->current_channel,
+ GTALK_FILE_COLLECTION_STATE_COMPLETED, FALSE);
+ set_current_channel (self, NULL);
+ }
+
+ return headers - buffer;
+ }
+ break;
+ case HTTP_SERVER_HEADERS:
+ {
+ gchar *line = buffer;
+ gchar *next_line = http_read_line (buffer, len);
+
+ if (next_line == NULL)
+ return 0;
+
+ DEBUG ("Found server headers line (%" G_GSIZE_FORMAT ") : %s",
+ strlen (line), line);
+ /* FIXME: how about content-length and an actual body ? */
+ if (line[0] == '\0')
+ {
+ gchar *response = NULL;
+ gchar *get_line = NULL;
+ GabbleJingleShareManifest *manifest = NULL;
+ gchar *source_url = NULL;
+ guint url_len;
+ gchar *separator = "";
+ gchar *filename = NULL;
+ GabbleFileTransferChannel *channel = NULL;
+
+ g_assert (self->priv->current_channel == NULL);
+
+ DEBUG ("Found empty line, received request : %s ",
+ share_channel->status_line);
+
+ manifest = gabble_jingle_share_get_manifest (
+ share_channel->content);
+ source_url = manifest->source_url;
+ url_len = (source_url != NULL? strlen (source_url) : 0);
+ if (source_url != NULL && source_url[url_len -1] != '/')
+ separator = "/";
+
+ get_line = g_strdup_printf ("GET %s%s%%s HTTP/1.1",
+ (source_url != NULL ? source_url : ""),
+ separator);
+ filename = g_malloc (strlen (share_channel->status_line));
+
+ if (sscanf (share_channel->status_line, get_line, filename) == 1)
+ {
+ gchar *unescaped = g_uri_unescape_string (filename, NULL);
+
+ g_free (filename);
+ filename = unescaped;
+ channel = get_channel_by_filename (self, filename);
+ }
+
+ if (channel != NULL)
+ {
+ guint64 size;
+
+ g_object_get (channel,
+ "size", &size,
+ NULL);
+
+ DEBUG ("Found valid filename, result : 200");
+
+ share_channel->http_status = HTTP_SERVER_SEND;
+ response = g_strdup_printf ("HTTP/1.1 200\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Length: %" G_GUINT64_FORMAT "\r\n"
+ "Content-Type: application/octet-stream\r\n\r\n",
+ size);
+
+ }
+ else
+ {
+ DEBUG ("Unable to find valid filename (%s), result : 404",
+ (filename != NULL? filename : ""));
+
+ share_channel->http_status = HTTP_SERVER_IDLE;
+ response = g_strdup_printf ("HTTP/1.1 404\r\n"
+ "Connection: Keep-Alive\r\n"
+ "Content-Length: 0\r\n\r\n");
+ }
+
+ /* FIXME: check for success of nice_agent_send */
+ nice_agent_send (share_channel->agent, share_channel->stream_id,
+ share_channel->component_id, strlen (response), response);
+
+ g_free (response);
+ g_free (filename);
+ g_free (get_line);
+
+ /* Now that we sent our response, we can assign the current
+ channel which sets it to OPEN (if non NULL) so data can
+ start flowing */
+ self->priv->status = GTALK_FT_STATUS_TRANSFERRING;
+ set_current_channel (self, channel);
+ }
+
+ return next_line - buffer;
+ }
+ break;
+ case HTTP_SERVER_SEND:
+ DEBUG ("received data when we're supposed to be sending data.. "
+ "not supposed to happen");
+ break;
+ case HTTP_CLIENT_IDLE:
+ DEBUG ("received data when we're supposed to be sending the GET.. "
+ "not supposed to happen");
+ break;
+ case HTTP_CLIENT_RECEIVE:
+ {
+ gchar *headers = http_read_line (buffer, len);
+
+ if (headers == NULL)
+ return 0;
+
+ share_channel->http_status = HTTP_CLIENT_HEADERS;
+ share_channel->status_line = g_strdup (buffer);
+
+ return headers - buffer;
+ }
+ case HTTP_CLIENT_HEADERS:
+ {
+ gchar *line = buffer;
+ gchar *next_line = http_read_line (buffer, len);
+
+ if (next_line == NULL)
+ return 0;
+
+ DEBUG ("Found client headers line (%" G_GSIZE_FORMAT ") : %s",
+ strlen (line), line);
+ if (line[0] == '\0')
+ {
+ DEBUG ("Found empty line, GET response : %s",
+ share_channel->status_line);
+
+ if (g_str_has_prefix (share_channel->status_line,
+ "HTTP/1.1 200"))
+ {
+ if (share_channel->is_chunked)
+ {
+ share_channel->http_status = HTTP_CLIENT_CHUNK_SIZE;
+ }
+ else
+ {
+ share_channel->http_status = HTTP_CLIENT_BODY;
+ if (share_channel->content_length == 0)
+ get_next_manifest_entry (self, share_channel, FALSE);
+ }
+ }
+ else
+ {
+ /* We expect content-length to be 0 and no chunks for
+ non-200 statuses (404 error) */
+ if (share_channel->is_chunked ||
+ share_channel->content_length != 0)
+ {
+ GList *i;
+
+ DEBUG ("Unexpected body for non-200 error!");
+ for (i = self->priv->channels; i;)
+ {
+ GabbleFileTransferChannel *channel = i->data;
+
+ i = i->next;
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_ERROR, FALSE);
+ }
+ }
+ else
+ {
+ get_next_manifest_entry (self, share_channel, TRUE);
+ }
+ }
+ }
+ else if (!g_ascii_strncasecmp (line, "Content-Length: ", 16))
+ {
+ share_channel->is_chunked = FALSE;
+ /* Check strtoull read all the length */
+ share_channel->content_length = g_ascii_strtoull (line + 16,
+ NULL, 10);
+ DEBUG ("Found data length : %" G_GUINT64_FORMAT,
+ share_channel->content_length);
+ }
+ else if (!g_ascii_strcasecmp (line,
+ "Transfer-Encoding: chunked"))
+ {
+ share_channel->is_chunked = TRUE;
+ share_channel->content_length = 0;
+ DEBUG ("Found file is chunked");
+ }
+
+ return next_line - buffer;
+ }
+ break;
+ case HTTP_CLIENT_CHUNK_SIZE:
+ {
+ gchar *line = buffer;
+ gchar *next_line = http_read_line (buffer, len);
+
+ if (next_line == NULL)
+ return 0;
+
+ /* FIXME : check validity of strtoul */
+ share_channel->content_length = strtoul (line, NULL, 16);
+ if (share_channel->content_length > 0)
+ share_channel->http_status = HTTP_CLIENT_BODY;
+ else
+ share_channel->http_status = HTTP_CLIENT_CHUNK_FINAL;
+
+
+ return next_line - buffer;
+ }
+ break;
+ case HTTP_CLIENT_BODY:
+ {
+ guint consumed = 0;
+
+ if (len >= share_channel->content_length)
+ {
+ if (self->priv->current_channel == NULL)
+ {
+ GList *i;
+
+ DEBUG ("Unexpected current_channel == NULL!");
+ for (i = self->priv->channels; i;)
+ {
+ GabbleFileTransferChannel *channel = i->data;
+
+ i = i->next;
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_ERROR, FALSE);
+ }
+ /* FIXME: Who knows what might happen here if we got destroyed
+ It shouldn't crash since our object isn't dereferences
+ anymore, but.. */
+ return len;
+ }
+ consumed = share_channel->content_length;
+ gabble_file_transfer_channel_gtalk_file_collection_data_received (
+ self->priv->current_channel, buffer, consumed);
+ share_channel->content_length = 0;
+ if (share_channel->is_chunked)
+ share_channel->http_status = HTTP_CLIENT_CHUNK_END;
+ else
+ get_next_manifest_entry (self, share_channel, FALSE);
+ }
+ else
+ {
+ consumed = len;
+ share_channel->content_length -= len;
+ gabble_file_transfer_channel_gtalk_file_collection_data_received (
+ self->priv->current_channel, buffer, consumed);
+ }
+
+ return consumed;
+ }
+ break;
+ case HTTP_CLIENT_CHUNK_END:
+ {
+ gchar *chunk = http_read_line (buffer, len);
+
+ if (chunk == NULL)
+ return 0;
+
+ share_channel->http_status = HTTP_CLIENT_CHUNK_SIZE;
+
+ return chunk - buffer;
+ }
+ break;
+ case HTTP_CLIENT_CHUNK_FINAL:
+ {
+ gchar *end = http_read_line (buffer, len);
+
+ if (end == NULL)
+ return 0;
+
+ share_channel->http_status = HTTP_CLIENT_IDLE;
+ get_next_manifest_entry (self, share_channel, FALSE);
+
+ return end - buffer;
+ }
+ break;
+ }
+
+ return 0;
+}
+
+static void
+nice_data_received_cb (NiceAgent *agent,
+ guint stream_id,
+ guint component_id,
+ guint len,
+ gchar *buffer,
+ gpointer user_data)
+{
+ GTalkFileCollection *self = GTALK_FILE_COLLECTION (user_data);
+ ShareChannel *share_channel = get_share_channel (self, agent);
+ gchar *free_buffer = NULL;
+
+ if (share_channel->read_buffer != NULL)
+ {
+ gchar *tmp = g_malloc (share_channel->read_len + len);
+
+ memcpy (tmp, share_channel->read_buffer, share_channel->read_len);
+ memcpy (tmp + share_channel->read_len, buffer, len);
+
+ free_buffer = buffer = tmp;
+ len += share_channel->read_len;
+
+ g_free (share_channel->read_buffer);
+ share_channel->read_buffer = NULL;
+ share_channel->read_len = 0;
+ }
+ while (len > 0)
+ {
+ guint consumed = http_data_received (self, share_channel, buffer, len);
+
+ if (consumed == 0)
+ {
+ share_channel->read_buffer = g_memdup (buffer, len);
+ share_channel->read_len = len;
+ break;
+ }
+ else
+ {
+ /* we assume http_data_received never returns consumed > len */
+ g_assert (consumed <= len);
+
+ len -= consumed;
+ buffer += consumed;
+ }
+ }
+
+ if (free_buffer != NULL)
+ g_free (free_buffer);
+
+}
+
+static void
+set_session (GTalkFileCollection * self,
+ GabbleJingleSession *session, GabbleJingleContent *content)
+{
+ self->priv->jingle = g_object_ref (session);
+
+ gabble_signal_connect_weak (session, "notify::state",
+ (GCallback) jingle_session_state_changed_cb, G_OBJECT (self));
+ gabble_signal_connect_weak (session, "terminated",
+ (GCallback) jingle_session_terminated_cb, G_OBJECT (self));
+
+ gabble_signal_connect_weak (content, "new-share-channel",
+ (GCallback) content_new_share_channel_cb, G_OBJECT (self));
+ gabble_signal_connect_weak (content, "completed",
+ (GCallback) content_completed, G_OBJECT (self));
+
+ self->priv->status = GTALK_FT_STATUS_PENDING;
+}
+
+GTalkFileCollection *
+gtalk_file_collection_new (GabbleFileTransferChannel *channel,
+ GabbleJingleFactory *jingle_factory, TpHandle handle, const gchar *jid)
+{
+ GTalkFileCollection * self = g_object_new (GTALK_TYPE_FILE_COLLECTION, NULL);
+ GabbleJingleSession *session = NULL;
+ GabbleJingleContent *content = NULL;
+ gchar *filename;
+ guint64 size;
+
+ self->priv->jingle_factory = jingle_factory;
+ self->priv->requested = TRUE;
+
+ session = gabble_jingle_factory_create_session (jingle_factory,
+ handle, jid, FALSE);
+
+ if (session == NULL)
+ {
+ g_object_unref (self);
+ return NULL;
+ }
+
+ g_object_set (session,
+ "dialect", JINGLE_DIALECT_GTALK4,
+ NULL);
+
+ content = gabble_jingle_session_add_content (session,
+ JINGLE_MEDIA_TYPE_NONE, "share", NS_GOOGLE_SESSION_SHARE,
+ NS_GOOGLE_TRANSPORT_P2P);
+
+ if (content == NULL)
+ {
+ g_object_unref (self);
+ g_object_unref (session);
+ return NULL;
+ }
+
+ g_object_get (channel,
+ "filename", &filename,
+ "size", &size,
+ NULL);
+ g_object_set (content,
+ "filename", filename,
+ "filesize", size,
+ NULL);
+
+ set_session (self, session, content);
+
+ add_channel (self, channel);
+
+
+ return self;
+}
+
+GTalkFileCollection *
+gtalk_file_collection_new_from_session (GabbleJingleFactory *jingle_factory,
+ GabbleJingleSession *session)
+{
+ GTalkFileCollection * self = NULL;
+ GabbleJingleContent *content = NULL;
+ GList *cs;
+
+ if (gabble_jingle_session_get_content_type (session) !=
+ GABBLE_TYPE_JINGLE_SHARE)
+ return NULL;
+
+ cs = gabble_jingle_session_get_contents (session);
+
+ if (cs != NULL)
+ {
+ content = GABBLE_JINGLE_CONTENT (cs->data);
+ g_list_free (cs);
+ }
+
+ if (content == NULL)
+ return NULL;
+
+ self = g_object_new (GTALK_TYPE_FILE_COLLECTION, NULL);
+
+ self->priv->jingle_factory = jingle_factory;
+ self->priv->requested = FALSE;
+
+ set_session (self, session, content);
+
+ return self;
+}
+
+void
+gtalk_file_collection_add_channel (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel)
+{
+ add_channel (self, channel);
+}
+
+void
+gtalk_file_collection_initiate (GTalkFileCollection *self,
+ GabbleFileTransferChannel * channel)
+{
+ if (channel_exists (self, channel))
+ {
+ g_hash_table_replace (self->priv->channels_reading, channel,
+ GINT_TO_POINTER (TRUE));
+ g_hash_table_replace (self->priv->channels_usable, channel,
+ GINT_TO_POINTER (TRUE));
+ }
+
+ if (self->priv->status == GTALK_FT_STATUS_PENDING)
+ {
+ gabble_jingle_session_accept (self->priv->jingle);
+ self->priv->status = GTALK_FT_STATUS_INITIATED;
+ }
+ else
+ {
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_ACCEPTED, FALSE);
+ }
+
+}
+
+void
+gtalk_file_collection_accept (GTalkFileCollection *self,
+ GabbleFileTransferChannel * channel)
+{
+ GList *cs = gabble_jingle_session_get_contents (self->priv->jingle);
+
+ DEBUG ("called");
+
+ if (channel_exists (self, channel))
+ {
+ g_hash_table_replace (self->priv->channels_usable, channel,
+ GINT_TO_POINTER (TRUE));
+ }
+
+ if (self->priv->status == GTALK_FT_STATUS_PENDING)
+ {
+ if (cs != NULL)
+ {
+ GabbleJingleContent *content = GABBLE_JINGLE_CONTENT (cs->data);
+ guint initial_id = 0;
+ guint share_channel_id;
+
+ gabble_jingle_session_accept (self->priv->jingle);
+ self->priv->status = GTALK_FT_STATUS_ACCEPTED;
+
+ /* The new-share-channel signal will take care of the rest.. */
+ do
+ {
+ gchar *share_channel_name = NULL;
+
+ share_channel_name = g_strdup_printf ("gabble-%d", ++initial_id);
+ share_channel_id = gabble_jingle_content_create_share_channel (
+ content, share_channel_name);
+ g_free (share_channel_name);
+ } while (share_channel_id == 0 && initial_id < 10);
+
+ /* FIXME: not assert but actually cancel the FT? */
+ g_assert (share_channel_id > 0);
+ g_list_free (cs);
+ }
+
+ }
+ else
+ {
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (
+ channel, GTALK_FILE_COLLECTION_STATE_ACCEPTED, FALSE);
+ }
+
+ if (self->priv->status == GTALK_FT_STATUS_WAITING)
+ {
+ /* FIXME: this and other lookups should not check for channel '1' */
+ ShareChannel *share_channel = g_hash_table_lookup (
+ self->priv->share_channels, GINT_TO_POINTER (1));
+
+ get_next_manifest_entry (self, share_channel, FALSE);
+ }
+}
+
+gboolean
+gtalk_file_collection_send_data (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel, const gchar *data, guint length)
+{
+
+ ShareChannel *share_channel = g_hash_table_lookup (self->priv->share_channels,
+ GINT_TO_POINTER (1));
+ gint ret;
+
+
+ g_return_val_if_fail (self->priv->current_channel == channel, FALSE);
+
+ ret = nice_agent_send (share_channel->agent, share_channel->stream_id,
+ share_channel->component_id, length, data);
+
+ if (ret < 0 || (guint) ret < length)
+ {
+ if (ret < 0)
+ ret = 0;
+
+ share_channel->write_buffer = g_memdup (data + ret,
+ length - ret);
+ share_channel->write_len = length - ret;
+
+ gabble_file_transfer_channel_gtalk_file_collection_write_blocked (channel,
+ TRUE);
+ }
+ return TRUE;
+}
+
+void
+gtalk_file_collection_block_reading (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel, gboolean block)
+{
+ ShareChannel *share_channel = g_hash_table_lookup (self->priv->share_channels,
+ GINT_TO_POINTER (1));
+
+ g_assert (channel_exists (self, channel));
+
+ if (self->priv->status != GTALK_FT_STATUS_TRANSFERRING)
+ DEBUG ("Channel %p %s reading ", channel, block?"blocks":"unblocks" );
+
+ g_hash_table_replace (self->priv->channels_reading, channel,
+ GINT_TO_POINTER (!block));
+
+ if (channel == self->priv->current_channel)
+ {
+ if (block)
+ {
+ if (share_channel && share_channel->agent_attached)
+ {
+ nice_agent_attach_recv (share_channel->agent,
+ share_channel->stream_id, share_channel->component_id,
+ NULL, NULL, NULL);
+ share_channel->agent_attached = FALSE;
+ }
+ }
+ else
+ {
+ if (share_channel && !share_channel->agent_attached)
+ {
+ share_channel->agent_attached = TRUE;
+ nice_agent_attach_recv (share_channel->agent,
+ share_channel->stream_id, share_channel->component_id,
+ g_main_context_default (), nice_data_received_cb, self);
+ }
+ }
+ }
+}
+
+void
+gtalk_file_collection_completed (GTalkFileCollection *self,
+ GabbleFileTransferChannel * channel)
+{
+ ShareChannel *share_channel = g_hash_table_lookup (self->priv->share_channels,
+ GINT_TO_POINTER (1));
+
+ DEBUG ("called");
+
+ g_return_if_fail (self->priv->current_channel == channel);
+
+ /* We shouldn't set the FT to completed until we receive the 'complete' info
+ or we receive a new HTTP request otherwise we might terminate the session
+ and cause a race condition where the peer thinks it got canceled before it
+ completed. */
+ share_channel->http_status = HTTP_SERVER_IDLE;
+ self->priv->status = GTALK_FT_STATUS_WAITING;
+}
+
+void
+gtalk_file_collection_terminate (GTalkFileCollection *self,
+ GabbleFileTransferChannel * channel)
+{
+
+ DEBUG ("called");
+
+ if (!channel_exists (self, channel))
+ return;
+
+ if (self->priv->current_channel == channel)
+ {
+
+ del_channel (self, channel);
+
+ /* Cancel the whole thing if we terminate the current channel */
+ if (self->priv->status == GTALK_FT_STATUS_TRANSFERRING)
+ {
+
+ /* The terminate should call our terminated_cb callback which should
+ terminate all channels which should unref us which will unref the
+ jingle session */
+ self->priv->status = GTALK_FT_STATUS_TERMINATED;
+ gabble_jingle_session_terminate (self->priv->jingle,
+ TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL);
+ return;
+ }
+ return;
+ }
+ else
+ {
+ del_channel (self, channel);
+
+ /* If this was the last channel, it will cause it to unref us and
+ the dispose will be called, which will call
+ gabble_jingle_session_terminate */
+ gabble_file_transfer_channel_gtalk_file_collection_state_changed (channel,
+ GTALK_FILE_COLLECTION_STATE_TERMINATED, TRUE);
+ }
+}
+
+
+static void
+channel_disposed (gpointer data, GObject *object)
+{
+ GTalkFileCollection *self = data;
+ GabbleFileTransferChannel *channel = (GabbleFileTransferChannel *) object;
+
+ DEBUG ("channel %p got destroyed", channel);
+
+ g_return_if_fail (channel_exists (self, channel));
+
+ if (self->priv->current_channel == channel)
+ {
+ del_channel (self, channel);
+
+ /* Cancel the whole thing if we terminate the current channel */
+ if (self->priv->status == GTALK_FT_STATUS_TRANSFERRING)
+ {
+
+ /* The terminate should call our terminated_cb callback which should
+ terminate all channels which should unref us which will unref the
+ jingle session */
+ self->priv->status = GTALK_FT_STATUS_TERMINATED;
+ gabble_jingle_session_terminate (self->priv->jingle,
+ TP_CHANNEL_GROUP_CHANGE_REASON_NONE, NULL, NULL);
+ return;
+ }
+ }
+ else
+ {
+ del_channel (self, channel);
+ }
+}
diff --git a/src/gtalk-file-collection.h b/src/gtalk-file-collection.h
new file mode 100644
index 000000000..cd0e61594
--- /dev/null
+++ b/src/gtalk-file-collection.h
@@ -0,0 +1,100 @@
+/*
+ * gtalk-file-collection.h - Header for GTalkFileCollection
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * 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 __GTALK_FILE_COLLECTION_H__
+#define __GTALK_FILE_COLLECTION_H__
+
+#include <glib-object.h>
+#include "jingle-session.h"
+#include "connection.h"
+
+typedef struct _GTalkFileCollection GTalkFileCollection;
+
+typedef enum {
+ GTALK_FILE_COLLECTION_STATE_PENDING,
+ GTALK_FILE_COLLECTION_STATE_ACCEPTED,
+ GTALK_FILE_COLLECTION_STATE_OPEN,
+ GTALK_FILE_COLLECTION_STATE_TERMINATED,
+ GTALK_FILE_COLLECTION_STATE_CONNECTION_FAILED,
+ GTALK_FILE_COLLECTION_STATE_ERROR,
+ GTALK_FILE_COLLECTION_STATE_COMPLETED
+} GTalkFileCollectionState;
+
+#include "ft-channel.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GTalkFileCollectionClass GTalkFileCollectionClass;
+
+GType gtalk_file_collection_get_type (void);
+
+/* TYPE MACROS */
+#define GTALK_TYPE_FILE_COLLECTION \
+ (gtalk_file_collection_get_type ())
+#define GTALK_FILE_COLLECTION(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj), GTALK_TYPE_FILE_COLLECTION, \
+ GTalkFileCollection))
+#define GTALK_FILE_COLLECTION_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST((klass), GTALK_TYPE_FILE_COLLECTION, \
+ GTalkFileCollectionClass))
+#define GTALK_IS_FILE_COLLECTION(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE((obj), GTALK_TYPE_FILE_COLLECTION))
+#define GTALK_IS_FILE_COLLECTION_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE((klass), GTALK_TYPE_FILE_COLLECTION))
+#define GTALK_FILE_COLLECTION_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS ((obj), GTALK_TYPE_FILE_COLLECTION, \
+ GTalkFileCollectionClass))
+
+struct _GTalkFileCollectionClass {
+ GObjectClass parent_class;
+};
+
+typedef struct _GTalkFileCollectionPrivate GTalkFileCollectionPrivate;
+
+struct _GTalkFileCollection {
+ GObject parent;
+ GTalkFileCollectionPrivate *priv;
+};
+
+GTalkFileCollection *gtalk_file_collection_new (
+ GabbleFileTransferChannel *channel, GabbleJingleFactory *jingle_factory,
+ TpHandle handle, const gchar *jid);
+
+GTalkFileCollection *gtalk_file_collection_new_from_session (
+ GabbleJingleFactory *jingle_factory, GabbleJingleSession *session);
+
+void gtalk_file_collection_add_channel (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel);
+
+void gtalk_file_collection_initiate (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel);
+void gtalk_file_collection_accept (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel);
+void gtalk_file_collection_terminate (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel);
+void gtalk_file_collection_completed (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel);
+void gtalk_file_collection_block_reading (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel, gboolean block);
+gboolean gtalk_file_collection_send_data (GTalkFileCollection *self,
+ GabbleFileTransferChannel *channel, const gchar *data, guint length);
+
+
+#endif /* __GTALK_FILE_COLLECTION_H__ */
+
diff --git a/src/jingle-content.c b/src/jingle-content.c
index f43a112f1..8219dcc97 100644
--- a/src/jingle-content.c
+++ b/src/jingle-content.c
@@ -33,8 +33,11 @@
#include "jingle-factory.h"
#include "jingle-session.h"
#include "jingle-transport-iface.h"
+#include "jingle-transport-google.h"
+#include "jingle-media-rtp.h"
#include "namespaces.h"
#include "util.h"
+#include "gabble-signals-marshal.h"
/* signal enum */
enum
@@ -42,6 +45,8 @@ enum
READY,
NEW_CANDIDATES,
REMOVED,
+ NEW_SHARE_CHANNEL,
+ COMPLETED,
LAST_SIGNAL
};
@@ -83,6 +88,7 @@ struct _GabbleJingleContentPrivate
gboolean have_local_candidates;
guint gtalk4_event_id;
+ guint last_share_channel_component_id;
gboolean dispose_has_run;
};
@@ -96,6 +102,7 @@ G_DEFINE_TYPE(GabbleJingleContent, gabble_jingle_content, G_TYPE_OBJECT);
static void new_transport_candidates_cb (GabbleJingleTransportIface *trans,
GList *candidates, GabbleJingleContent *content);
static void _maybe_ready (GabbleJingleContent *self);
+static void transport_created (GabbleJingleContent *c);
static void
gabble_jingle_content_init (GabbleJingleContent *obj)
@@ -237,6 +244,8 @@ gabble_jingle_content_set_property (GObject *object,
g_signal_connect (priv->transport, "new-candidates",
(GCallback) new_transport_candidates_cb, self);
+
+ transport_created (self);
}
break;
case PROP_NAME:
@@ -261,6 +270,13 @@ gabble_jingle_content_set_property (GObject *object,
}
}
+static JingleContentSenders
+get_default_senders_real (GabbleJingleContent *c)
+{
+ return JINGLE_CONTENT_SENDERS_BOTH;
+}
+
+
static void
gabble_jingle_content_class_init (GabbleJingleContentClass *cls)
{
@@ -273,6 +289,8 @@ gabble_jingle_content_class_init (GabbleJingleContentClass *cls)
object_class->set_property = gabble_jingle_content_set_property;
object_class->dispose = gabble_jingle_content_dispose;
+ cls->get_default_senders = get_default_senders_real;
+
/* property definitions */
param_spec = g_param_spec_object ("connection", "GabbleConnection object",
"Gabble connection object used for exchanging messages.",
@@ -346,6 +364,27 @@ gabble_jingle_content_class_init (GabbleJingleContentClass *cls)
NULL, NULL,
g_cclosure_marshal_VOID__POINTER, G_TYPE_NONE, 1, G_TYPE_POINTER);
+ signals[NEW_SHARE_CHANNEL] = g_signal_new (
+ "new-share-channel",
+ G_TYPE_FROM_CLASS (cls),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ gabble_marshal_VOID__STRING_UINT,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_STRING, G_TYPE_UINT);
+
+ signals[COMPLETED] = g_signal_new (
+ "completed",
+ G_TYPE_FROM_CLASS (cls),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
/* This signal serves as notification that the GabbleJingleContent is now
* meaningless; everything holding a reference should drop it after receiving
* 'removed'.
@@ -359,6 +398,18 @@ gabble_jingle_content_class_init (GabbleJingleContentClass *cls)
G_TYPE_NONE, 0);
}
+
+static JingleContentSenders
+get_default_senders (GabbleJingleContent *c)
+{
+ JingleContentSenders (*virtual_method)(GabbleJingleContent *) = \
+ GABBLE_JINGLE_CONTENT_GET_CLASS (c)->get_default_senders;
+
+ g_assert (virtual_method != NULL);
+ return virtual_method (c);
+}
+
+
static JingleContentSenders
parse_senders (const gchar *txt)
{
@@ -406,6 +457,17 @@ new_transport_candidates_cb (GabbleJingleTransportIface *trans,
}
static void
+transport_created (GabbleJingleContent *c)
+{
+ void (*virtual_method)(GabbleJingleContent *, GabbleJingleTransportIface *) = \
+ GABBLE_JINGLE_CONTENT_GET_CLASS (c)->transport_created;
+
+ if (virtual_method != NULL)
+ virtual_method (c, c->priv->transport);
+}
+
+
+static void
parse_description (GabbleJingleContent *c, LmMessageNode *desc_node,
GError **error)
{
@@ -453,9 +515,6 @@ gabble_jingle_content_parse_add (GabbleJingleContent *c,
g_assert (priv->transport_ns == NULL);
- if (senders == NULL)
- senders = "both";
-
if (google_mode)
{
if (creator == NULL)
@@ -519,7 +578,11 @@ gabble_jingle_content_parse_add (GabbleJingleContent *c,
}
priv->created_by_us = FALSE;
- priv->senders = parse_senders (senders);
+ if (senders == NULL)
+ priv->senders = get_default_senders (c);
+ else
+ priv->senders = parse_senders (senders);
+
if (priv->senders == JINGLE_CONTENT_SENDERS_NONE)
{
SET_BAD_REQ ("invalid content senders");
@@ -561,6 +624,7 @@ gabble_jingle_content_parse_add (GabbleJingleContent *c,
g_assert (priv->transport == NULL);
priv->transport = trans;
+ transport_created (c);
g_assert (priv->creator == NULL);
priv->creator = g_strdup (creator);
@@ -577,6 +641,100 @@ gabble_jingle_content_parse_add (GabbleJingleContent *c,
return;
}
+static guint
+new_share_channel (GabbleJingleContent *c, const gchar *name)
+{
+ GabbleJingleContentPrivate *priv = c->priv;
+ GabbleJingleTransportGoogle *gtrans = NULL;
+
+ if (priv->transport &&
+ GABBLE_IS_JINGLE_TRANSPORT_GOOGLE (priv->transport))
+ {
+ guint id = priv->last_share_channel_component_id + 1;
+
+ gtrans = GABBLE_JINGLE_TRANSPORT_GOOGLE (priv->transport);
+
+ if (!jingle_transport_google_set_component_name (gtrans, name, id))
+ return 0;
+
+ priv->last_share_channel_component_id++;
+
+ DEBUG ("New Share channel '%s' with id : %d", name, id);
+
+ g_signal_emit (c, signals[NEW_SHARE_CHANNEL], 0, name, id);
+
+ return priv->last_share_channel_component_id;
+ }
+ return 0;
+}
+
+guint
+gabble_jingle_content_create_share_channel (GabbleJingleContent *self,
+ const gchar *name)
+{
+ GabbleJingleContentPrivate *priv = self->priv;
+ LmMessageNode *sess_node, *channel_node;
+ LmMessage *msg = NULL;
+
+ /* Send the info action before creating the channel, in case candidates need
+ to be sent on the signal emit. It doesn't matter if the channel already
+ exists anyways... */
+ msg = gabble_jingle_session_new_message (self->session,
+ JINGLE_ACTION_INFO, &sess_node);
+
+ DEBUG ("Sending 'info' message to peer : channel %s", name);
+ channel_node = lm_message_node_add_child (sess_node, "channel", NULL);
+ lm_message_node_set_attribute (channel_node, "xmlns", priv->content_ns);
+ lm_message_node_set_attribute (channel_node, "name", name);
+
+ gabble_jingle_session_send (self->session, msg, NULL, NULL);
+
+ return new_share_channel (self, name);
+}
+
+void
+gabble_jingle_content_send_complete (GabbleJingleContent *self)
+{
+ GabbleJingleContentPrivate *priv = self->priv;
+ LmMessageNode *sess_node, *complete_node;
+ LmMessage *msg = NULL;
+
+ msg = gabble_jingle_session_new_message (self->session,
+ JINGLE_ACTION_INFO, &sess_node);
+
+ DEBUG ("Sending 'info' message to peer : complete");
+ complete_node = lm_message_node_add_child (sess_node, "complete", NULL);
+ lm_message_node_set_attribute (complete_node, "xmlns", priv->content_ns);
+
+ gabble_jingle_session_send (self->session, msg, NULL, NULL);
+
+}
+
+void
+gabble_jingle_content_parse_info (GabbleJingleContent *c,
+ LmMessageNode *content_node, GError **error)
+{
+ LmMessageNode *channel_node;
+ LmMessageNode *complete_node;
+
+ channel_node = lm_message_node_get_child_any_ns (content_node, "channel");
+ complete_node = lm_message_node_get_child_any_ns (content_node, "complete");
+
+ DEBUG ("parsing info message : %p - %p", channel_node, complete_node);
+ if (channel_node)
+ {
+ const gchar *name;
+ name = lm_message_node_get_attribute (channel_node, "name");
+ if (name != NULL)
+ new_share_channel (c, name);
+ }
+ else if (complete_node)
+ {
+ g_signal_emit (c, signals[COMPLETED], 0);
+ }
+
+}
+
void
gabble_jingle_content_parse_accept (GabbleJingleContent *c,
LmMessageNode *content_node, gboolean google_mode, GError **error)
@@ -591,7 +749,8 @@ gabble_jingle_content_parse_accept (GabbleJingleContent *c,
trans_node = lm_message_node_get_child_any_ns (content_node, "transport");
senders = lm_message_node_get_attribute (content_node, "senders");
- if (JINGLE_IS_GOOGLE_DIALECT (dialect) && trans_node == NULL)
+ if (GABBLE_IS_JINGLE_MEDIA_RTP (c) &&
+ JINGLE_IS_GOOGLE_DIALECT (dialect) && trans_node == NULL)
{
DEBUG ("no transport node, assuming GTalk3 dialect");
/* gtalk lj0.3 assumes google-p2p transport */
@@ -599,9 +758,10 @@ gabble_jingle_content_parse_accept (GabbleJingleContent *c,
}
if (senders == NULL)
- senders = "both";
+ newsenders = get_default_senders (c);
+ else
+ newsenders = parse_senders (senders);
- newsenders = parse_senders (senders);
if (newsenders == JINGLE_CONTENT_SENDERS_NONE)
{
SET_BAD_REQ ("invalid content senders");
@@ -610,7 +770,8 @@ gabble_jingle_content_parse_accept (GabbleJingleContent *c,
if (newsenders != priv->senders)
{
- DEBUG ("changing senders from %s to %s", produce_senders (priv->senders), senders);
+ DEBUG ("changing senders from %s to %s", produce_senders (priv->senders),
+ produce_senders (newsenders));
priv->senders = newsenders;
g_object_notify ((GObject *) c, "senders");
}
@@ -774,17 +935,17 @@ gabble_jingle_content_is_ready (GabbleJingleContent *self)
{
/* If it's created by us, media ready, not signalled, and we have
* at least one local candidate, it's ready to be added. */
- if (priv->media_ready && priv->have_local_candidates &&
- (priv->state == JINGLE_CONTENT_STATE_EMPTY))
+ if (priv->media_ready && priv->state == JINGLE_CONTENT_STATE_EMPTY &&
+ (!GABBLE_IS_JINGLE_MEDIA_RTP (self) || priv->have_local_candidates))
return TRUE;
}
else
{
/* If it's created by peer, media and transports ready,
* and not acknowledged yet, it's ready for acceptance. */
- if (priv->media_ready &&
- gabble_jingle_transport_iface_can_accept (priv->transport) &&
- (priv->state == JINGLE_CONTENT_STATE_NEW))
+ if (priv->media_ready && priv->state == JINGLE_CONTENT_STATE_NEW &&
+ (!GABBLE_IS_JINGLE_MEDIA_RTP (self) ||
+ gabble_jingle_transport_iface_can_accept (priv->transport)))
return TRUE;
}
diff --git a/src/jingle-content.h b/src/jingle-content.h
index 2efe47a06..4f04d551b 100644
--- a/src/jingle-content.h
+++ b/src/jingle-content.h
@@ -31,7 +31,7 @@ G_BEGIN_DECLS
typedef enum {
JINGLE_MEDIA_TYPE_NONE = 0,
JINGLE_MEDIA_TYPE_AUDIO,
- JINGLE_MEDIA_TYPE_VIDEO
+ JINGLE_MEDIA_TYPE_VIDEO,
} JingleMediaType;
typedef enum {
@@ -85,6 +85,9 @@ struct _GabbleJingleContentClass {
void (*parse_description) (GabbleJingleContent *, LmMessageNode *,
GError **);
void (*produce_description) (GabbleJingleContent *, LmMessageNode *);
+ void (*transport_created) (GabbleJingleContent *,
+ GabbleJingleTransportIface *);
+ JingleContentSenders (*get_default_senders) (GabbleJingleContent *);
};
typedef struct _GabbleJingleContentPrivate GabbleJingleContentPrivate;
@@ -109,10 +112,14 @@ void gabble_jingle_content_produce_node (GabbleJingleContent *c,
void gabble_jingle_content_parse_accept (GabbleJingleContent *c,
LmMessageNode *content_node, gboolean google_mode, GError **error);
+void gabble_jingle_content_parse_info (GabbleJingleContent *c,
+ LmMessageNode *content_node, GError **error);
void gabble_jingle_content_parse_transport_info (GabbleJingleContent *self,
LmMessageNode *trans_node, GError **error);
void gabble_jingle_content_parse_description_info (GabbleJingleContent *self,
LmMessageNode *trans_node, GError **error);
+guint gabble_jingle_content_create_share_channel (GabbleJingleContent *self,
+ const gchar *name);
void gabble_jingle_content_add_candidates (GabbleJingleContent *self, GList *li);
void _gabble_jingle_content_set_media_ready (GabbleJingleContent *self);
gboolean gabble_jingle_content_is_ready (GabbleJingleContent *self);
@@ -144,5 +151,7 @@ gboolean gabble_jingle_content_receiving (GabbleJingleContent *self);
void gabble_jingle_content_set_sending (GabbleJingleContent *self,
gboolean send);
+void gabble_jingle_content_send_complete (GabbleJingleContent *self);
+
#endif /* __JINGLE_CONTENT_H__ */
diff --git a/src/jingle-factory.c b/src/jingle-factory.c
index 8128e4c22..0ad4d6b0a 100644
--- a/src/jingle-factory.c
+++ b/src/jingle-factory.c
@@ -32,6 +32,7 @@
#include "connection.h"
#include "debug.h"
+#include "jingle-share.h"
#include "jingle-media-rtp.h"
#include "jingle-session.h"
#include "jingle-transport-google.h"
@@ -560,6 +561,7 @@ gabble_jingle_factory_constructor (GType type,
gabble_signal_connect_weak (priv->conn, "status-changed",
(GCallback) connection_status_changed_cb, G_OBJECT (self));
+ jingle_share_register (self);
jingle_media_rtp_register (self);
jingle_transport_google_register (self);
jingle_transport_rawudp_register (self);
@@ -702,13 +704,14 @@ get_unique_sid_for (GabbleJingleFactory *factory,
{
guint32 val;
gchar *sid = NULL;
- gchar *key_;
+ gchar *key_ = NULL;
do
{
val = g_random_int_range (1000000, G_MAXINT);
g_free (sid);
+ g_free (key_);
sid = g_strdup_printf ("%u", val);
key_ = make_session_map_key (peer, jid, sid);
}
diff --git a/src/jingle-factory.h b/src/jingle-factory.h
index 04dce6d5a..1c0865ec5 100644
--- a/src/jingle-factory.h
+++ b/src/jingle-factory.h
@@ -66,7 +66,8 @@ typedef enum {
JINGLE_ACTION_SESSION_TERMINATE,
JINGLE_ACTION_TRANSPORT_INFO,
JINGLE_ACTION_TRANSPORT_ACCEPT,
- JINGLE_ACTION_DESCRIPTION_INFO
+ JINGLE_ACTION_DESCRIPTION_INFO,
+ JINGLE_ACTION_INFO
} JingleAction;
typedef enum {
diff --git a/src/jingle-media-rtp.c b/src/jingle-media-rtp.c
index 62bbd7588..0bf894be7 100644
--- a/src/jingle-media-rtp.c
+++ b/src/jingle-media-rtp.c
@@ -40,6 +40,7 @@
#include "jingle-session.h"
#include "namespaces.h"
#include "util.h"
+#include "jingle-transport-google.h"
G_DEFINE_TYPE (GabbleJingleMediaRtp,
gabble_jingle_media_rtp, GABBLE_TYPE_JINGLE_CONTENT);
@@ -244,6 +245,8 @@ static void parse_description (GabbleJingleContent *content,
LmMessageNode *desc_node, GError **error);
static void produce_description (GabbleJingleContent *obj,
LmMessageNode *content_node);
+static void transport_created (GabbleJingleContent *obj,
+ GabbleJingleTransportIface *transport);
static void
gabble_jingle_media_rtp_class_init (GabbleJingleMediaRtpClass *cls)
@@ -260,6 +263,7 @@ gabble_jingle_media_rtp_class_init (GabbleJingleMediaRtpClass *cls)
content_class->parse_description = parse_description;
content_class->produce_description = produce_description;
+ content_class->transport_created = transport_created;
param_spec = g_param_spec_uint ("media-type", "RTP media type",
"Media type.",
@@ -280,6 +284,34 @@ gabble_jingle_media_rtp_class_init (GabbleJingleMediaRtpClass *cls)
G_TYPE_NONE, 1, G_TYPE_POINTER);
}
+static void transport_created (GabbleJingleContent *content,
+ GabbleJingleTransportIface *transport)
+{
+ GabbleJingleMediaRtp *self = GABBLE_JINGLE_MEDIA_RTP (content);
+ GabbleJingleMediaRtpPrivate *priv = self->priv;
+ GabbleJingleTransportGoogle *gtrans = NULL;
+ JingleDialect dialect;
+
+ if (GABBLE_IS_JINGLE_TRANSPORT_GOOGLE (transport))
+ {
+ gtrans = GABBLE_JINGLE_TRANSPORT_GOOGLE (transport);
+ dialect = gabble_jingle_session_get_dialect (content->session);
+
+ if (priv->media_type == JINGLE_MEDIA_TYPE_VIDEO &&
+ JINGLE_IS_GOOGLE_DIALECT (dialect))
+ {
+ jingle_transport_google_set_component_name (gtrans, "video_rtp", 1);
+ jingle_transport_google_set_component_name (gtrans, "video_rtcp", 2);
+ }
+ else
+ {
+ jingle_transport_google_set_component_name (gtrans, "rtp", 1);
+ jingle_transport_google_set_component_name (gtrans, "rtcp", 2);
+ }
+ }
+}
+
+
static JingleMediaType
extract_media_type (LmMessageNode *desc_node,
GError **error)
diff --git a/src/jingle-session.c b/src/jingle-session.c
index 5aa51bfc3..9a9ba7dbd 100644
--- a/src/jingle-session.c
+++ b/src/jingle-session.c
@@ -108,7 +108,7 @@ typedef struct {
} JingleStateActions;
/* gcc should be able to figure this out from the table below, but.. */
-#define MAX_ACTIONS_PER_STATE 11
+#define MAX_ACTIONS_PER_STATE 12
/* NB: JINGLE_ACTION_UNKNOWN is used as a terminator here. */
static JingleAction allowed_actions[MAX_JINGLE_STATES][MAX_ACTIONS_PER_STATE] = {
@@ -118,24 +118,27 @@ static JingleAction allowed_actions[MAX_JINGLE_STATES][MAX_ACTIONS_PER_STATE] =
{ JINGLE_ACTION_SESSION_TERMINATE, JINGLE_ACTION_SESSION_ACCEPT,
JINGLE_ACTION_TRANSPORT_ACCEPT, /* required for GTalk4 */
JINGLE_ACTION_DESCRIPTION_INFO, JINGLE_ACTION_SESSION_INFO,
- JINGLE_ACTION_TRANSPORT_INFO, JINGLE_ACTION_UNKNOWN },
+ JINGLE_ACTION_TRANSPORT_INFO, JINGLE_ACTION_INFO,
+ JINGLE_ACTION_UNKNOWN },
/* JS_STATE_PENDING_INITIATED */
{ JINGLE_ACTION_SESSION_ACCEPT, JINGLE_ACTION_SESSION_TERMINATE,
JINGLE_ACTION_TRANSPORT_INFO, JINGLE_ACTION_CONTENT_REJECT,
JINGLE_ACTION_CONTENT_MODIFY, JINGLE_ACTION_CONTENT_ACCEPT,
JINGLE_ACTION_CONTENT_REMOVE, JINGLE_ACTION_DESCRIPTION_INFO,
JINGLE_ACTION_TRANSPORT_ACCEPT, JINGLE_ACTION_SESSION_INFO,
+ JINGLE_ACTION_INFO,
JINGLE_ACTION_UNKNOWN },
/* JS_STATE_PENDING_ACCEPT_SENT */
{ JINGLE_ACTION_TRANSPORT_INFO, JINGLE_ACTION_DESCRIPTION_INFO,
JINGLE_ACTION_SESSION_TERMINATE, JINGLE_ACTION_SESSION_INFO,
+ JINGLE_ACTION_INFO,
JINGLE_ACTION_UNKNOWN },
/* JS_STATE_ACTIVE */
{ JINGLE_ACTION_CONTENT_MODIFY, JINGLE_ACTION_CONTENT_ADD,
JINGLE_ACTION_CONTENT_REMOVE, JINGLE_ACTION_CONTENT_REPLACE,
JINGLE_ACTION_CONTENT_ACCEPT, JINGLE_ACTION_CONTENT_REJECT,
JINGLE_ACTION_SESSION_INFO, JINGLE_ACTION_TRANSPORT_INFO,
- JINGLE_ACTION_DESCRIPTION_INFO,
+ JINGLE_ACTION_DESCRIPTION_INFO, JINGLE_ACTION_INFO,
JINGLE_ACTION_SESSION_TERMINATE, JINGLE_ACTION_UNKNOWN },
/* JS_STATE_ENDED */
{ JINGLE_ACTION_UNKNOWN }
@@ -158,13 +161,15 @@ gabble_jingle_session_defines_action (GabbleJingleSession *sess,
return (a != JINGLE_ACTION_DESCRIPTION_INFO &&
a != JINGLE_ACTION_SESSION_INFO);
case JINGLE_DIALECT_GTALK4:
- if (a == JINGLE_ACTION_TRANSPORT_ACCEPT)
+ if (a == JINGLE_ACTION_TRANSPORT_ACCEPT ||
+ a == JINGLE_ACTION_INFO )
return TRUE;
case JINGLE_DIALECT_GTALK3:
return (a == JINGLE_ACTION_SESSION_ACCEPT ||
a == JINGLE_ACTION_SESSION_INITIATE ||
a == JINGLE_ACTION_SESSION_TERMINATE ||
- a == JINGLE_ACTION_TRANSPORT_INFO);
+ a == JINGLE_ACTION_TRANSPORT_INFO ||
+ a == JINGLE_ACTION_INFO);
default:
return FALSE;
}
@@ -519,6 +524,8 @@ parse_action (const gchar *txt)
return JINGLE_ACTION_TRANSPORT_ACCEPT;
else if (!tp_strdiff (txt, "description-info"))
return JINGLE_ACTION_DESCRIPTION_INFO;
+ else if (!tp_strdiff (txt, "info"))
+ return JINGLE_ACTION_INFO;
return JINGLE_ACTION_UNKNOWN;
}
@@ -559,6 +566,8 @@ produce_action (JingleAction action, JingleDialect dialect)
return "transport-accept";
case JINGLE_ACTION_DESCRIPTION_INFO:
return "description-info";
+ case JINGLE_ACTION_INFO:
+ return "info";
default:
/* only reached if g_return_val_if_fail is disabled */
DEBUG ("unknown action %u", action);
@@ -1332,6 +1341,7 @@ on_transport_info (GabbleJingleSession *sess, LmMessageNode *node,
if (JINGLE_IS_GOOGLE_DIALECT (priv->dialect))
{
GHashTableIter iter;
+ gpointer value;
if (priv->dialect == JINGLE_DIALECT_GTALK4)
{
@@ -1363,8 +1373,9 @@ on_transport_info (GabbleJingleSession *sess, LmMessageNode *node,
}
g_hash_table_iter_init (&iter, priv->initiator_contents);
- while (g_hash_table_iter_next (&iter, NULL, (gpointer) &c))
+ while (g_hash_table_iter_next (&iter, NULL, &value))
{
+ c = value;
gabble_jingle_content_parse_transport_info (c, node, error);
if (error != NULL && *error != NULL)
break;
@@ -1401,6 +1412,26 @@ on_description_info (GabbleJingleSession *sess, LmMessageNode *node,
_foreach_content (sess, node, TRUE, _each_description_info, error);
}
+static void
+on_info (GabbleJingleSession *sess, LmMessageNode *node,
+ GError **error)
+{
+ GabbleJingleSessionPrivate *priv = sess->priv;
+ GabbleJingleContent *c = NULL;
+
+ DEBUG ("received info ");
+ if (JINGLE_IS_GOOGLE_DIALECT (priv->dialect))
+ {
+ GHashTableIter iter;
+ g_hash_table_iter_init (&iter, priv->initiator_contents);
+ while (g_hash_table_iter_next (&iter, NULL, (gpointer) &c))
+ {
+ gabble_jingle_content_parse_info (c, node, error);
+ if (error != NULL && *error != NULL)
+ break;
+ }
+ }
+}
static HandlerFunc handlers[] = {
NULL, /* for unknown action */
@@ -1416,7 +1447,8 @@ static HandlerFunc handlers[] = {
on_session_terminate, /* jingle_on_session_terminate */
on_transport_info, /* jingle_on_transport_info */
on_transport_accept,
- on_description_info
+ on_description_info,
+ on_info
};
static void
@@ -1875,13 +1907,17 @@ try_session_initiate_or_accept (GabbleJingleSession *sess)
DEBUG ("Contents are ready: %s", contents_ready ? "yes" : "no");
if (!contents_ready)
+ {
+ DEBUG ("Contents not yet ready, not initiating/accepting now..");
return;
+ }
msg = gabble_jingle_session_new_message (sess, action, &sess_node);
if (priv->dialect == JINGLE_DIALECT_GTALK3)
{
gboolean has_video = FALSE;
+ gboolean has_audio = FALSE;
GHashTableIter iter;
gpointer value;
@@ -1895,19 +1931,25 @@ try_session_initiate_or_accept (GabbleJingleSession *sess)
if (type == JINGLE_MEDIA_TYPE_VIDEO)
{
has_video = TRUE;
- break;
+ }
+ else if (type == JINGLE_MEDIA_TYPE_AUDIO)
+ {
+ has_audio = TRUE;
}
}
- sess_node = lm_message_node_add_child (sess_node, "description",
- NULL);
+ if (has_video || has_audio)
+ {
+ sess_node = lm_message_node_add_child (sess_node, "description",
+ NULL);
- if (has_video)
- lm_message_node_set_attribute (sess_node, "xmlns",
- NS_GOOGLE_SESSION_VIDEO);
- else
- lm_message_node_set_attribute (sess_node, "xmlns",
- NS_GOOGLE_SESSION_PHONE);
+ if (has_video)
+ lm_message_node_set_attribute (sess_node, "xmlns",
+ NS_GOOGLE_SESSION_VIDEO);
+ else
+ lm_message_node_set_attribute (sess_node, "xmlns",
+ NS_GOOGLE_SESSION_PHONE);
+ }
}
@@ -2219,6 +2261,12 @@ gabble_jingle_session_get_peer_resource (GabbleJingleSession *sess)
}
const gchar *
+gabble_jingle_session_get_initiator (GabbleJingleSession *sess)
+{
+ return sess->priv->initiator;
+}
+
+const gchar *
gabble_jingle_session_get_sid (GabbleJingleSession *sess)
{
return sess->priv->sid;
diff --git a/src/jingle-session.h b/src/jingle-session.h
index 123c3432d..a71bcdaa0 100644
--- a/src/jingle-session.h
+++ b/src/jingle-session.h
@@ -111,6 +111,8 @@ GType gabble_jingle_session_get_content_type (GabbleJingleSession *);
GList *gabble_jingle_session_get_contents (GabbleJingleSession *sess);
const gchar *gabble_jingle_session_get_peer_resource (
GabbleJingleSession *sess);
+const gchar *gabble_jingle_session_get_initiator (
+ GabbleJingleSession *sess);
const gchar *gabble_jingle_session_get_sid (GabbleJingleSession *sess);
JingleDialect gabble_jingle_session_get_dialect (GabbleJingleSession *sess);
diff --git a/src/jingle-share.c b/src/jingle-share.c
new file mode 100644
index 000000000..918235843
--- /dev/null
+++ b/src/jingle-share.c
@@ -0,0 +1,528 @@
+/*
+ * jingle-share.c - Source for GabbleJingleShare
+ *
+ * Copyright (C) 2010 Collabora Ltd.
+ *
+ * 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
+ */
+
+/* Share content type deals with file sharing content, ie. file transfers. It
+ * Google's jingle variants (libjingle 0.3/0.4). */
+
+#include "jingle-share.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <glib.h>
+
+#include <loudmouth/loudmouth.h>
+
+#define DEBUG_FLAG GABBLE_DEBUG_SHARE
+
+#include "connection.h"
+#include "debug.h"
+#include "jingle-content.h"
+#include "jingle-factory.h"
+#include "jingle-session.h"
+#include "namespaces.h"
+#include "util.h"
+
+/******************************************************************
+ * Example description XML:
+ *
+ * <description xmlns="http://www.google.com/session/share">
+ * <manifest>
+ * <file size='341'>
+ * <name>foo.txt</name>
+ * </file>
+ * <file size='51321'>
+ * <name>foo.jpg</name>
+ * <image width='480' height='320'/>
+ * </file>
+ * <folder>
+ * <name>stuff</name>
+ * </folder>
+ * </manifest>
+ * <protocol>
+ * <http>
+ * <url name='source-path'>/temporary/23A53F01/</url>
+ * <url name='preview-path'>/temporary/90266EA1/</url>
+ * </http>
+ * </protocol>
+ * </description>
+ *
+ *******************************************************************/
+
+
+G_DEFINE_TYPE (GabbleJingleShare,
+ gabble_jingle_share, GABBLE_TYPE_JINGLE_CONTENT);
+
+/* properties */
+enum
+{
+ PROP_MEDIA_TYPE = 1,
+ PROP_FILENAME,
+ PROP_FILESIZE,
+ LAST_PROPERTY
+};
+
+struct _GabbleJingleSharePrivate
+{
+ gboolean dispose_has_run;
+
+ GabbleJingleShareManifest *manifest;
+ gchar *filename;
+ guint64 filesize;
+};
+
+
+static gchar *
+generate_temp_url (void)
+{
+ gchar *uuid = gabble_generate_id ();
+ gchar *url = NULL;
+
+ url = g_strdup_printf ("/temporary/%s/", uuid);
+ g_free (uuid);
+
+ return url;
+}
+
+static void
+free_manifest (GabbleJingleShare *self)
+{
+ GList * i;
+
+ if (self->priv->manifest)
+ {
+ for (i = self->priv->manifest->entries; i; i = i->next)
+ {
+ GabbleJingleShareManifestEntry *item = i->data;
+
+ g_free (item->name);
+ g_slice_free (GabbleJingleShareManifestEntry, item);
+ }
+ g_list_free (self->priv->manifest->entries);
+
+ g_free (self->priv->manifest->source_url);
+ g_free (self->priv->manifest->preview_url);
+
+ g_slice_free (GabbleJingleShareManifest, self->priv->manifest);
+ self->priv->manifest = NULL;
+ }
+}
+
+static void
+ensure_manifest (GabbleJingleShare *self)
+{
+ if (self->priv->manifest == NULL)
+ {
+ GabbleJingleShareManifestEntry *m = NULL;
+
+ self->priv->manifest = g_slice_new0 (GabbleJingleShareManifest);
+ self->priv->manifest->source_url = generate_temp_url ();
+ self->priv->manifest->preview_url = generate_temp_url ();
+
+ if (self->priv->filename != NULL)
+ {
+ m = g_slice_new0 (GabbleJingleShareManifestEntry);
+ m->name = g_strdup (self->priv->filename);
+ m->size = self->priv->filesize;
+ self->priv->manifest->entries = g_list_prepend (NULL, m);
+ }
+ }
+}
+
+static void
+gabble_jingle_share_init (GabbleJingleShare *obj)
+{
+ GabbleJingleSharePrivate *priv =
+ G_TYPE_INSTANCE_GET_PRIVATE (obj, GABBLE_TYPE_JINGLE_SHARE,
+ GabbleJingleSharePrivate);
+
+ DEBUG ("jingle share init called");
+ obj->priv = priv;
+
+ priv->dispose_has_run = FALSE;
+}
+
+
+static void
+gabble_jingle_share_dispose (GObject *object)
+{
+ GabbleJingleShare *self = GABBLE_JINGLE_SHARE (object);
+ GabbleJingleSharePrivate *priv = self->priv;
+
+ if (priv->dispose_has_run)
+ return;
+
+ DEBUG ("dispose called");
+ priv->dispose_has_run = TRUE;
+
+ g_free (priv->filename);
+ priv->filename = NULL;
+
+ free_manifest (self);
+
+ if (G_OBJECT_CLASS (gabble_jingle_share_parent_class)->dispose)
+ G_OBJECT_CLASS (gabble_jingle_share_parent_class)->dispose (object);
+}
+
+
+static void parse_description (GabbleJingleContent *content,
+ LmMessageNode *desc_node, GError **error);
+static void produce_description (GabbleJingleContent *obj,
+ LmMessageNode *content_node);
+
+
+static void
+gabble_jingle_share_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GabbleJingleShare *self = GABBLE_JINGLE_SHARE (object);
+ GabbleJingleSharePrivate *priv = self->priv;
+
+ switch (property_id)
+ {
+ case PROP_MEDIA_TYPE:
+ g_value_set_uint (value, JINGLE_MEDIA_TYPE_NONE);
+ break;
+ case PROP_FILENAME:
+ g_value_set_string (value, priv->filename);
+ break;
+ case PROP_FILESIZE:
+ g_value_set_uint64 (value, priv->filesize);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gabble_jingle_share_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GabbleJingleShare *self = GABBLE_JINGLE_SHARE (object);
+ GabbleJingleSharePrivate *priv = self->priv;
+
+ switch (property_id)
+ {
+ case PROP_MEDIA_TYPE:
+ break;
+ case PROP_FILENAME:
+ g_free (priv->filename);
+ priv->filename = g_value_dup_string (value);
+ free_manifest (self);
+ /* simulate a media_ready when we know our own filename */
+ _gabble_jingle_content_set_media_ready (GABBLE_JINGLE_CONTENT (self));
+ break;
+ case PROP_FILESIZE:
+ priv->filesize = g_value_get_uint64 (value);
+ free_manifest (self);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static JingleContentSenders
+get_default_senders (GabbleJingleContent *c)
+{
+ return JINGLE_CONTENT_SENDERS_INITIATOR;
+}
+
+static void
+gabble_jingle_share_class_init (GabbleJingleShareClass *cls)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (cls);
+ GabbleJingleContentClass *content_class = GABBLE_JINGLE_CONTENT_CLASS (cls);
+
+ g_type_class_add_private (cls, sizeof (GabbleJingleSharePrivate));
+
+ object_class->get_property = gabble_jingle_share_get_property;
+ object_class->set_property = gabble_jingle_share_set_property;
+ object_class->dispose = gabble_jingle_share_dispose;
+
+ content_class->parse_description = parse_description;
+ content_class->produce_description = produce_description;
+ content_class->get_default_senders = get_default_senders;
+
+ /* This property is here only because jingle-session sets the media-type
+ when constructing the object.. */
+ g_object_class_install_property (object_class, PROP_MEDIA_TYPE,
+ g_param_spec_uint ("media-type", "media type",
+ "irrelevant media type. Will always be NONE.",
+ JINGLE_MEDIA_TYPE_NONE, JINGLE_MEDIA_TYPE_NONE,
+ JINGLE_MEDIA_TYPE_NONE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (object_class, PROP_FILENAME,
+ g_param_spec_string ("filename", "file name",
+ "The name of the file",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (object_class, PROP_FILESIZE,
+ g_param_spec_uint64 ("filesize", "file size",
+ "The size of the file",
+ 0, G_MAXUINT64, 0,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+}
+
+static void
+parse_description (GabbleJingleContent *content,
+ LmMessageNode *desc_node, GError **error)
+{
+ GabbleJingleShare *self = GABBLE_JINGLE_SHARE (content);
+ GabbleJingleSharePrivate *priv = self->priv;
+ NodeIter i;
+ LmMessageNode *manifest_node = NULL;
+ LmMessageNode *protocol_node = NULL;
+ LmMessageNode *http_node = NULL;
+
+ DEBUG ("parse description called");
+
+ if (priv->manifest != NULL)
+ {
+ DEBUG ("Not parsing description, we already have a manifest");
+ return;
+ }
+
+ manifest_node = lm_message_node_get_child (desc_node, "manifest");
+ protocol_node = lm_message_node_get_child (desc_node, "protocol");
+ if (protocol_node != NULL)
+ http_node = lm_message_node_get_child (protocol_node, "http");
+
+ free_manifest (self);
+ priv->manifest = g_slice_new0 (GabbleJingleShareManifest);
+
+ /* Build the manifest */
+ for (i = node_iter (manifest_node); i; i = node_iter_next (i))
+ {
+ LmMessageNode *node = node_iter_data (i);
+ LmMessageNode *name = NULL;
+ LmMessageNode *image = NULL;
+ gboolean folder;
+ const gchar *size;
+ GabbleJingleShareManifestEntry *m = NULL;
+
+ if (!tp_strdiff (lm_message_node_get_name (node), "folder"))
+ folder = TRUE;
+ else if (!tp_strdiff (lm_message_node_get_name (node), "file"))
+ folder = FALSE;
+ else
+ continue;
+
+ name = lm_message_node_get_child (node, "name");
+ if (name == NULL)
+ continue;
+
+ m = g_slice_new0 (GabbleJingleShareManifestEntry);
+ m->folder = folder;
+ m->name = g_strdup (lm_message_node_get_value (name));
+
+ size = lm_message_node_get_attribute (node, "size");
+ if (size)
+ m->size = strtoull (size, NULL, 10);
+
+ image = lm_message_node_get_child (node, "image");
+ if (image)
+ {
+ const gchar *width;
+ const gchar *height;
+
+ m->image = TRUE;
+
+ width = lm_message_node_get_attribute (image, "width");
+ if (width)
+ m->image_width = g_ascii_strtoull (width, NULL, 10);
+
+ height =lm_message_node_get_attribute (image, "height");
+ if (height)
+ m->image_height = g_ascii_strtoull (height, NULL, 10);
+ }
+ priv->manifest->entries = g_list_prepend (priv->manifest->entries, m);
+ }
+
+ /* Get the source and preview url paths from the protocol/http node */
+ if (http_node != NULL)
+ {
+ /* clear the previously set values */
+ for (i = node_iter (http_node); i; i = node_iter_next (i))
+ {
+ LmMessageNode *node = node_iter_data (i);
+ const gchar *name;
+
+ if (tp_strdiff (lm_message_node_get_name (node), "url"))
+ continue;
+
+ name = lm_message_node_get_attribute (node, "name");
+ if (name == NULL)
+ continue;
+
+ if (!tp_strdiff (name, "source-path"))
+ {
+ const gchar *url = lm_message_node_get_value (node);
+ priv->manifest->source_url = g_strdup (url);
+ }
+
+ if (!tp_strdiff (name, "preview-path"))
+ {
+ const gchar *url = lm_message_node_get_value (node);
+ priv->manifest->preview_url = g_strdup (url);
+ }
+ }
+ }
+
+ /* Build the filename/filesize property values based on the new manifest */
+ g_free (priv->filename);
+ priv->filename = NULL;
+ priv->filesize = 0;
+
+ if (g_list_length (priv->manifest->entries) > 0)
+ {
+ if (g_list_length (priv->manifest->entries) == 1)
+ {
+ GabbleJingleShareManifestEntry *m = priv->manifest->entries->data;
+
+ if (m->folder)
+ priv->filename = g_strdup_printf ("%s.tar", m->name);
+ else
+ priv->filename = g_strdup (m->name);
+
+ priv->filesize = m->size;
+ }
+ else
+ {
+ GList *li;
+ gchar *temp;
+
+ priv->filename = g_strdup ("");
+ for (li = priv->manifest->entries; li; li = li->next)
+ {
+ GabbleJingleShareManifestEntry *m = li->data;
+
+ temp = priv->filename;
+ priv->filename = g_strdup_printf ("%s%s%s%s", temp, m->name,
+ m->folder? ".tar":"", li->next == NULL? "": "-");
+ g_free (temp);
+
+ priv->filesize += m->size;
+ }
+ temp = priv->filename;
+ priv->filename = g_strdup_printf ("%s.tar", temp);
+ g_free (temp);
+ }
+ }
+ _gabble_jingle_content_set_media_ready (content);
+}
+
+static void
+produce_description (GabbleJingleContent *content, LmMessageNode *content_node)
+{
+ GabbleJingleShare *self = GABBLE_JINGLE_SHARE (content);
+ GabbleJingleSharePrivate *priv = self->priv;
+ GList *i;
+
+ LmMessageNode *desc_node;
+ LmMessageNode *manifest_node;
+ LmMessageNode *protocol_node;
+ LmMessageNode *http_node;
+ LmMessageNode *url_node;
+
+ DEBUG ("produce description called");
+
+ ensure_manifest (self);
+
+ desc_node = lm_message_node_add_child (content_node, "description", NULL);
+
+ lm_message_node_set_attribute (desc_node, "xmlns", NS_GOOGLE_SESSION_SHARE);
+
+ manifest_node = lm_message_node_add_child (desc_node, "manifest", NULL);
+
+ for (i = priv->manifest->entries; i; i = i->next)
+ {
+ GabbleJingleShareManifestEntry *m = i->data;
+ LmMessageNode *file_node;
+ LmMessageNode *image_node;
+ gchar *size_str, *width_str, *height_str;
+
+ if (m->folder)
+ file_node = lm_message_node_add_child (manifest_node, "folder", NULL);
+ else
+ file_node = lm_message_node_add_child (manifest_node, "file", NULL);
+
+ if (m->size > 0)
+ {
+ size_str = g_strdup_printf ("%" G_GUINT64_FORMAT, m->size);
+ lm_message_node_set_attribute (file_node, "size", size_str);
+ g_free (size_str);
+ }
+ lm_message_node_add_child (file_node, "name", m->name);
+
+ if (m->image &&
+ (m->image_width > 0 || m->image_height > 0))
+ {
+ image_node = lm_message_node_add_child (file_node, "image", NULL);
+ if (m->image_width > 0)
+ {
+ width_str = g_strdup_printf ("%d", m->image_width);
+ lm_message_node_set_attribute (image_node, "width", width_str);
+ g_free (width_str);
+ }
+
+ if (m->image_height > 0)
+ {
+ height_str = g_strdup_printf ("%d", m->image_height);
+ lm_message_node_set_attribute (image_node, "height", height_str);
+ g_free (height_str);
+ }
+ }
+ }
+
+ protocol_node = lm_message_node_add_child (desc_node, "protocol", NULL);
+ http_node = lm_message_node_add_child (protocol_node, "http", NULL);
+ url_node = lm_message_node_add_child (http_node, "url",
+ priv->manifest->source_url);
+ lm_message_node_set_attribute (url_node, "name", "source-path");
+ url_node = lm_message_node_add_child (http_node, "url",
+ priv->manifest->preview_url);
+ lm_message_node_set_attribute (url_node, "name", "preview-path");
+
+}
+
+GabbleJingleShareManifest *
+gabble_jingle_share_get_manifest (GabbleJingleShare *self)
+{
+ ensure_manifest (self);
+ return self->priv->manifest;
+}
+
+void
+jingle_share_register (GabbleJingleFactory *factory)
+{
+ /* GTalk video call namespace */
+ gabble_jingle_factory_register_content_type (factory,
+ NS_GOOGLE_SESSION_SHARE,
+ GABBLE_TYPE_JINGLE_SHARE);
+}
diff --git a/src/jingle-share.h b/src/jingle-share.h
new file mode 100644
index 000000000..5444743c0
--- /dev/null
+++ b/src/jingle-share.h
@@ -0,0 +1,87 @@
+/*
+ * jingle-share.h - Header for GabbleJingleShare
+ * Copyright (C) 2008 Collabora Ltd.
+ *
+ * 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 __JINGLE_SHARE_H__
+#define __JINGLE_SHARE_H__
+
+#include <glib-object.h>
+#include <loudmouth/loudmouth.h>
+#include "types.h"
+
+#include "jingle-content.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GabbleJingleShareClass GabbleJingleShareClass;
+
+GType gabble_jingle_share_get_type (void);
+
+/* TYPE MACROS */
+#define GABBLE_TYPE_JINGLE_SHARE \
+ (gabble_jingle_share_get_type ())
+#define GABBLE_JINGLE_SHARE(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST((obj), GABBLE_TYPE_JINGLE_SHARE, \
+ GabbleJingleShare))
+#define GABBLE_JINGLE_SHARE_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST((klass), GABBLE_TYPE_JINGLE_SHARE, \
+ GabbleJingleShareClass))
+#define GABBLE_IS_JINGLE_SHARE(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE((obj), GABBLE_TYPE_JINGLE_SHARE))
+#define GABBLE_IS_JINGLE_SHARE_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE((klass), GABBLE_TYPE_JINGLE_SHARE))
+#define GABBLE_JINGLE_SHARE_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS ((obj), GABBLE_TYPE_JINGLE_SHARE, \
+ GabbleJingleShareClass))
+
+struct _GabbleJingleShareClass {
+ GabbleJingleContentClass parent_class;
+};
+
+typedef struct _GabbleJingleSharePrivate GabbleJingleSharePrivate;
+
+struct _GabbleJingleShare {
+ GabbleJingleContent parent;
+ GabbleJingleSharePrivate *priv;
+};
+
+typedef struct {
+ gboolean folder;
+ gboolean image;
+ guint64 size;
+ gchar *name;
+ guint image_width;
+ guint image_height;
+} GabbleJingleShareManifestEntry;
+
+typedef struct {
+ gchar *source_url;
+ gchar *preview_url;
+ /* a list of g_slice_new (GabbleJingleShareManifestEntry) */
+ GList *entries;
+} GabbleJingleShareManifest;
+
+void jingle_share_register (GabbleJingleFactory *factory);
+
+gchar *gabble_jingle_share_get_source_url (GabbleJingleShare *content);
+gchar *gabble_jingle_share_get_preview_url (GabbleJingleShare *content);
+GabbleJingleShareManifest *gabble_jingle_share_get_manifest (
+ GabbleJingleShare *content);
+
+#endif /* __JINGLE_SHARE_H__ */
+
diff --git a/src/jingle-transport-google.c b/src/jingle-transport-google.c
index 5087494bf..8b7ed9461 100644
--- a/src/jingle-transport-google.c
+++ b/src/jingle-transport-google.c
@@ -69,6 +69,10 @@ struct _GabbleJingleTransportGooglePrivate
JingleTransportState state;
gchar *transport_ns;
+ /* Component names or jingle-share transport 'channels'
+ g_strdup'd component name => GINT_TO_POINTER (component id) */
+ GHashTable *component_names;
+
GList *local_candidates;
/* A pointer into "local_candidates" list to mark the
@@ -88,6 +92,9 @@ gabble_jingle_transport_google_init (GabbleJingleTransportGoogle *obj)
GabbleJingleTransportGooglePrivate);
obj->priv = priv;
+ priv->component_names = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, NULL);
+
priv->dispose_has_run = FALSE;
}
@@ -103,6 +110,9 @@ gabble_jingle_transport_google_dispose (GObject *object)
DEBUG ("dispose called");
priv->dispose_has_run = TRUE;
+ g_hash_table_destroy (priv->component_names);
+ priv->component_names = NULL;
+
jingle_transport_free_candidates (priv->remote_candidates);
priv->remote_candidates = NULL;
@@ -231,13 +241,8 @@ parse_candidates (GabbleJingleTransportIface *obj,
GabbleJingleTransportGoogle *t = GABBLE_JINGLE_TRANSPORT_GOOGLE (obj);
GabbleJingleTransportGooglePrivate *priv = t->priv;
GList *candidates = NULL;
- JingleMediaType media_type;
- JingleDialect dialect;
NodeIter i;
- g_object_get (priv->content, "media-type", &media_type, NULL);
- dialect = gabble_jingle_session_get_dialect (priv->content->session);
-
for (i = node_iter (transport_node); i; i = node_iter_next (i))
{
LmMessageNode *node = node_iter_data (i);
@@ -255,32 +260,15 @@ parse_candidates (GabbleJingleTransportIface *obj,
if (name == NULL)
break;
- if (g_str_has_prefix (name, "video_"))
- {
- if (media_type != JINGLE_MEDIA_TYPE_VIDEO)
- continue;
-
- if (!tp_strdiff (name, "video_rtp"))
- component = 1;
- else if (!tp_strdiff (name, "video_rtcp"))
- component = 2;
- else
- break;
- }
- else
+ if (!g_hash_table_lookup_extended (priv->component_names, name,
+ NULL, NULL))
{
- if (media_type != JINGLE_MEDIA_TYPE_AUDIO
- && JINGLE_IS_GOOGLE_DIALECT (dialect))
- continue;
-
- if (!tp_strdiff (name, "rtp"))
- component = 1;
- else if (!tp_strdiff (name, "rtcp"))
- component = 2;
- else
- break;
+ DEBUG ("component name %s unknown to this transport", name);
+ continue;
}
+ component = GPOINTER_TO_INT (g_hash_table_lookup (priv->component_names,
+ name));
address = lm_message_node_get_attribute (node, "address");
if (address == NULL)
break;
@@ -484,40 +472,63 @@ group_and_transmit_candidates (GabbleJingleTransportGoogle *transport,
GList *candidates)
{
GabbleJingleTransportGooglePrivate *priv = transport->priv;
- GList *rtp_candidates = NULL;
- GList *rtcp_candidates = NULL;
- JingleDialect dialect;
+ GList *all_candidates = NULL;
JingleMediaType media;
GList *li;
+ GList *cands;
for (li = candidates; li != NULL; li = g_list_next (li))
{
JingleCandidate *c = li->data;
- if (c->component == 1)
- rtp_candidates = g_list_prepend (rtp_candidates, c);
- else if (c->component == 2)
- rtcp_candidates = g_list_prepend (rtcp_candidates, c);
- else
- DEBUG ("Ignoring unknown component %d", c->component);
+ for (cands = all_candidates; cands != NULL; cands = g_list_next (cands))
+ {
+ JingleCandidate *c2 = ((GList *) cands->data)->data;
+
+ if (c->component == c2->component)
+ {
+ break;
+ }
+ }
+ if (cands == NULL)
+ {
+ all_candidates = g_list_prepend (all_candidates, NULL);
+ cands = all_candidates;
+ }
+
+ cands->data = g_list_prepend (cands->data, c);
}
- dialect = gabble_jingle_session_get_dialect (priv->content->session);
g_object_get (priv->content, "media-type", &media, NULL);
- if (media == JINGLE_MEDIA_TYPE_VIDEO && JINGLE_IS_GOOGLE_DIALECT (dialect))
+ for (cands = all_candidates; cands != NULL; cands = g_list_next (cands))
{
- transmit_candidates (transport, "video_rtp", rtp_candidates);
- transmit_candidates (transport, "video_rtcp", rtcp_candidates);
- }
- else
- {
- transmit_candidates (transport, "rtp", rtp_candidates);
- transmit_candidates (transport, "rtcp", rtcp_candidates);
+ GHashTableIter iter;
+ gpointer key, value;
+ gchar *name = NULL;
+ JingleCandidate *c = ((GList *) cands->data)->data;
+
+ g_hash_table_iter_init (&iter, priv->component_names);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ {
+ if (GPOINTER_TO_INT (value) == c->component)
+ {
+ name = key;
+ break;
+ }
+ }
+ if (name)
+ {
+ transmit_candidates (transport, name, cands->data);
+ }
+ else
+ {
+ DEBUG ("Ignoring unknown component %d", c->component);
+ }
+ g_list_free (cands->data);
}
- g_list_free (rtp_candidates);
- g_list_free (rtcp_candidates);
+ g_list_free (all_candidates);
}
/* Takes in a list of slice-allocated JingleCandidate structs */
@@ -607,6 +618,23 @@ transport_iface_init (gpointer g_iface, gpointer iface_data)
klass->get_transport_type = get_transport_type;
}
+/* Returns FALSE if the component name already exists */
+gboolean
+jingle_transport_google_set_component_name (
+ GabbleJingleTransportGoogle *transport,
+ const gchar *name, guint component_id)
+{
+ GabbleJingleTransportGooglePrivate *priv = transport->priv;
+
+ if (g_hash_table_lookup_extended (priv->component_names, name, NULL, NULL))
+ return FALSE;
+
+ g_hash_table_insert (priv->component_names, g_strdup (name),
+ GINT_TO_POINTER (component_id));
+
+ return TRUE;
+}
+
void
jingle_transport_google_register (GabbleJingleFactory *factory)
{
diff --git a/src/jingle-transport-google.h b/src/jingle-transport-google.h
index 5cb7d029d..32c7ea76a 100644
--- a/src/jingle-transport-google.h
+++ b/src/jingle-transport-google.h
@@ -61,5 +61,9 @@ struct _GabbleJingleTransportGoogle {
void jingle_transport_google_register (GabbleJingleFactory *factory);
+gboolean jingle_transport_google_set_component_name (
+ GabbleJingleTransportGoogle *transport,
+ const gchar *name, guint component_id);
+
#endif /* __JINGLE_TRANSPORT_GOOGLE_H__ */
diff --git a/src/namespaces.h b/src/namespaces.h
index b1d666433..04445fac5 100644
--- a/src/namespaces.h
+++ b/src/namespaces.h
@@ -32,6 +32,7 @@
#define NS_FILE_TRANSFER "http://jabber.org/protocol/si/profile/file-transfer"
#define NS_GOOGLE_CAPS "http://www.google.com/xmpp/client/caps"
#define NS_GOOGLE_FEAT_SESSION "http://www.google.com/xmpp/protocol/session"
+#define NS_GOOGLE_FEAT_SHARE "http://google.com/xmpp/protocol/share/v1"
#define NS_GOOGLE_FEAT_VOICE "http://www.google.com/xmpp/protocol/voice/v1"
#define NS_GOOGLE_FEAT_VIDEO "http://www.google.com/xmpp/protocol/video/v1"
#define NS_GOOGLE_JINGLE_INFO "google:jingleinfo"
@@ -69,6 +70,8 @@
#define NS_GOOGLE_SESSION_PHONE "http://www.google.com/session/phone"
/* Video capability in Google's Jingle dialect */
#define NS_GOOGLE_SESSION_VIDEO "http://www.google.com/session/video"
+/* File transfer capability in Google's Jingle dialect */
+#define NS_GOOGLE_SESSION_SHARE "http://www.google.com/session/share"
/* google-p2p transport */
#define NS_GOOGLE_TRANSPORT_P2P "http://www.google.com/transport/p2p"
diff --git a/src/types.h b/src/types.h
index 2cbac15e8..95a6a4377 100644
--- a/src/types.h
+++ b/src/types.h
@@ -52,6 +52,7 @@ typedef struct _GabbleJingleTransportGoogle GabbleJingleTransportGoogle;
typedef struct _GabbleJingleTransportRawUdp GabbleJingleTransportRawUdp;
typedef struct _GabbleJingleTransportIceUdp GabbleJingleTransportIceUdp;
typedef struct _GabbleJingleMediaRtp GabbleJingleMediaRtp;
+typedef struct _GabbleJingleShare GabbleJingleShare;
typedef struct _GabbleCallMember GabbleCallMember;
typedef struct _GabbleCallMemberContent GabbleCallMemberContent;
diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am
index dab481522..1b151220f 100644
--- a/tests/twisted/Makefile.am
+++ b/tests/twisted/Makefile.am
@@ -148,6 +148,17 @@ TWISTED_TESTS = \
jingle/test-content-complex.py \
jingle/test-wait-for-caps.py \
jingle/test-wait-for-caps-incomplete.py \
+ jingle-share/test-caps-file-transfer.py \
+ jingle-share/test-send-file.py \
+ jingle-share/test-send-file-send-before-accept.py \
+ jingle-share/test-receive-file-and-close-socket-while-receiving.py \
+ jingle-share/test-receive-file-and-disconnect.py \
+ jingle-share/test-receive-file-and-sender-disconnect-while-pending.py \
+ jingle-share/test-receive-file-and-sender-disconnect-while-transfering.py \
+ jingle-share/test-receive-file-decline.py \
+ jingle-share/test-send-file-and-cancel-immediately.py \
+ jingle-share/test-send-file-wait-to-provide.py \
+ jingle-share/test-multift.py \
file-transfer/test-caps-file-transfer.py \
file-transfer/test-ibb-too-early.py \
file-transfer/test-receive-file-and-close-socket-while-receiving.py \
diff --git a/tests/twisted/constants.py b/tests/twisted/constants.py
index 69b0b736b..25f9aacb8 100644
--- a/tests/twisted/constants.py
+++ b/tests/twisted/constants.py
@@ -224,6 +224,7 @@ FT_DATE = CHANNEL_TYPE_FILE_TRANSFER + '.Date'
FT_AVAILABLE_SOCKET_TYPES = CHANNEL_TYPE_FILE_TRANSFER + '.AvailableSocketTypes'
FT_TRANSFERRED_BYTES = CHANNEL_TYPE_FILE_TRANSFER + '.TransferredBytes'
FT_INITIAL_OFFSET = CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset'
+FT_FILE_COLLECTION = CHANNEL_TYPE_FILE_TRANSFER + '.FUTURE.FileCollection'
GF_CAN_ADD = 1
GF_CAN_REMOVE = 2
diff --git a/tests/twisted/jingle-share/file_transfer_helper.py b/tests/twisted/jingle-share/file_transfer_helper.py
new file mode 100644
index 000000000..e677d0745
--- /dev/null
+++ b/tests/twisted/jingle-share/file_transfer_helper.py
@@ -0,0 +1,578 @@
+import dbus
+import socket
+import hashlib
+import time
+import datetime
+
+from servicetest import EventPattern, TimeoutError, assertEquals, assertLength
+from gabbletest import exec_test, sync_stream, make_result_iq, elem_iq, elem
+import ns
+
+from caps_helper import text_fixed_properties, text_allowed_properties, \
+ stream_tube_fixed_properties, stream_tube_allowed_properties, \
+ dbus_tube_fixed_properties, dbus_tube_allowed_properties, \
+ ft_fixed_properties, ft_allowed_properties, compute_caps_hash
+
+from twisted.words.xish import domish, xpath
+
+import constants as cs
+import sys
+
+
+class File(object):
+ DEFAULT_DATA = "What a nice file"
+ DEFAULT_NAME = "The foo.txt"
+ DEFAULT_CONTENT_TYPE = 'text/plain'
+ DEFAULT_DESCRIPTION = "A nice file to test"
+
+ def __init__(self, data=DEFAULT_DATA, name=DEFAULT_NAME,
+ content_type=DEFAULT_CONTENT_TYPE, description=DEFAULT_DESCRIPTION,
+ hash_type=cs.FILE_HASH_TYPE_MD5):
+ self.data = data
+ self.size = len(self.data)
+ self.name = name
+
+ self.content_type = content_type
+ self.description = description
+ self.date = int(time.time())
+
+ self.compute_hash(hash_type)
+
+ self.offset = 0
+
+ def compute_hash(self, hash_type):
+ assert hash_type == cs.FILE_HASH_TYPE_MD5
+ self.hash_type = hash_type
+ self.hash = hashlib.md5(self.data).hexdigest()
+
+generic_ft_caps = [(text_fixed_properties, text_allowed_properties),
+ (stream_tube_fixed_properties, \
+ stream_tube_allowed_properties),
+ (dbus_tube_fixed_properties, dbus_tube_allowed_properties),
+ (ft_fixed_properties, ft_allowed_properties)]
+
+generic_caps = [(text_fixed_properties, text_allowed_properties),
+ (stream_tube_fixed_properties, \
+ stream_tube_allowed_properties),
+ (dbus_tube_fixed_properties, dbus_tube_allowed_properties)]
+
+class FileTransferTest(object):
+ caps_identities = None
+ caps_features = None
+ caps_ft = None
+
+ def __init__(self, file, address_type, access_control, access_control_param):
+ self.file = file
+ self.address_type = address_type
+ self.access_control = access_control
+ self.access_control_param = access_control_param
+ self.closed = True
+
+ def connect(self):
+ self.conn.Connect()
+
+ self.q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED],
+ path=self.conn.object.object_path)
+
+ self.self_handle = self.conn.GetSelfHandle()
+ self.self_handle_name = self.conn.InspectHandles(cs.HT_CONTACT, [self.self_handle])[0]
+
+ def set_target(self, jid):
+ self.target = jid
+ self.handle = self.conn.RequestHandles(cs.HT_CONTACT, [jid])[0]
+
+ def set_ft_caps(self):
+ caps_iface = dbus.Interface(self.conn, cs.CONN_IFACE_CONTACT_CAPS)
+ caps_iface.UpdateCapabilities([("self",
+ [ft_fixed_properties],
+ dbus.Array([], signature="s"))])
+
+ self.q.expect('dbus-signal', signal='ContactCapabilitiesChanged',
+ path=self.conn.object.object_path,
+ args=[{self.self_handle:generic_ft_caps}])
+
+ def wait_for_ft_caps(self):
+ conn_caps_iface = dbus.Interface(self.conn, cs.CONN_IFACE_CONTACT_CAPS)
+
+ caps = conn_caps_iface.GetContactCapabilities([self.handle])
+ if caps != dbus.Dictionary({self.handle:generic_ft_caps}):
+ self.q.expect('dbus-signal',
+ signal='ContactCapabilitiesChanged',
+ path=self.conn.object.object_path,
+ args=[{self.handle:generic_ft_caps}])
+ caps = conn_caps_iface.GetContactCapabilities([self.handle])
+ assert caps == dbus.Dictionary({self.handle:generic_ft_caps}), caps
+
+ def create_ft_channel(self):
+ ft_chan = self.bus.get_object(self.conn.object.bus_name, self.ft_path)
+ self.channel = dbus.Interface(ft_chan, cs.CHANNEL)
+ self.ft_channel = dbus.Interface(ft_chan, cs.CHANNEL_TYPE_FILE_TRANSFER)
+ self.ft_props = dbus.Interface(ft_chan, cs.PROPERTIES_IFACE)
+
+ self.closed = False
+ def channel_closed_cb():
+ self.closed = True
+ self.channel.connect_to_signal('Closed', channel_closed_cb)
+
+ def close_channel(self):
+ if self.closed is False:
+ self.channel.Close()
+ self.q.expect('dbus-signal', signal='Closed',
+ path=self.channel.object_path)
+
+ def done(self):
+ pass
+
+ def test(self, q, bus, conn, stream):
+ self.q = q
+ self.bus = bus
+ self.conn = conn
+ self.stream = stream
+
+ self.stream.addObserver(
+ "//presence", self._cb_presence_iq, priority=1)
+ self.stream.addObserver(
+ "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']",
+ self._cb_disco_iq, priority=1)
+
+ def _cb_presence_iq(self, stanza):
+ nodes = xpath.queryForNodes("/presence/c", stanza)
+ c = nodes[0]
+ if 'share-v1' in c.getAttribute('ext'):
+ assert FileTransferTest.caps_identities is not None and \
+ FileTransferTest.caps_features is not None
+
+ new_hash = compute_caps_hash(FileTransferTest.caps_identities,
+ FileTransferTest.caps_features + \
+ [ns.GOOGLE_FEAT_SHARE],
+ {})
+ # Replace ver hash from one with file-transfer ns to one without
+ FileTransferTest.caps_ft = c.attributes['ver']
+ c.attributes['ver'] = new_hash
+ else:
+ node = c.attributes['node']
+ ver = c.attributes['ver']
+ # ask for raw caps
+ request = elem_iq(self.stream, 'get',
+ from_='fake_contact@jabber.org/resource')(
+ elem(ns.DISCO_INFO, 'query', node=(node + '#' + ver)))
+ self.stream.send(request)
+
+
+ def _cb_disco_iq(self, iq):
+ nodes = xpath.queryForNodes("/iq/query", iq)
+ query = nodes[0]
+
+ if query.getAttribute('node') is None:
+ return
+
+ node = query.attributes['node']
+ ver = node.replace("http://telepathy.freedesktop.org/caps#", "")
+
+ if iq.getAttribute('type') == 'result':
+
+ if FileTransferTest.caps_identities is None or \
+ FileTransferTest.caps_features is None:
+ identity_nodes = xpath.queryForNodes('/iq/query/identity', iq)
+ assertLength(1, identity_nodes)
+ identity_node = identity_nodes[0]
+
+ identity_category = identity_node['category']
+ identity_type = identity_node['type']
+ identity_name = identity_node['name']
+ identity = '%s/%s//%s' % (identity_category, identity_type,
+ identity_name)
+ FileTransferTest.caps_identities = [identity]
+
+ FileTransferTest.caps_features = []
+ for feature in xpath.queryForNodes('/iq/query/feature', iq):
+ FileTransferTest.caps_features.append(feature['var'])
+
+ # Check if the hash matches the announced capabilities
+ assertEquals(compute_caps_hash(FileTransferTest.caps_identities,
+ FileTransferTest.caps_features,
+ {}), ver)
+
+ if ver == FileTransferTest.caps_ft:
+ caps_share = compute_caps_hash(FileTransferTest.caps_identities,
+ FileTransferTest.caps_features + \
+ [ns.GOOGLE_FEAT_SHARE],
+ {})
+ n = query.attributes['node'].replace(ver, caps_share)
+ query.attributes['node'] = n
+
+ for feature in xpath.queryForNodes('/iq/query/feature', iq):
+ query.children.remove(feature)
+
+ for f in FileTransferTest.caps_features + [ns.GOOGLE_FEAT_SHARE]:
+ el = domish.Element((None, 'feature'))
+ el['var'] = f
+ query.addChild(el)
+
+ elif iq.getAttribute('type') == 'get':
+ caps_share = compute_caps_hash(FileTransferTest.caps_identities,
+ FileTransferTest.caps_features + \
+ [ns.GOOGLE_FEAT_SHARE],
+ {})
+
+ if ver == caps_share:
+ n = query.attributes['node'].replace(ver,
+ FileTransferTest.caps_ft)
+ query.attributes['node'] = n
+
+ def create_socket(self):
+ if self.address_type == cs.SOCKET_ADDRESS_TYPE_UNIX:
+ return socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ elif self.address_type == cs.SOCKET_ADDRESS_TYPE_IPV4:
+ return socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ elif self.address_type == cs.SOCKET_ADDRESS_TYPE_IPV6:
+ return socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+ else:
+ assert False
+
+class ReceiveFileTest(FileTransferTest):
+ def __init__(self, file, address_type, access_control, access_control_param):
+ FileTransferTest.__init__(self, file, address_type, access_control,
+ access_control_param)
+
+ self._actions = [self.connect, self.set_ft_caps, None,
+
+ self.wait_for_ft_caps, None,
+
+ self.check_new_channel,
+ self.accept_file, None,
+
+ self.receive_file, None,
+
+ self.close_channel, self.done]
+
+ def check_new_channel(self):
+ e = self.q.expect('dbus-signal', signal='NewChannels',
+ path=self.conn.object.object_path)
+ channels = e.args[0]
+ assert len(channels) == 1
+ path, props = channels[0]
+
+ # check channel properties
+ # org.freedesktop.Telepathy.Channel D-Bus properties
+ assert props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_FILE_TRANSFER, props
+ assert props[cs.INTERFACES] == [], props
+ assert props[cs.TARGET_HANDLE] == self.handle, props
+ assert props[cs.TARGET_ID] == self.target, props
+ assert props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT, props
+ assert props[cs.REQUESTED] == False, props
+ assert props[cs.INITIATOR_HANDLE] == self.handle, props
+ assert props[cs.INITIATOR_ID] == self.target, props
+
+ # org.freedesktop.Telepathy.Channel.Type.FileTransfer D-Bus properties
+ assert props[cs.FT_STATE] == cs.FT_STATE_PENDING, props
+ assert props[cs.FT_CONTENT_TYPE] == '', props
+ assert props[cs.FT_FILENAME].encode('utf-8') == self.file.name, props
+ assert props[cs.FT_SIZE] == self.file.size, props
+ # FT's protocol doesn't allow us the send the hash info
+ assert props[cs.FT_CONTENT_HASH_TYPE] == cs.FILE_HASH_TYPE_NONE, props
+ assert props[cs.FT_CONTENT_HASH] == '', props
+ assert props[cs.FT_DESCRIPTION] == '', props
+ assert props[cs.FT_DATE] == 0, props
+ assert props[cs.FT_AVAILABLE_SOCKET_TYPES] == \
+ {cs.SOCKET_ADDRESS_TYPE_UNIX: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST],
+ cs.SOCKET_ADDRESS_TYPE_IPV4: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST],
+ cs.SOCKET_ADDRESS_TYPE_IPV6: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST]}, \
+ props[cs.FT_AVAILABLE_SOCKET_TYPES]
+ assert props[cs.FT_TRANSFERRED_BYTES] == 0, props
+ assert props[cs.FT_INITIAL_OFFSET] == 0, props
+
+ self.ft_path = path
+
+ self.create_ft_channel()
+
+ def accept_file(self):
+ self.address = self.ft_channel.AcceptFile(self.address_type,
+ self.access_control, self.access_control_param, self.file.offset,
+ byte_arrays=True)
+
+ state_event = self.q.expect('dbus-signal',
+ signal='FileTransferStateChanged',
+ path=self.channel.object_path)
+
+ state, reason = state_event.args
+ assert state == cs.FT_STATE_ACCEPTED
+ assert reason == cs.FT_STATE_CHANGE_REASON_REQUESTED
+
+
+ state_event, offset_event = self.q.expect_many(
+ EventPattern ('dbus-signal',
+ signal='FileTransferStateChanged',
+ path=self.channel.object_path),
+ EventPattern ('dbus-signal',
+ signal='InitialOffsetDefined',
+ path=self.channel.object_path))
+
+ offset = offset_event.args[0]
+ assert offset == 0
+
+ state, reason = state_event.args
+ assert state == cs.FT_STATE_OPEN
+ assert reason == cs.FT_STATE_CHANGE_REASON_NONE
+
+ def receive_file(self):
+ # Connect to Gabble's socket
+ s = self.create_socket()
+ s.connect(self.address)
+
+ self._read_file_from_socket(s)
+
+ def _read_file_from_socket(self, s):
+ # Read the file from Gabble's socket
+ data = ''
+ read = 0
+ to_receive = self.file.size
+
+ e = self.q.expect('dbus-signal', signal='TransferredBytesChanged',
+ path=self.channel.object_path)
+ count = e.args[0]
+
+ while True:
+ received = s.recv(1024)
+ if len(received) == 0:
+ break
+ data += received
+ assert data == self.file.data
+
+ while count < to_receive:
+ # Catch TransferredBytesChanged until we transfered all the data
+ e = self.q.expect('dbus-signal', signal='TransferredBytesChanged',
+ path=self.channel.object_path)
+ count = e.args[0]
+
+ e = self.q.expect('dbus-signal', signal='FileTransferStateChanged',
+ path=self.channel.object_path)
+ state, reason = e.args
+ assert state == cs.FT_STATE_COMPLETED
+ assert reason == cs.FT_STATE_CHANGE_REASON_NONE
+
+class SendFileTest(FileTransferTest):
+ def __init__(self, file, address_type,
+ access_control, acces_control_param):
+ FileTransferTest.__init__(self, file, address_type,
+ access_control, acces_control_param)
+
+ self._actions = [self.connect, self.set_ft_caps,
+ self.check_ft_available, None,
+
+ self.wait_for_ft_caps, None,
+
+ self.request_ft_channel, self.provide_file, None,
+
+ self.send_file, self.wait_for_completion, None,
+
+ self.close_channel, self.done]
+
+ def check_ft_available(self):
+ properties = self.conn.GetAll(cs.CONN_IFACE_REQUESTS,
+ dbus_interface=cs.PROPERTIES_IFACE)
+
+ # general FT class
+ assert ({cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT},
+ [cs.FT_CONTENT_HASH_TYPE, cs.TARGET_HANDLE, cs.TARGET_ID, cs.FT_CONTENT_TYPE,
+ cs.FT_FILENAME, cs.FT_SIZE, cs.FT_CONTENT_HASH, cs.FT_DESCRIPTION, cs.FT_DATE]
+ ) in properties.get('RequestableChannelClasses'),\
+ properties['RequestableChannelClasses']
+
+ # FT class with MD5 as HashType
+ assert ({cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.FT_CONTENT_HASH_TYPE: cs.FILE_HASH_TYPE_MD5},
+ [cs.TARGET_HANDLE, cs.TARGET_ID, cs.FT_CONTENT_TYPE, cs.FT_FILENAME,
+ cs.FT_SIZE, cs.FT_CONTENT_HASH, cs.FT_DESCRIPTION, cs.FT_DATE]
+ ) in properties.get('RequestableChannelClasses'),\
+ properties['RequestableChannelClasses']
+
+ def request_ft_channel(self):
+ requests_iface = dbus.Interface(self.conn, cs.CONN_IFACE_REQUESTS)
+
+ self.ft_path, props = requests_iface.CreateChannel({
+ cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER,
+ cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+ cs.TARGET_HANDLE: self.handle,
+ cs.FT_CONTENT_TYPE: self.file.content_type,
+ cs.FT_FILENAME: self.file.name,
+ cs.FT_SIZE: self.file.size,
+ cs.FT_CONTENT_HASH_TYPE: self.file.hash_type,
+ cs.FT_CONTENT_HASH: self.file.hash,
+ cs.FT_DESCRIPTION: self.file.description,
+ cs.FT_DATE: self.file.date,
+ cs.FT_INITIAL_OFFSET: 0,
+ })
+
+ # org.freedesktop.Telepathy.Channel D-Bus properties
+ assert props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_FILE_TRANSFER
+ assert props[cs.INTERFACES] == []
+ assert props[cs.TARGET_HANDLE] == self.handle
+ assert props[cs.TARGET_ID] == self.target
+ assert props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT
+ assert props[cs.REQUESTED] == True
+ assert props[cs.INITIATOR_HANDLE] == self.self_handle
+ assert props[cs.INITIATOR_ID] == self.self_handle_name
+
+ # org.freedesktop.Telepathy.Channel.Type.FileTransfer D-Bus properties
+ assert props[cs.FT_STATE] == cs.FT_STATE_PENDING
+ assert props[cs.FT_CONTENT_TYPE] == self.file.content_type
+ assert props[cs.FT_FILENAME].encode('utf-8') == self.file.name, props
+ assert props[cs.FT_SIZE] == self.file.size
+ assert props[cs.FT_CONTENT_HASH_TYPE] == self.file.hash_type
+ assert props[cs.FT_CONTENT_HASH] == self.file.hash
+ assert props[cs.FT_DESCRIPTION] == self.file.description
+ assert props[cs.FT_DATE] == self.file.date
+ assert props[cs.FT_AVAILABLE_SOCKET_TYPES] == \
+ {cs.SOCKET_ADDRESS_TYPE_UNIX: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST],
+ cs.SOCKET_ADDRESS_TYPE_IPV4: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST],
+ cs.SOCKET_ADDRESS_TYPE_IPV6: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST]}, \
+ props[cs.FT_AVAILABLE_SOCKET_TYPES]
+ assert props[cs.FT_TRANSFERRED_BYTES] == 0
+ assert props[cs.FT_INITIAL_OFFSET] == 0
+
+ self.create_ft_channel()
+
+ self.open = False
+ self.offset_defined = False
+
+ def initial_offset_defined_cb(offset):
+ self.offset_defined = True
+ assert offset == 0, offset
+
+ self.ft_channel.connect_to_signal('InitialOffsetDefined',
+ initial_offset_defined_cb)
+
+
+ # Make sure the file transfer is of type jingle-share
+ event = self.q.expect('stream-iq', stream=self.stream,
+ query_name = 'session',
+ query_ns = ns.GOOGLE_SESSION)
+ description_node = xpath.queryForNodes('/iq/session/description',
+ event.stanza)[0]
+ assert description_node.uri == ns.GOOGLE_SESSION_SHARE, \
+ description_node.uri
+
+ def provide_file(self):
+ # try to accept our outgoing file transfer
+ try:
+ self.ft_channel.AcceptFile(self.address_type,
+ self.access_control, self.access_control_param, self.file.offset,
+ byte_arrays=True)
+ except dbus.DBusException, e:
+ assert e.get_dbus_name() == cs.NOT_AVAILABLE
+ else:
+ assert False
+
+ # In case a unit test accepts the FT before we ProvideFile
+ # then the ProvideFile will result in an OPEN state with reason
+ state = self.ft_props.Get(cs.CHANNEL_TYPE_FILE_TRANSFER, 'State')
+ if state == cs.FT_STATE_ACCEPTED:
+ self.open_reason = cs.FT_STATE_CHANGE_REASON_REQUESTED
+ else:
+ self.open_reason = cs.FT_STATE_CHANGE_REASON_NONE
+
+ self.address = self.ft_channel.ProvideFile(self.address_type,
+ self.access_control, self.access_control_param,
+ byte_arrays=True)
+
+
+ def send_file(self):
+
+ if self.open is False:
+ self.q.expect('dbus-signal',
+ signal='FileTransferStateChanged',
+ path=self.channel.object_path,
+ args=[cs.FT_STATE_OPEN, self.open_reason])
+
+ assert self.offset_defined == True
+
+ s = self.create_socket()
+ s.connect(self.address)
+ s.send(self.file.data)
+
+ def wait_for_completion(self):
+ to_send = self.file.size
+ self.count = 0
+
+ def bytes_changed_cb(bytes):
+ self.count = bytes
+
+ self.ft_channel.connect_to_signal('TransferredBytesChanged', bytes_changed_cb)
+
+
+ # FileTransferStateChanged can be fired while we are receiving data
+ self.completed = False
+ def ft_state_changed_cb(state, reason):
+ if state == cs.FT_STATE_COMPLETED:
+ self.completed = True
+ self.ft_channel.connect_to_signal('FileTransferStateChanged', ft_state_changed_cb)
+
+
+ # If not all the bytes transferred have been announced using
+ # TransferredBytesChanged, wait for them
+ while self.count < to_send:
+ self.q.expect('dbus-signal', signal='TransferredBytesChanged',
+ path=self.channel.object_path)
+
+ assert self.count == to_send
+
+
+def exec_file_transfer_test(send_cls, recv_cls, file = None):
+ addr_type = cs.SOCKET_ADDRESS_TYPE_IPV4
+ access_control = cs.SOCKET_ACCESS_CONTROL_LOCALHOST
+ access_control_param = ""
+
+ if file is None:
+ file = File()
+
+ def test(q, bus, conns, streams):
+ q.timeout = 15
+ conn1, conn2 = conns
+ stream1, stream2 = streams
+ send = send_cls(file, addr_type, access_control,
+ access_control_param)
+ recv = recv_cls(file, addr_type, access_control,
+ access_control_param)
+ send.test(q, bus, conn1, stream1)
+ recv.test(q, bus, conn2, stream2)
+
+ send_action = 0
+ recv_action = 0
+ target_set = False
+ done = False
+ while send_action < len(send._actions) or \
+ recv_action < len(recv._actions):
+ for i in range(send_action, len(send._actions)):
+ action = send._actions[i]
+ if action is None:
+ break
+ done = action()
+ if done is True:
+ break
+ send_action = i + 1
+
+ if done is True:
+ break
+
+ for i in range(recv_action, len(recv._actions)):
+ action = recv._actions[i]
+ if action is None:
+ break
+ done = action()
+ if done is True:
+ break
+ recv_action = i + 1
+
+ if done is True:
+ break
+
+ if target_set == False:
+ send.set_target(recv.self_handle_name)
+ recv.set_target(send.self_handle_name)
+ target_set = True
+
+ exec_test(test, num_instances=2)
diff --git a/tests/twisted/jingle-share/jingleshareutils.py b/tests/twisted/jingle-share/jingleshareutils.py
new file mode 100644
index 000000000..16c75fbda
--- /dev/null
+++ b/tests/twisted/jingle-share/jingleshareutils.py
@@ -0,0 +1,103 @@
+import dbus
+
+from twisted.words.xish import xpath
+
+from servicetest import (assertEquals, EventPattern)
+from gabbletest import exec_test, make_result_iq, sync_stream, make_presence
+import constants as cs
+
+from caps_helper import compute_caps_hash, \
+ text_fixed_properties, text_allowed_properties, \
+ ft_fixed_properties, ft_allowed_properties
+
+import ns
+
+run = 0
+
+def test_ft_caps_from_contact(q, bus, conn, stream, contact, contact_handle, client):
+ global run
+ run += 1
+
+ conn_caps_iface = dbus.Interface(conn, cs.CONN_IFACE_CONTACT_CAPS)
+ conn_contacts_iface = dbus.Interface(conn, cs.CONN_IFACE_CONTACTS)
+
+ # send presence with no FT cap
+ presence = make_presence(contact, status='hello')
+ c = presence.addElement((ns.CAPS, 'c'))
+ c['node'] = client
+ c['ver'] = compute_caps_hash(['client/pc//jingleshareutils-%d' % run], [], {})
+ c['ext'] = ""
+ stream.send(presence)
+
+ # Gabble looks up our capabilities
+ event = q.expect('stream-iq', to=contact, query_ns=ns.DISCO_INFO)
+ query_node = xpath.queryForNodes('/iq/query', event.stanza)[0]
+ assert query_node.attributes['node'] == \
+ client + '#' + c['ver']
+
+ # send good reply
+ result = make_result_iq(stream, event.stanza)
+ query = result.firstChildElement()
+ query['node'] = client + '#' + c['ver']
+ stream.send(result)
+
+ # no change in ContactCapabilities, so no signal ContactCapabilitiesChanged
+ sync_stream(q, stream)
+
+ # no special capabilities
+ basic_caps = dbus.Dictionary({contact_handle:
+ [(text_fixed_properties, text_allowed_properties)]})
+ caps = conn_caps_iface.GetContactCapabilities([contact_handle])
+ assert caps == basic_caps, caps
+ # test again, to check GetContactCapabilities does not have side effect
+ caps = conn_caps_iface.GetContactCapabilities([contact_handle])
+ assert caps == basic_caps, caps
+ # check the Contacts interface give the same caps
+ caps_via_contacts_iface = conn_contacts_iface.GetContactAttributes(
+ [contact_handle], [cs.CONN_IFACE_CONTACT_CAPS], False) \
+ [contact_handle][cs.ATTR_CONTACT_CAPABILITIES]
+ assert caps_via_contacts_iface == caps[contact_handle], \
+ caps_via_contacts_iface
+
+ # send presence with ft capa
+ presence = make_presence(contact, status='hello')
+ c = presence.addElement((ns.CAPS, 'c'))
+ c['node'] = client
+ c['ext'] = "share-v1"
+ c['ver'] = compute_caps_hash([], [], {})
+ stream.send(presence)
+
+ # Gabble looks up our capabilities
+ event = q.expect('stream-iq', to=contact, query_ns=ns.DISCO_INFO)
+ query_node = xpath.queryForNodes('/iq/query', event.stanza)[0]
+ assert query_node.attributes['node'] == \
+ client + '#' + c['ext']
+
+ # send good reply
+ result = make_result_iq(stream, event.stanza)
+ query = result.firstChildElement()
+ query['node'] = client + '#' + c['ext']
+ feature = query.addElement('feature')
+ feature['var'] = ns.GOOGLE_FEAT_SHARE
+ stream.send(result)
+
+
+ generic_ft_caps = dbus.Dictionary({contact_handle:
+ [(text_fixed_properties, text_allowed_properties),
+ (ft_fixed_properties, ft_allowed_properties)]})
+
+ event = q.expect('dbus-signal', signal='ContactCapabilitiesChanged')
+ assert len(event.args) == 1
+ assert event.args[0] == generic_ft_caps
+
+ caps = conn_caps_iface.GetContactCapabilities([contact_handle])
+ assert caps == generic_ft_caps, caps
+ # test again, to check GetContactCapabilities does not have side effect
+ caps = conn_caps_iface.GetContactCapabilities([contact_handle])
+ assert caps == generic_ft_caps, caps
+ # check the Contacts interface give the same caps
+ caps_via_contacts_iface = conn_contacts_iface.GetContactAttributes(
+ [contact_handle], [cs.CONN_IFACE_CONTACT_CAPS], False) \
+ [contact_handle][cs.ATTR_CONTACT_CAPABILITIES]
+ assert caps_via_contacts_iface == caps[contact_handle], \
+ caps_via_contacts_iface
diff --git a/tests/twisted/jingle-share/test-caps-file-transfer.py b/tests/twisted/jingle-share/test-caps-file-transfer.py
new file mode 100644
index 000000000..21a19ab8e
--- /dev/null
+++ b/tests/twisted/jingle-share/test-caps-file-transfer.py
@@ -0,0 +1,156 @@
+import dbus
+
+from twisted.words.xish import xpath
+
+from servicetest import (assertEquals, EventPattern)
+from gabbletest import exec_test, make_result_iq, sync_stream, make_presence
+import constants as cs
+
+from caps_helper import compute_caps_hash, \
+ text_fixed_properties, text_allowed_properties, \
+ stream_tube_fixed_properties, stream_tube_allowed_properties, \
+ dbus_tube_fixed_properties, dbus_tube_allowed_properties, \
+ ft_fixed_properties, ft_allowed_properties
+
+import ns
+from jingleshareutils import test_ft_caps_from_contact
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ client = 'http://telepathy.freedesktop.org/fake-client'
+
+ test_ft_caps_from_contact(q, bus, conn, stream, 'bilbo1@foo.com/Foo',
+ 2L, client)
+
+ # our own capabilities, formerly tested here, are now in
+ # tests/twisted/caps/advertise-contact-capabilities.py
+
+
+generic_ft_caps = [(text_fixed_properties, text_allowed_properties),
+ (stream_tube_fixed_properties, \
+ stream_tube_allowed_properties),
+ (dbus_tube_fixed_properties, dbus_tube_allowed_properties),
+ (ft_fixed_properties, ft_allowed_properties)]
+
+generic_caps = [(text_fixed_properties, text_allowed_properties),
+ (stream_tube_fixed_properties, \
+ stream_tube_allowed_properties),
+ (dbus_tube_fixed_properties, dbus_tube_allowed_properties)]
+
+def check_contact_caps(conn, handle, with_ft):
+ conn_caps_iface = dbus.Interface(conn, cs.CONN_IFACE_CONTACT_CAPS)
+ conn_contacts_iface = dbus.Interface(conn, cs.CONN_IFACE_CONTACTS)
+
+ if with_ft:
+ expected_caps = dbus.Dictionary({handle: generic_ft_caps})
+ else:
+ expected_caps = dbus.Dictionary({handle: generic_caps})
+
+ caps = conn_caps_iface.GetContactCapabilities([handle])
+ assert caps == expected_caps, caps
+ # check the Contacts interface give the same caps
+ caps_via_contacts_iface = conn_contacts_iface.GetContactAttributes(
+ [handle], [cs.CONN_IFACE_CONTACT_CAPS], False) \
+ [handle][cs.ATTR_CONTACT_CAPABILITIES]
+ assert caps_via_contacts_iface == caps[handle], \
+ caps_via_contacts_iface
+
+
+def test2(q, bus, connections, streams):
+
+ for i, conn in enumerate(connections):
+ path = conn.object.object_path
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged', path=path,
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ conn1, conn2 = connections
+ stream1, stream2 = streams
+ conn1_handle = conn1.Properties.Get(cs.CONN, 'SelfHandle')
+ conn1_jid = conn1.InspectHandles(cs.HT_CONTACT, [conn1_handle])[0]
+ conn2_handle = conn2.Properties.Get(cs.CONN, 'SelfHandle')
+ conn2_jid = conn2.InspectHandles(cs.HT_CONTACT, [conn2_handle])[0]
+ handle1 = conn2.RequestHandles(cs.HT_CONTACT, [conn1_jid])[0]
+ handle2 = conn1.RequestHandles(cs.HT_CONTACT, [conn2_jid])[0]
+
+ q.expect_many(EventPattern('dbus-signal',
+ signal='ContactCapabilitiesChanged',
+ path=conn1.object.object_path),
+ EventPattern('dbus-signal',
+ signal='ContactCapabilitiesChanged',
+ path=conn2.object.object_path))
+
+ check_contact_caps (conn1, handle2, False)
+ check_contact_caps (conn2, handle1, False)
+
+ caps_iface = dbus.Interface(conn1, cs.CONN_IFACE_CONTACT_CAPS)
+ caps_iface.UpdateCapabilities([("self",
+ [ft_fixed_properties],
+ dbus.Array([], signature="s"))])
+
+ _, presence, disco, _ = \
+ q.expect_many(EventPattern('dbus-signal',
+ signal='ContactCapabilitiesChanged',
+ path=conn1.object.object_path,
+ args=[{conn1_handle:generic_ft_caps}]),
+ EventPattern('stream-presence', stream=stream1),
+ EventPattern('stream-iq', stream=stream1,
+ query_ns=ns.DISCO_INFO,
+ iq_type = 'result'),
+ EventPattern('dbus-signal',
+ signal='ContactCapabilitiesChanged',
+ path=conn2.object.object_path,
+ args=[{handle1:generic_ft_caps}]))
+
+ presence_c = xpath.queryForNodes('/presence/c', presence.stanza)[0]
+ assert "share-v1" in presence_c.attributes['ext']
+
+ conn1_ver = presence_c.attributes['ver']
+
+ found_share = False
+ for feature in xpath.queryForNodes('/iq/query/feature', disco.stanza):
+ if feature.attributes['var'] == ns.GOOGLE_FEAT_SHARE:
+ found_share = True
+ assert found_share
+
+ check_contact_caps (conn2, handle1, True)
+
+ caps_iface = dbus.Interface(conn2, cs.CONN_IFACE_CONTACT_CAPS)
+ caps_iface.UpdateCapabilities([("self",
+ [ft_fixed_properties],
+ dbus.Array([], signature="s"))])
+
+ _, presence, _ = \
+ q.expect_many(EventPattern('dbus-signal',
+ signal='ContactCapabilitiesChanged',
+ path=conn2.object.object_path,
+ args=[{conn2_handle:generic_ft_caps}]),
+ EventPattern('stream-presence', stream=stream2),
+ EventPattern('dbus-signal',
+ signal='ContactCapabilitiesChanged',
+ path=conn1.object.object_path,
+ args=[{handle2:generic_ft_caps}]))
+
+ presence_c = xpath.queryForNodes('/presence/c', presence.stanza)[0]
+ assert "share-v1" in presence_c.attributes['ext']
+
+ # We will have the same capabilities on both sides, so we can't check for
+ # a cap disco since the hash will be the same, so we need to make sure the
+ # hash is indeed the same
+ assert presence_c.attributes['ver'] == conn1_ver
+
+ found_share = False
+ for feature in xpath.queryForNodes('/iq/query/feature', disco.stanza):
+ if feature.attributes['var'] == ns.GOOGLE_FEAT_SHARE:
+ found_share = True
+ assert found_share
+
+ check_contact_caps (conn1, handle2, True)
+
+
+if __name__ == '__main__':
+ exec_test(test)
+ exec_test(test2, num_instances=2)
diff --git a/tests/twisted/jingle-share/test-multift.py b/tests/twisted/jingle-share/test-multift.py
new file mode 100644
index 000000000..cf1a6818c
--- /dev/null
+++ b/tests/twisted/jingle-share/test-multift.py
@@ -0,0 +1,159 @@
+import dbus
+
+from twisted.words.xish import xpath
+from twisted.words.protocols.jabber.client import IQ
+
+from servicetest import (assertEquals, EventPattern, TimeoutError)
+from gabbletest import exec_test, make_result_iq, sync_stream, make_presence
+import constants as cs
+
+from caps_helper import compute_caps_hash, \
+ text_fixed_properties, text_allowed_properties, \
+ stream_tube_fixed_properties, stream_tube_allowed_properties, \
+ dbus_tube_fixed_properties, dbus_tube_allowed_properties, \
+ ft_fixed_properties, ft_allowed_properties
+
+from jingleshareutils import test_ft_caps_from_contact
+
+import ns
+
+def test(q, bus, conn, stream):
+ conn.Connect()
+ q.expect('dbus-signal', signal='StatusChanged',
+ args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED])
+
+ client = 'http://telepathy.freedesktop.org/fake-client'
+ contact = 'bilbo1@foo.com/Resource'
+ files = [("file", "File.txt", 12345, False),
+ ("file", "Image.txt", 54321, True),
+ ("folder", "Folder", 123, False),
+ ("folder", "Folder no size", None, True)]
+
+ test_ft_caps_from_contact(q, bus, conn, stream, contact,
+ 2L, client)
+
+ self_handle = conn.GetSelfHandle()
+ jid = conn.InspectHandles(cs.HT_CONTACT, [self_handle])[0]
+
+ iq = IQ(stream, "set")
+ iq['to'] = jid
+ iq['from'] = contact
+ session = iq.addElement("session", "http://www.google.com/session")
+ session['type'] = "initiate"
+ session['id'] = "2156517633"
+ session['initiator'] = contact
+ session.addElement("transport", "http://www.google.com/transport/p2p")
+ description = session.addElement("description",
+ "http://www.google.com/session/share")
+
+ manifest = description.addElement("manifest")
+ for f in files:
+ type, name, size, image = f
+ file = manifest.addElement(type)
+ if size is not None:
+ file['size'] = str(size)
+ file.addElement("name", None, name)
+ if image:
+ image = file.addElement("image")
+ image['width'] = '1200'
+ image['height'] = '1024'
+
+ protocol = description.addElement("protocol")
+ http = protocol.addElement("http")
+ url = http.addElement("url", None, "/temporary/ade15194140cf7b7bceafe/")
+ url['name'] = 'source-path'
+ url = http.addElement("url", None, "/temporary/578d715be25ddc28870d3f/")
+ url['name'] = 'preview-path'
+
+ stream.send(iq)
+ event = q.expect('dbus-signal', signal="NewChannels")
+ channels = event.args[0]
+
+ # Make sure we get the right amout of channels
+ assert len(channels) == len(files)
+
+ # Make sure every file transfer has a channel associated with it
+ found = [False for i in files]
+ file_collection = None
+ for channel in channels:
+ path, props = channel
+
+ # Get the FileCollection and make sure it exists
+ if file_collection is None:
+ file_collection = props[cs.FT_FILE_COLLECTION]
+ assert file_collection != ''
+ assert file_collection is not None
+
+ # FileCollection must be the same for every channel
+ assert props[cs.FT_FILE_COLLECTION] == file_collection, props
+
+ for i, f in enumerate(files):
+ type, name, size, image = f
+ if type == "folder":
+ name = "%s.tar" % name
+ if size is None:
+ size = 0
+
+ if props[cs.FT_FILENAME].encode('utf=8') == name:
+ assert found[i] == False
+ found[i] = True
+ assert props[cs.FT_SIZE] == size, props
+
+ assert props[cs.CHANNEL_TYPE] == cs.CHANNEL_TYPE_FILE_TRANSFER, props
+ assert props[cs.INTERFACES] == [], props
+ assert props[cs.TARGET_HANDLE] == 2L, props
+ assert props[cs.TARGET_ID] == contact.replace("/Resource", ""), props
+ assert props[cs.TARGET_HANDLE_TYPE] == cs.HT_CONTACT, props
+ assert props[cs.REQUESTED] == False, props
+ assert props[cs.INITIATOR_HANDLE] == 2L, props
+ assert props[cs.INITIATOR_ID] == contact.replace("/Resource", ""), props
+ assert props[cs.FT_STATE] == cs.FT_STATE_PENDING, props
+ assert props[cs.FT_CONTENT_TYPE] == '', props
+ # FT's protocol doesn't allow us the send the hash info
+ assert props[cs.FT_CONTENT_HASH_TYPE] == cs.FILE_HASH_TYPE_NONE, props
+ assert props[cs.FT_CONTENT_HASH] == '', props
+ assert props[cs.FT_DESCRIPTION] == '', props
+ assert props[cs.FT_DATE] == 0, props
+ assert props[cs.FT_AVAILABLE_SOCKET_TYPES] == \
+ {cs.SOCKET_ADDRESS_TYPE_UNIX: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST],
+ cs.SOCKET_ADDRESS_TYPE_IPV4: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST],
+ cs.SOCKET_ADDRESS_TYPE_IPV6: [cs.SOCKET_ACCESS_CONTROL_LOCALHOST]}, \
+ props[cs.FT_AVAILABLE_SOCKET_TYPES]
+ assert props[cs.FT_TRANSFERRED_BYTES] == 0, props
+ assert props[cs.FT_INITIAL_OFFSET] == 0, props
+
+ assert False not in found
+
+ event = q.expect('stream-iq', to=contact,
+ iq_type='set', query_name='session')
+ stanza = event.stanza
+ session_node = xpath.queryForNodes('/iq/session', event.stanza)[0]
+ assert session_node.attributes['type'] == 'transport-accept'
+
+ # Lower the timeout because we will do a q.expect where we expect it to
+ # timeout since we check for the *not* reception of the terminate
+ q.timeout = 2
+
+ # Cancel all the channels and make sure gabble cancels the multiFT only
+ # once the last channel has been closed
+ last_path, props = channels[-1]
+ for i in range(len(channels)):
+ path, props = channels[i]
+ ft_chan = bus.get_object(conn.object.bus_name, path)
+ channel = dbus.Interface(ft_chan, cs.CHANNEL)
+ channel.Close()
+ try:
+ event = q.expect('stream-iq', to=contact,
+ iq_type='set', query_name='session')
+ # If the iq is received, it must be for the last channel closed
+ assert path == last_path, event
+ # Make sure it's a terminate message
+ stanza = event.stanza
+ session_node = xpath.queryForNodes('/iq/session', event.stanza)[0]
+ assert session_node.attributes['type'] == 'terminate'
+ except TimeoutError, e:
+ # Timeout only for the non last channel getting closed
+ assert path != last_path
+
+if __name__ == '__main__':
+ exec_test(test)
diff --git a/tests/twisted/jingle-share/test-receive-file-and-close-socket-while-receiving.py b/tests/twisted/jingle-share/test-receive-file-and-close-socket-while-receiving.py
new file mode 100644
index 000000000..298fedc7d
--- /dev/null
+++ b/tests/twisted/jingle-share/test-receive-file-and-close-socket-while-receiving.py
@@ -0,0 +1,19 @@
+
+import constants as cs
+from file_transfer_helper import SendFileTest, ReceiveFileTest, \
+ exec_file_transfer_test
+
+class ReceiveFileAndCancelWhileReceiving(ReceiveFileTest):
+ def receive_file(self):
+ # Connect to Gabble's socket
+ s = self.create_socket()
+ s.connect(self.address)
+
+ # for some reason the socket is closed
+ s.close()
+
+ self.q.expect('dbus-signal', signal='FileTransferStateChanged',
+ args=[cs.FT_STATE_CANCELLED, cs.FT_STATE_CHANGE_REASON_LOCAL_ERROR])
+
+if __name__ == '__main__':
+ exec_file_transfer_test(SendFileTest, ReceiveFileAndCancelWhileReceiving)
diff --git a/tests/twisted/jingle-share/test-receive-file-and-disconnect.py b/tests/twisted/jingle-share/test-receive-file-and-disconnect.py
new file mode 100644
index 000000000..f7f958788
--- /dev/null
+++ b/tests/twisted/jingle-share/test-receive-file-and-disconnect.py
@@ -0,0 +1,16 @@
+
+from file_transfer_helper import SendFileTest, ReceiveFileTest, \
+ exec_file_transfer_test
+
+class ReceiveFileAndDisconnectTest(ReceiveFileTest):
+ def receive_file(self):
+ s = self.create_socket()
+ s.connect(self.address)
+
+ # return True so the test will be ended and the connection
+ # disconnected
+ return True
+
+if __name__ == '__main__':
+ exec_file_transfer_test(SendFileTest, ReceiveFileAndDisconnectTest)
+
diff --git a/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-pending.py b/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-pending.py
new file mode 100644
index 000000000..2437483ad
--- /dev/null
+++ b/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-pending.py
@@ -0,0 +1,52 @@
+import dbus
+
+import constants as cs
+from file_transfer_helper import SendFileTest, ReceiveFileTest, \
+ FileTransferTest, exec_file_transfer_test
+
+class ReceiveFileAndSenderDisconnectWhilePendingTest(ReceiveFileTest):
+ def accept_file(self):
+
+ e = self.q.expect('dbus-signal', signal='FileTransferStateChanged',
+ path = self.channel.object_path,
+ args=[cs.FT_STATE_CANCELLED, \
+ cs.FT_STATE_CHANGE_REASON_REMOTE_STOPPED])
+
+ # We can't accept the transfer now
+ try:
+ self.ft_channel.AcceptFile(cs.SOCKET_ADDRESS_TYPE_UNIX,
+ cs.SOCKET_ACCESS_CONTROL_LOCALHOST, "", 0)
+ except dbus.DBusException, e:
+ assert e.get_dbus_name() == cs.NOT_AVAILABLE
+ else:
+ assert False
+
+ self.close_channel()
+
+ # stop the test
+ return True
+
+
+class SendFileAndDisconnect (SendFileTest):
+ def __init__(self, file, address_type,
+ access_control, acces_control_param):
+ FileTransferTest.__init__(self, file, address_type,
+ access_control, acces_control_param)
+
+ self._actions = [self.connect, self.set_ft_caps,
+ self.check_ft_available, None,
+
+ self.wait_for_ft_caps, None,
+
+ self.request_ft_channel, self.provide_file,
+ self.disconnect, None,
+
+ self.close_channel, self.done]
+
+ def disconnect(self):
+ self.conn.Disconnect()
+
+
+if __name__ == '__main__':
+ exec_file_transfer_test(SendFileAndDisconnect, \
+ ReceiveFileAndSenderDisconnectWhilePendingTest)
diff --git a/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-transfering.py b/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-transfering.py
new file mode 100644
index 000000000..c7b5b92a9
--- /dev/null
+++ b/tests/twisted/jingle-share/test-receive-file-and-sender-disconnect-while-transfering.py
@@ -0,0 +1,45 @@
+import dbus
+
+import constants as cs
+from file_transfer_helper import SendFileTest, ReceiveFileTest, \
+ FileTransferTest, exec_file_transfer_test
+
+class ReceiveFileAndSenderDisconnectWhileTransfering(ReceiveFileTest):
+ def receive_file(self):
+ self.q.expect('dbus-signal', signal='FileTransferStateChanged',
+ path = self.channel.object_path,
+ args=[cs.FT_STATE_CANCELLED, \
+ cs.FT_STATE_CHANGE_REASON_REMOTE_STOPPED])
+
+ self.close_channel()
+
+ # stop the test
+ return True
+
+
+class SendFileAndDisconnect (SendFileTest):
+ def __init__(self, file, address_type,
+ access_control, acces_control_param):
+ FileTransferTest.__init__(self, file, address_type,
+ access_control, acces_control_param)
+
+ self._actions = [self.connect, self.set_ft_caps,
+ self.check_ft_available, None,
+
+ self.wait_for_ft_caps, None,
+
+ self.request_ft_channel, self.provide_file, None,
+
+ self.send_file, self.wait_for_completion,
+ self.disconnect, None,
+
+ self.close_channel, self.done]
+
+
+ def disconnect(self):
+ self.conn.Disconnect()
+
+
+if __name__ == '__main__':
+ exec_file_transfer_test(SendFileAndDisconnect, \
+ ReceiveFileAndSenderDisconnectWhileTransfering)
diff --git a/tests/twisted/jingle-share/test-receive-file-decline.py b/tests/twisted/jingle-share/test-receive-file-decline.py
new file mode 100644
index 000000000..e7950f3f4
--- /dev/null
+++ b/tests/twisted/jingle-share/test-receive-file-decline.py
@@ -0,0 +1,79 @@
+import dbus
+import constants as cs
+
+from servicetest import EventPattern
+from file_transfer_helper import SendFileTest, ReceiveFileTest, \
+ FileTransferTest, exec_file_transfer_test
+
+class ReceiveFileDecline(ReceiveFileTest):
+
+ def __init__(self, file, address_type, access_control, access_control_param):
+ FileTransferTest.__init__(self, file, address_type, access_control,
+ access_control_param)
+ self._actions = [self.connect, self.set_ft_caps, None,
+
+ self.wait_for_ft_caps, None,
+
+ self.check_new_channel, self.close_and_check, self.done]
+
+ def close_and_check(self):
+ self.channel.Close()
+
+ state_event, event, _ = self.q.expect_many(
+ EventPattern('dbus-signal', signal='FileTransferStateChanged',
+ path=self.channel.object_path),
+ EventPattern('stream-iq', stream=self.stream,
+ iq_type='set', query_name='session'),
+ EventPattern('dbus-signal', signal='Closed',
+ path=self.channel.object_path))
+
+ state, reason = state_event.args
+ assert state == cs.FT_STATE_CANCELLED
+ assert reason == cs.FT_STATE_CHANGE_REASON_LOCAL_STOPPED
+
+ while event.query.getAttribute('type') != 'terminate':
+ event = self.q.expect('stream-iq', stream=self.stream,
+ iq_type='set', query_name='session')
+
+
+
+class SendFileDeclined (SendFileTest):
+ def __init__(self, file, address_type,
+ access_control, acces_control_param):
+ FileTransferTest.__init__(self, file, address_type,
+ access_control, acces_control_param)
+
+ self._actions = [self.connect, self.set_ft_caps,
+ self.check_ft_available, None,
+
+ self.wait_for_ft_caps, None,
+
+ self.request_ft_channel, self.provide_file, None,
+
+ self.check_declined, self.close_channel, self.done]
+
+ def check_declined(self):
+ state_event = self.q.expect('dbus-signal',
+ signal='FileTransferStateChanged',
+ path=self.channel.object_path)
+
+ state, reason = state_event.args
+ assert state == cs.FT_STATE_CANCELLED
+ assert reason == cs.FT_STATE_CHANGE_REASON_REMOTE_STOPPED
+
+ transferred = self.ft_props.Get(cs.CHANNEL_TYPE_FILE_TRANSFER,
+ 'TransferredBytes')
+ # no byte has been transferred as the file was declined
+ assert transferred == 0
+
+ # try to provide the file
+ try:
+ self.provide_file()
+ except dbus.DBusException, e:
+ assert e.get_dbus_name() == cs.NOT_AVAILABLE
+ else:
+ assert False
+
+
+if __name__ == '__main__':
+ exec_file_transfer_test(SendFileDeclined, ReceiveFileDecline)
diff --git a/tests/twisted/jingle-share/test-send-file-and-cancel-immediately.py b/tests/twisted/jingle-share/test-send-file-and-cancel-immediately.py
new file mode 100644
index 000000000..2375e849c
--- /dev/null
+++ b/tests/twisted/jingle-share/test-send-file-and-cancel-immediately.py
@@ -0,0 +1,74 @@
+import dbus
+import constants as cs
+
+from servicetest import EventPattern
+from file_transfer_helper import SendFileTest, ReceiveFileTest, \
+ FileTransferTest, exec_file_transfer_test
+
+class ReceiveFileStopped(ReceiveFileTest):
+
+ def __init__(self, file, address_type, access_control, access_control_param):
+ FileTransferTest.__init__(self, file, address_type, access_control,
+ access_control_param)
+ self._actions = [self.connect, self.set_ft_caps, None,
+
+ self.wait_for_ft_caps, None,
+
+ self.check_new_channel, None,
+
+ self.check_stopped, None,
+
+ self.close_channel, self.done]
+
+ def check_stopped(self):
+ state_event = self.q.expect ('dbus-signal',
+ signal='FileTransferStateChanged',
+ path=self.channel.object_path)
+
+ state, reason = state_event.args
+ assert state == cs.FT_STATE_CANCELLED
+ assert reason == cs.FT_STATE_CHANGE_REASON_REMOTE_STOPPED
+
+ # try to provide the file
+ try:
+ self.accept_file()
+ except dbus.DBusException, e:
+ assert e.get_dbus_name() == cs.NOT_AVAILABLE
+ else:
+ assert False
+
+
+
+
+class SendFileAndClose (SendFileTest):
+ def __init__(self, file, address_type,
+ access_control, acces_control_param):
+ FileTransferTest.__init__(self, file, address_type,
+ access_control, acces_control_param)
+
+ self._actions = [self.connect, self.set_ft_caps,
+ self.check_ft_available, None,
+
+ self.wait_for_ft_caps, None,
+
+ self.request_ft_channel, self.provide_file, None,
+
+ self.close_and_check, None,
+
+ self.close_channel, self.done]
+
+ def close_and_check(self):
+ self.channel.Close()
+
+ state_event, _ = self.q.expect_many(
+ EventPattern('dbus-signal', signal='FileTransferStateChanged',
+ path=self.channel.object_path),
+ EventPattern('dbus-signal', signal='Closed',
+ path=self.channel.object_path))
+
+ state, reason = state_event.args
+ assert state == cs.FT_STATE_CANCELLED
+ assert reason == cs.FT_STATE_CHANGE_REASON_LOCAL_STOPPED
+
+if __name__ == '__main__':
+ exec_file_transfer_test(SendFileAndClose, ReceiveFileStopped)
diff --git a/tests/twisted/jingle-share/test-send-file-send-before-accept.py b/tests/twisted/jingle-share/test-send-file-send-before-accept.py
new file mode 100644
index 000000000..d67991a91
--- /dev/null
+++ b/tests/twisted/jingle-share/test-send-file-send-before-accept.py
@@ -0,0 +1,28 @@
+from file_transfer_helper import SendFileTest, FileTransferTest, \
+ ReceiveFileTest, exec_file_transfer_test
+
+
+class SendFileBeforeAccept(SendFileTest):
+ def __init__(self, file, address_type,
+ access_control, acces_control_param):
+ FileTransferTest.__init__(self, file, address_type,
+ access_control, acces_control_param)
+
+ self._actions = [self.connect, self.set_ft_caps,
+ self.check_ft_available, None,
+
+ self.wait_for_ft_caps, None,
+
+ self.request_ft_channel, self.provide_file,
+ self.set_open, self.send_file, None,
+
+ self.wait_for_completion, None,
+
+ self.close_channel, self.done]
+
+ def set_open(self):
+ self.open = True
+ self.offset_defined = True
+
+if __name__ == '__main__':
+ exec_file_transfer_test(SendFileBeforeAccept, ReceiveFileTest)
diff --git a/tests/twisted/jingle-share/test-send-file-wait-to-provide.py b/tests/twisted/jingle-share/test-send-file-wait-to-provide.py
new file mode 100644
index 000000000..0a1ba653e
--- /dev/null
+++ b/tests/twisted/jingle-share/test-send-file-wait-to-provide.py
@@ -0,0 +1,37 @@
+import dbus
+import constants as cs
+
+from servicetest import EventPattern
+from file_transfer_helper import SendFileTest, ReceiveFileTest, \
+ FileTransferTest, exec_file_transfer_test
+
+class SendFileAndWaitToProvide (SendFileTest):
+ def __init__(self, file, address_type,
+ access_control, acces_control_param):
+ FileTransferTest.__init__(self, file, address_type,
+ access_control, acces_control_param)
+
+ self._actions = [self.connect, self.set_ft_caps,
+ self.check_ft_available, None,
+
+ self.wait_for_ft_caps, None,
+
+ self.request_ft_channel, self.check_pending_state, None,
+
+ self.check_accepted_state, self.provide_file,
+ self.send_file, self.wait_for_completion, None,
+
+ self.close_channel, self.done]
+
+ def check_pending_state(self):
+ # state is still Pending as remote didn't accept the transfer yet
+ state = self.ft_props.Get(cs.CHANNEL_TYPE_FILE_TRANSFER, 'State')
+ assert state == cs.FT_STATE_PENDING
+
+ def check_accepted_state(self):
+ # Remote accepted the transfer
+ state = self.ft_props.Get(cs.CHANNEL_TYPE_FILE_TRANSFER, 'State')
+ assert state == cs.FT_STATE_ACCEPTED, state
+
+if __name__ == '__main__':
+ exec_file_transfer_test(SendFileAndWaitToProvide, ReceiveFileTest)
diff --git a/tests/twisted/jingle-share/test-send-file.py b/tests/twisted/jingle-share/test-send-file.py
new file mode 100644
index 000000000..0f8ae7611
--- /dev/null
+++ b/tests/twisted/jingle-share/test-send-file.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+from file_transfer_helper import SendFileTest, ReceiveFileTest, \
+ exec_file_transfer_test, File
+
+if __name__ == '__main__':
+ file = File()
+ file.offset = 5
+ file.name = "The greek foo δοκιμή.txt"
+ exec_file_transfer_test(SendFileTest, ReceiveFileTest, file)
diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py
index c9008022d..d9b290fff 100644
--- a/tests/twisted/ns.py
+++ b/tests/twisted/ns.py
@@ -8,6 +8,7 @@ FEATURE_NEG = 'http://jabber.org/protocol/feature-neg'
FILE_TRANSFER = 'http://jabber.org/protocol/si/profile/file-transfer'
GEOLOC = 'http://jabber.org/protocol/geoloc'
GOOGLE_FEAT_SESSION = 'http://www.google.com/xmpp/protocol/session'
+GOOGLE_FEAT_SHARE = 'http://google.com/xmpp/protocol/share/v1'
GOOGLE_FEAT_VOICE = 'http://www.google.com/xmpp/protocol/voice/v1'
GOOGLE_FEAT_VIDEO = 'http://www.google.com/xmpp/protocol/video/v1'
GOOGLE_JINGLE_INFO = 'google:jingleinfo'
@@ -15,6 +16,7 @@ GOOGLE_P2P = "http://www.google.com/transport/p2p"
GOOGLE_QUEUE = 'google:queue'
GOOGLE_ROSTER = 'google:roster'
GOOGLE_SESSION = "http://www.google.com/session"
+GOOGLE_SESSION_SHARE = "http://www.google.com/session/share"
GOOGLE_SESSION_PHONE = "http://www.google.com/session/phone"
GOOGLE_SESSION_VIDEO = "http://www.google.com/session/video"
GOOGLE_MAIL_NOTIFY = "google:mail:notify"