1 /*
2  * This file is part of TelepathyQt
3  *
4  * @copyright Copyright (C) 2010-2012 Collabora Ltd. <http://www.collabora.co.uk/>
5  * @copyright Copyright (C) 2012 Nokia Corporation
6  * @license LGPL 2.1
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2.1 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to the Free Software
20  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
21  */
22 
23 #include <TelepathyQt/CallContent>
24 
25 #include "TelepathyQt/_gen/call-content.moc.hpp"
26 
27 #include "TelepathyQt/_gen/cli-call-content-body.hpp"
28 #include "TelepathyQt/_gen/cli-call-content.moc.hpp"
29 
30 #include <TelepathyQt/debug-internal.h>
31 
32 #include <TelepathyQt/CallChannel>
33 #include <TelepathyQt/DBus>
34 #include <TelepathyQt/PendingReady>
35 #include <TelepathyQt/PendingVoid>
36 #include <TelepathyQt/PendingVariantMap>
37 #include <TelepathyQt/ReadinessHelper>
38 
39 namespace Tp
40 {
41 
42 /* ====== CallContent ====== */
43 struct TP_QT_NO_EXPORT CallContent::Private
44 {
45     Private(CallContent *parent, const CallChannelPtr &channel);
46 
47     static void introspectMainProperties(Private *self);
48     void checkIntrospectionCompleted();
49 
50     CallStreamPtr addStream(const QDBusObjectPath &streamPath);
51     CallStreamPtr lookupStream(const QDBusObjectPath &streamPath);
52 
53     // Public object
54     CallContent *parent;
55 
56     WeakPtr<CallChannel> channel;
57 
58     // Mandatory proxies
59     Client::CallContentInterface *contentInterface;
60 
61     ReadinessHelper *readinessHelper;
62 
63     // Introspection
64     QString name;
65     uint type;
66     uint disposition;
67     CallStreams streams;
68     CallStreams incompleteStreams;
69 };
70 
Private(CallContent * parent,const CallChannelPtr & channel)71 CallContent::Private::Private(CallContent *parent, const CallChannelPtr &channel)
72     : parent(parent),
73       channel(channel.data()),
74       contentInterface(parent->interface<Client::CallContentInterface>()),
75       readinessHelper(parent->readinessHelper())
76 {
77     ReadinessHelper::Introspectables introspectables;
78 
79     ReadinessHelper::Introspectable introspectableCore(
80         QSet<uint>() << 0,                                                         // makesSenseForStatuses
81         Features(),                                                            // dependsOnFeatures
82         QStringList(),                                                             // dependsOnInterfaces
83         (ReadinessHelper::IntrospectFunc) &Private::introspectMainProperties,
84         this);
85     introspectables[FeatureCore] = introspectableCore;
86 
87     readinessHelper->addIntrospectables(introspectables);
88     readinessHelper->becomeReady(FeatureCore);
89 }
90 
introspectMainProperties(CallContent::Private * self)91 void CallContent::Private::introspectMainProperties(CallContent::Private *self)
92 {
93     CallContent *parent = self->parent;
94     CallChannelPtr channel = parent->channel();
95 
96     parent->connect(self->contentInterface,
97             SIGNAL(StreamsAdded(Tp::ObjectPathList)),
98             SLOT(onStreamsAdded(Tp::ObjectPathList)));
99     parent->connect(self->contentInterface,
100             SIGNAL(StreamsRemoved(Tp::ObjectPathList,Tp::CallStateReason)),
101             SLOT(onStreamsRemoved(Tp::ObjectPathList,Tp::CallStateReason)));
102 
103     parent->connect(self->contentInterface->requestAllProperties(),
104             SIGNAL(finished(Tp::PendingOperation*)),
105             SLOT(gotMainProperties(Tp::PendingOperation*)));
106 }
107 
checkIntrospectionCompleted()108 void CallContent::Private::checkIntrospectionCompleted()
109 {
110     if (!parent->isReady(FeatureCore) && incompleteStreams.size() == 0) {
111         readinessHelper->setIntrospectCompleted(FeatureCore, true);
112     }
113 }
114 
addStream(const QDBusObjectPath & streamPath)115 CallStreamPtr CallContent::Private::addStream(const QDBusObjectPath &streamPath)
116 {
117     CallStreamPtr stream = CallStreamPtr(
118             new CallStream(CallContentPtr(parent), streamPath));
119     incompleteStreams.append(stream);
120     parent->connect(stream->becomeReady(),
121             SIGNAL(finished(Tp::PendingOperation*)),
122             SLOT(onStreamReady(Tp::PendingOperation*)));
123     return stream;
124 }
125 
lookupStream(const QDBusObjectPath & streamPath)126 CallStreamPtr CallContent::Private::lookupStream(const QDBusObjectPath &streamPath)
127 {
128     foreach (const CallStreamPtr &stream, streams) {
129         if (stream->objectPath() == streamPath.path()) {
130             return stream;
131         }
132     }
133     foreach (const CallStreamPtr &stream, incompleteStreams) {
134         if (stream->objectPath() == streamPath.path()) {
135             return stream;
136         }
137     }
138     return CallStreamPtr();
139 }
140 
141 /**
142  * \class CallContent
143  * \ingroup clientchannel
144  * \headerfile TelepathyQt/call-content.h <TelepathyQt/CallContent>
145  *
146  * \brief The CallContent class provides an object representing a Telepathy
147  * Call.Content.
148  *
149  * Instances of this class cannot be constructed directly; the only way to get
150  * one is via CallChannel.
151  *
152  * See \ref async_model
153  */
154 
155 /**
156  * Feature representing the core that needs to become ready to make the
157  * CallContent object usable.
158  *
159  * Note that this feature must be enabled in order to use most CallContent
160  * methods. See specific methods documentation for more details.
161  *
162  * When calling isReady(), becomeReady(), this feature is implicitly added
163  * to the requested features.
164  */
165 const Feature CallContent::FeatureCore = Feature(QLatin1String(CallContent::staticMetaObject.className()), 0);
166 
167 /**
168  * Construct a new CallContent object.
169  *
170  * \param channel The channel owning this media content.
171  * \param name The object path of this media content.
172  */
CallContent(const CallChannelPtr & channel,const QDBusObjectPath & objectPath)173 CallContent::CallContent(const CallChannelPtr &channel, const QDBusObjectPath &objectPath)
174     : StatefulDBusProxy(channel->dbusConnection(), channel->busName(),
175             objectPath.path(), FeatureCore),
176       OptionalInterfaceFactory<CallContent>(this),
177       mPriv(new Private(this, channel))
178 {
179 }
180 
181 /**
182  * Class destructor.
183  */
~CallContent()184 CallContent::~CallContent()
185 {
186     delete mPriv;
187 }
188 
189 /**
190  * Return the channel owning this media content.
191  *
192  * \return The channel owning this media content.
193  */
channel() const194 CallChannelPtr CallContent::channel() const
195 {
196     return CallChannelPtr(mPriv->channel);
197 }
198 
199 /**
200  * Return the name of this media content.
201  *
202  * \return The name of this media content.
203  */
name() const204 QString CallContent::name() const
205 {
206     return mPriv->name;
207 }
208 
209 /**
210  * Return the type of this media content.
211  *
212  * \return The type of this media content.
213  */
type() const214 MediaStreamType CallContent::type() const
215 {
216     return (MediaStreamType) mPriv->type;
217 }
218 
219 /**
220  * Return the disposition of this media content.
221  *
222  * \return The disposition of this media content.
223  */
disposition() const224 CallContentDisposition CallContent::disposition() const
225 {
226     return (CallContentDisposition) mPriv->disposition;
227 }
228 
229 /**
230  * Return the media streams of this media content.
231  *
232  * \return A list of media streams of this media content.
233  * \sa streamAdded(), streamRemoved()
234  */
streams() const235 CallStreams CallContent::streams() const
236 {
237     return mPriv->streams;
238 }
239 
240 /**
241  * Removes this media content from the call.
242  *
243  * \return A PendingOperation which will emit PendingOperation::finished
244  *         when the call has finished.
245  */
remove()246 PendingOperation *CallContent::remove()
247 {
248     return new PendingVoid(mPriv->contentInterface->Remove(), CallContentPtr(this));
249 }
250 
251 /**
252  * Return whether sending DTMF events is supported on this content.
253  * DTMF is only supported on audio contents that implement the
254  * #TP_QT_IFACE_CALL_CONTENT_INTERFACE_DTMF interface.
255  *
256  * \returns \c true if DTMF is supported, or \c false otherwise.
257  */
supportsDTMF() const258 bool CallContent::supportsDTMF() const
259 {
260     return hasInterface(TP_QT_IFACE_CALL_CONTENT_INTERFACE_DTMF);
261 }
262 
263 /**
264  * Start sending a DTMF tone on this media stream.
265  *
266  * Where possible, the tone will continue until stopDTMFTone() is called.
267  * On certain protocols, it may only be possible to send events with a predetermined
268  * length. In this case, the implementation may emit a fixed-length tone,
269  * and the stopDTMFTone() method call should return #TP_QT_ERROR_NOT_AVAILABLE.
270  *
271  * If this content does not support the #TP_QT_IFACE_CALL_CONTENT_INTERFACE_DTMF
272  * interface, the resulting PendingOperation will fail with error code
273  * #TP_QT_ERROR_NOT_IMPLEMENTED.
274  *
275  * \param event A numeric event code from the #DTMFEvent enum.
276  * \return A PendingOperation which will emit PendingOperation::finished
277  *         when the request finishes.
278  * \sa stopDTMFTone(), supportsDTMF()
279  */
startDTMFTone(DTMFEvent event)280 PendingOperation *CallContent::startDTMFTone(DTMFEvent event)
281 {
282     if (!supportsDTMF()) {
283         warning() << "CallContent::startDTMFTone() used with no dtmf interface";
284         return new PendingFailure(TP_QT_ERROR_NOT_IMPLEMENTED,
285                 QLatin1String("This CallContent does not support the dtmf interface"),
286                 CallContentPtr(this));
287     }
288 
289     Client::CallContentInterfaceDTMFInterface *dtmfInterface =
290         interface<Client::CallContentInterfaceDTMFInterface>();
291     return new PendingVoid(dtmfInterface->StartTone(event), CallContentPtr(this));
292 }
293 
294 /**
295  * Stop sending any DTMF tone which has been started using the startDTMFTone()
296  * method.
297  *
298  * If there is no current tone, the resulting PendingOperation will
299  * finish successfully.
300  *
301  * If this content does not support the #TP_QT_IFACE_CALL_CONTENT_INTERFACE_DTMF
302  * interface, the resulting PendingOperation will fail with error code
303  * #TP_QT_ERROR_NOT_IMPLEMENTED.
304  *
305  * \return A PendingOperation which will emit PendingOperation::finished
306  *         when the request finishes.
307  * \sa startDTMFTone(), supportsDTMF()
308  */
stopDTMFTone()309 PendingOperation *CallContent::stopDTMFTone()
310 {
311     if (!supportsDTMF()) {
312         warning() << "CallContent::stopDTMFTone() used with no dtmf interface";
313         return new PendingFailure(TP_QT_ERROR_NOT_IMPLEMENTED,
314                 QLatin1String("This CallContent does not support the dtmf interface"),
315                 CallContentPtr(this));
316     }
317 
318     Client::CallContentInterfaceDTMFInterface *dtmfInterface =
319         interface<Client::CallContentInterfaceDTMFInterface>();
320     return new PendingVoid(dtmfInterface->StopTone(), CallContentPtr(this));
321 }
322 
gotMainProperties(PendingOperation * op)323 void CallContent::gotMainProperties(PendingOperation *op)
324 {
325     if (op->isError()) {
326         warning().nospace() << "CallContentInterface::requestAllProperties() failed with" <<
327             op->errorName() << ": " << op->errorMessage();
328         mPriv->readinessHelper->setIntrospectCompleted(FeatureCore, false,
329             op->errorName(), op->errorMessage());
330         return;
331     }
332 
333     debug() << "Got reply to CallContentInterface::requestAllProperties()";
334 
335     PendingVariantMap *pvm = qobject_cast<PendingVariantMap*>(op);
336     Q_ASSERT(pvm);
337 
338     QVariantMap props = pvm->result();
339 
340     mPriv->name = qdbus_cast<QString>(props[QLatin1String("Name")]);
341     mPriv->type = qdbus_cast<uint>(props[QLatin1String("Type")]);
342     mPriv->disposition = qdbus_cast<uint>(props[QLatin1String("Disposition")]);
343     setInterfaces(qdbus_cast<QStringList>(props[QLatin1String("Interfaces")]));
344 
345     ObjectPathList streamsPaths = qdbus_cast<ObjectPathList>(props[QLatin1String("Streams")]);
346     if (streamsPaths.size() != 0) {
347         foreach (const QDBusObjectPath &streamPath, streamsPaths) {
348             CallStreamPtr stream = mPriv->lookupStream(streamPath);
349             if (!stream) {
350                 mPriv->addStream(streamPath);
351             }
352         }
353     } else {
354         mPriv->readinessHelper->setIntrospectCompleted(FeatureCore, true);
355     }
356 }
357 
onStreamsAdded(const ObjectPathList & streamsPaths)358 void CallContent::onStreamsAdded(const ObjectPathList &streamsPaths)
359 {
360     foreach (const QDBusObjectPath &streamPath, streamsPaths) {
361         debug() << "Received Call::Content::StreamAdded for stream" << streamPath.path();
362 
363         if (mPriv->lookupStream(streamPath)) {
364             debug() << "Stream already exists, ignoring";
365             return;
366         }
367 
368         mPriv->addStream(streamPath);
369     }
370 }
371 
onStreamsRemoved(const ObjectPathList & streamsPaths,const CallStateReason & reason)372 void CallContent::onStreamsRemoved(const ObjectPathList &streamsPaths,
373         const CallStateReason &reason)
374 {
375     foreach (const QDBusObjectPath &streamPath, streamsPaths) {
376         debug() << "Received Call::Content::StreamRemoved for stream" << streamPath.path();
377 
378         CallStreamPtr stream = mPriv->lookupStream(streamPath);
379         if (!stream) {
380             debug() << "Stream does not exist, ignoring";
381             return;
382         }
383 
384         bool incomplete = mPriv->incompleteStreams.contains(stream);
385         if (incomplete) {
386             mPriv->incompleteStreams.removeOne(stream);
387         } else {
388             mPriv->streams.removeOne(stream);
389         }
390 
391         if (isReady(FeatureCore) && !incomplete) {
392             emit streamRemoved(stream, reason);
393         }
394 
395         mPriv->checkIntrospectionCompleted();
396     }
397 }
398 
onStreamReady(PendingOperation * op)399 void CallContent::onStreamReady(PendingOperation *op)
400 {
401     PendingReady *pr = qobject_cast<PendingReady*>(op);
402     CallStreamPtr stream = CallStreamPtr::qObjectCast(pr->proxy());
403 
404     if (op->isError() || !mPriv->incompleteStreams.contains(stream)) {
405         mPriv->incompleteStreams.removeOne(stream);
406         mPriv->checkIntrospectionCompleted();
407         return;
408     }
409 
410     mPriv->incompleteStreams.removeOne(stream);
411     mPriv->streams.append(stream);
412 
413     if (isReady(FeatureCore)) {
414         emit streamAdded(stream);
415     }
416 
417     mPriv->checkIntrospectionCompleted();
418 }
419 
420 /**
421  * \fn void CallContent::streamAdded(const Tp::CallStreamPtr &stream);
422  *
423  * This signal is emitted when a new media stream is added to this media
424  * content.
425  *
426  * \param stream The media stream that was added.
427  * \sa streams()
428  */
429 
430 /**
431  * \fn void CallContent::streamRemoved(const Tp::CallStreamPtr &stream, const Tp::CallStateReason &reason);
432  *
433  * This signal is emitted when a new media stream is removed from this media
434  * content.
435  *
436  * \param stream The media stream that was removed.
437  * \param reason The reason for this removal.
438  * \sa streams()
439  */
440 
441 
442 /* ====== PendingCallContent ====== */
443 struct TP_QT_NO_EXPORT PendingCallContent::Private
444 {
PrivateTp::PendingCallContent::Private445     Private(PendingCallContent *parent, const CallChannelPtr &channel)
446         : parent(parent),
447           channel(channel)
448     {
449     }
450 
451     PendingCallContent *parent;
452     CallChannelPtr channel;
453     CallContentPtr content;
454 };
455 
PendingCallContent(const CallChannelPtr & channel,const QString & name,MediaStreamType type,MediaStreamDirection direction)456 PendingCallContent::PendingCallContent(const CallChannelPtr &channel,
457         const QString &name, MediaStreamType type, MediaStreamDirection direction)
458     : PendingOperation(channel),
459       mPriv(new Private(this, channel))
460 {
461     Client::ChannelTypeCallInterface *callInterface =
462         channel->interface<Client::ChannelTypeCallInterface>();
463     QDBusPendingCallWatcher *watcher =
464         new QDBusPendingCallWatcher(
465                 callInterface->AddContent(name, type, direction), this);
466     connect(watcher,
467             SIGNAL(finished(QDBusPendingCallWatcher*)),
468             SLOT(gotContent(QDBusPendingCallWatcher*)));
469 }
470 
~PendingCallContent()471 PendingCallContent::~PendingCallContent()
472 {
473     delete mPriv;
474 }
475 
content() const476 CallContentPtr PendingCallContent::content() const
477 {
478     if (!isFinished() || !isValid()) {
479         return CallContentPtr();
480     }
481 
482     return mPriv->content;
483 }
484 
gotContent(QDBusPendingCallWatcher * watcher)485 void PendingCallContent::gotContent(QDBusPendingCallWatcher *watcher)
486 {
487     QDBusPendingReply<QDBusObjectPath> reply = *watcher;
488     if (reply.isError()) {
489         warning().nospace() << "Call::AddContent failed with " <<
490             reply.error().name() << ": " << reply.error().message();
491         setFinishedWithError(reply.error());
492         watcher->deleteLater();
493         return;
494     }
495 
496     QDBusObjectPath contentPath = reply.value();
497     CallChannelPtr channel(mPriv->channel);
498     CallContentPtr content = channel->lookupContent(contentPath);
499     if (!content) {
500         content = channel->addContent(contentPath);
501     }
502 
503     connect(content->becomeReady(),
504             SIGNAL(finished(Tp::PendingOperation*)),
505             SLOT(onContentReady(Tp::PendingOperation*)));
506     connect(channel.data(),
507             SIGNAL(contentRemoved(Tp::CallContentPtr,Tp::CallStateReason)),
508             SLOT(onContentRemoved(Tp::CallContentPtr)));
509 
510     mPriv->content = content;
511 
512     watcher->deleteLater();
513 }
514 
onContentReady(PendingOperation * op)515 void PendingCallContent::onContentReady(PendingOperation *op)
516 {
517     if (op->isError()) {
518         setFinishedWithError(op->errorName(), op->errorMessage());
519         return;
520     }
521 
522     setFinished();
523 }
524 
onContentRemoved(const CallContentPtr & content)525 void PendingCallContent::onContentRemoved(const CallContentPtr &content)
526 {
527     if (isFinished()) {
528         return;
529     }
530 
531     if (mPriv->content == content) {
532         // the content was removed before becoming ready
533         setFinishedWithError(TP_QT_ERROR_CANCELLED,
534                 QLatin1String("Content removed before ready"));
535     }
536 }
537 
538 } // Tp
539