managerimpl.cpp 82.4 KB
Newer Older
jpbl's avatar
jpbl committed
1
/*
2
 *  Copyright (C) 2004-2012 Savoir-Faire Linux Inc.
3
 *  Author: Alexandre Bourget <alexandre.bourget@savoirfairelinux.com>
jpbl's avatar
jpbl committed
4
 *  Author: Yan Morin <yan.morin@savoirfairelinux.com>
5
 *  Author: Laurielle Lea <laurielle.lea@savoirfairelinux.com>
6
 *  Author: Emmanuel Milou <emmanuel.milou@savoirfairelinux.com>
7
 *  Author: Alexandre Savard <alexandre.savard@savoirfairelinux.com>
8
 *  Author: Guillaume Carmel-Archambault <guillaume.carmel-archambault@savoirfairelinux.com>
jpbl's avatar
jpbl committed
9
10
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
11
 *  the Free Software Foundation; either version 3 of the License, or
jpbl's avatar
jpbl committed
12
 *  (at your option) any later version.
13
 *
jpbl's avatar
jpbl committed
14
15
16
17
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
18
 *
jpbl's avatar
jpbl committed
19
20
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
21
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA.
22
23
24
25
26
27
28
29
30
31
32
 *
 *  Additional permission under GNU GPL version 3 section 7:
 *
 *  If you modify this program, or any covered work, by linking or
 *  combining it with the OpenSSL project's OpenSSL library (or a
 *  modified version of that library), containing parts covered by the
 *  terms of the OpenSSL or SSLeay licenses, Savoir-Faire Linux Inc.
 *  grants you additional permission to convey the resulting work.
 *  Corresponding Source for a non-source form of such a combination
 *  shall include the source code for the parts of OpenSSL used as well
 *  as that of the covered work.
jpbl's avatar
jpbl committed
33
34
 */

35
#ifdef HAVE_CONFIG_H
36
#include "config.h"
37
#endif
38

39
#include "logger.h"
40
#include "managerimpl.h"
41
#include "account_schema.h"
42

43
#include "global.h"
44
#include "fileutils.h"
45
#include "map_utils.h"
46
#include "sip/sipvoiplink.h"
47
#include "sip/sipaccount.h"
48
#include "sip/sipcall.h"
49
#include "im/instant_messaging.h"
50

51
#if HAVE_IAX
52
#include "iax/iaxaccount.h"
53
#include "iax/iaxcall.h"
54
#include "iax/iaxvoiplink.h"
55
#endif
56

57
#include "numbercleaner.h"
58
59
#include "config/yamlparser.h"
#include "config/yamlemitter.h"
60

Adrien Béraud's avatar
Adrien Béraud committed
61
#ifndef __ANDROID__
62
#include "audio/alsa/alsalayer.h"
63
#endif
64

65
#include "audio/sound/tonelist.h"
66
67
#include "audio/sound/audiofile.h"
#include "audio/sound/dtmf.h"
68
#include "history/historynamecache.h"
Emeric Vigier's avatar
Emeric Vigier committed
69
#include "history/history.h"
70
#include "manager.h"
71

72
#include "client/configurationmanager.h"
73
#include "client/callmanager.h"
74

75
#ifdef SFL_VIDEO
76
#include "client/video_controls.h"
77
#endif
78

79
#include "conference.h"
80
#include "scoped_lock.h"
81

82
#include <cerrno>
83
#include <algorithm>
84
#include <ctime>
jpbl's avatar
jpbl committed
85
86
#include <cstdlib>
#include <iostream>
87
#include <tr1/functional>
Tristan Matthews's avatar
cleanup    
Tristan Matthews committed
88
#include <iterator>
jpbl's avatar
jpbl committed
89
#include <fstream>
90
#include <sstream>
jpbl's avatar
jpbl committed
91
#include <sys/types.h> // mkdir(2)
92
#include <sys/stat.h>  // mkdir(2)
93

94
ManagerImpl::ManagerImpl() :
95
    preferences(), voipPreferences(),
96
    hookPreference(),  audioPreference(), shortcutPreferences(),
97
    hasTriedToRegister_(false), audioCodecFactory(), client_(), config_(),
98
99
    currentCallId_(), currentCallMutex_(), audiodriver_(0), dtmfKey_(),
    toneMutex_(), telephoneTone_(), audiofile_(), audioLayerMutex_(),
100
    waitingCalls_(), waitingCallsMutex_(), path_(),
101
    IPToIPMap_(), mainBuffer_(), conferenceMap_(), history_(), finished_(false)
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
102
{
103
104
105
    pthread_mutex_init(&currentCallMutex_, NULL);
    pthread_mutex_init(&toneMutex_, NULL);
    pthread_mutex_init(&audioLayerMutex_, NULL);
106
    pthread_mutex_init(&waitingCallsMutex_, NULL);
107
    // initialize random generator for call id
108
    srand(time(NULL));
jpbl's avatar
jpbl committed
109
110
}

111
112
113
ManagerImpl::~ManagerImpl()
{
    // destroy in reverse order of initialization
114
    pthread_mutex_destroy(&waitingCallsMutex_);
115
116
117
    pthread_mutex_destroy(&audioLayerMutex_);
    pthread_mutex_destroy(&toneMutex_);
    pthread_mutex_destroy(&currentCallMutex_);
118
119
}

120
121
122
123
124
125
126
127
128
129
130
namespace {
    // Creates a backup of the file at "path" with a .bak suffix appended
    void make_backup(const std::string &path)
    {
        const std::string backup_path(path + ".bak");
        std::ifstream src(path.c_str());
        std::ofstream dest(backup_path.c_str());
        dest << src.rdbuf();
        src.close();
        dest.close();
    }
131
132
}

133
void ManagerImpl::init(const std::string &config_file)
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
134
{
135
    path_ = config_file.empty() ? retrieveConfigPath() : config_file;
136
    DEBUG("Configuration file path: %s", path_.c_str());
137

138
139
    bool no_errors = true;

140
    try {
141
        FILE *file = fopen(path_.c_str(), "rb");
142

143
144
        if (file) {
            Conf::YamlParser parser(file);
145
146
147
            parser.serializeEvents();
            parser.composeEvents();
            parser.constructNativeData();
148
            const int error_count = loadAccountMap(parser);
149
            fclose(file);
150
            if (error_count > 0) {
151
152
                WARN("Errors while parsing %s", path_.c_str());
                no_errors = false;
153
            }
154
        } else {
155
156
157
            WARN("Config file not found: creating default account map");
            loadDefaultAccountMap();
        }
158
    } catch (const Conf::YamlParserException &e) {
159
        ERROR("%s", e.what());
160
        no_errors = false;
161
162
    }

163
164
165
166
    // always back up last error-free configuration
    if (no_errors)
        make_backup(path_);

167
    initAudioDriver();
168

Tristan Matthews's avatar
Tristan Matthews committed
169
    {
170
        sfl::ScopedLock lock(audioLayerMutex_);
Tristan Matthews's avatar
Tristan Matthews committed
171
        if (audiodriver_) {
172
            {
173
                sfl::ScopedLock toneLock(toneMutex_);
174
175
                telephoneTone_.reset(new TelephoneTone(preferences.getZoneToneChoice(), audiodriver_->getSampleRate()));
            }
176
            dtmfKey_.reset(new DTMF(getMainBuffer().getInternalSamplingRate()));
Tristan Matthews's avatar
Tristan Matthews committed
177
        }
178
    }
179

180
    history_.load(preferences.getHistoryLimit());
181
    registerAccounts();
jpbl's avatar
jpbl committed
182
183
}

Emeric Vigier's avatar
Emeric Vigier committed
184
185
186
187
void ManagerImpl::setPath(const std::string &path) {
	history_.setPath(path);
}

188
#if HAVE_DBUS
189
190
void ManagerImpl::run()
{
191
    DEBUG("Starting client event loop");
192
    client_.event_loop();
193
}
194
#endif
195
196
197

void ManagerImpl::finish()
{
198
199
200
201
202
203
204
205
    if (finished_)
        return;

    finished_ = true;
    // Unset signal handlers
    signal(SIGHUP, SIG_DFL);
    signal(SIGINT, SIG_DFL);
    signal(SIGTERM, SIG_DFL);
206

207
    std::vector<std::string> callList(getCallList());
208
    DEBUG("Hangup %zu remaining call(s)", callList.size());
209

210
    for (std::vector<std::string>::iterator iter = callList.begin();
211
            iter != callList.end(); ++iter)
212
        hangupCall(*iter);
213

214
215
    saveConfig();

216
    unregisterAllAccounts();
217

218
    SIPVoIPLink::destroy();
219
220
221
#if HAVE_IAX
    IAXVoIPLink::unloadAccountMap();
#endif
222

223
    {
224
        sfl::ScopedLock lock(audioLayerMutex_);
225
226
227
228

        delete audiodriver_;
        audiodriver_ = NULL;
    }
229

230
    client_.exit();
jpbl's avatar
jpbl committed
231
232
}

233
bool ManagerImpl::isCurrentCall(const std::string& callId) const
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
234
{
235
    return currentCallId_ == callId;
jpbl's avatar
jpbl committed
236
237
}

238
bool ManagerImpl::hasCurrentCall() const
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
239
{
240
    return not currentCallId_.empty();
jpbl's avatar
jpbl committed
241
242
}

243
std::string
244
ManagerImpl::getCurrentCallId() const
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
245
{
246
    return currentCallId_;
jpbl's avatar
jpbl committed
247
248
}

249
250
251
252
253
void ManagerImpl::unsetCurrentCall()
{
    switchCall("");
}

254
void ManagerImpl::switchCall(const std::string& id)
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
255
{
256
    sfl::ScopedLock m(currentCallMutex_);
257
    DEBUG("----- Switch current call id to %s -----", id.c_str());
258
    currentCallId_ = id;
jpbl's avatar
jpbl committed
259
260
261
262
263
}

///////////////////////////////////////////////////////////////////////////////
// Management of events' IP-phone user
///////////////////////////////////////////////////////////////////////////////
264
/* Main Thread */
Emmanuel Milou's avatar
Emmanuel Milou committed
265

266
bool ManagerImpl::outgoingCall(const std::string& account_id,
267
268
269
                               const std::string& call_id,
                               const std::string& to,
                               const std::string& conf_id)
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
270
{
271
    if (call_id.empty()) {
272
        DEBUG("New outgoing call abort, missing callid");
Emmanuel Lepage's avatar
Emmanuel Lepage committed
273
274
        return false;
    }
275

276
    // Call ID must be unique
277
    if (isValidCall(call_id)) {
278
        ERROR("Call id already exists in outgoing call");
279
280
281
        return false;
    }

282
    DEBUG("New outgoing call %s to %s", call_id.c_str(), to.c_str());
283

284
285
    stopTone();

286
    std::string current_call_id(getCurrentCallId());
287

288
    std::string prefix(hookPreference.getNumberAddPrefix());
289

290
    std::string to_cleaned(NumberCleaner::clean(to, prefix));
Emmanuel Milou's avatar
Emmanuel Milou committed
291

292
293
    // in any cases we have to detach from current communication
    if (hasCurrentCall()) {
294
        DEBUG("Has current call (%s) put it onhold", current_call_id.c_str());
295

296
        // if this is not a conference and this and is not a conference participant
297
        if (not isConference(current_call_id) and not isConferenceParticipant(current_call_id))
298
            onHoldCall(current_call_id);
299
        else if (isConference(current_call_id) and not isConferenceParticipant(call_id))
300
            detachParticipant(MainBuffer::DEFAULT_ID);
301
    }
302

303
    DEBUG("Selecting account %s", account_id.c_str());
304

305
    // fallback using the default sip account if the specied doesn't exist
Tristan Matthews's avatar
Tristan Matthews committed
306
    std::string use_account_id;
307
    if (!accountExists(account_id)) {
308
        WARN("Account does not exist, trying with default SIP account");
309
310
311
312
        use_account_id = SIPAccount::IP2IP_PROFILE;
    }
    else {
        use_account_id = account_id;
313
    }
314

315
    try {
316
        Call *call = getAccountLink(account_id)->newOutgoingCall(call_id, to_cleaned, use_account_id);
317
318

        // try to reverse match the peer name using the cache
Tristan Matthews's avatar
Tristan Matthews committed
319
        if (call->getDisplayName().empty()) {
320
            const std::string pseudo_contact_name(HistoryNameCache::getInstance().getNameFromHistory(call->getPeerNumber(), call->getAccountId()));
Tristan Matthews's avatar
Tristan Matthews committed
321
            if (not pseudo_contact_name.empty())
322
323
                call->setDisplayName(pseudo_contact_name);
        }
324
        switchCall(call_id);
325
        call->setConfId(conf_id);
326
    } catch (const VoipLinkException &e) {
327
        callFailure(call_id);
328
        ERROR("%s", e.what());
329
        return false;
330
331
332
333
    } catch (ost::Socket *) {
        callFailure(call_id);
        ERROR("Could not bind socket");
        return false;
334
335
    }

336
    getMainBuffer().dumpInfo();
Alexandre Savard's avatar
Alexandre Savard committed
337

338
    return true;
jpbl's avatar
jpbl committed
339
340
}

yanmorin's avatar
   
yanmorin committed
341
//THREAD=Main : for outgoing Call
342
bool ManagerImpl::answerCall(const std::string& call_id)
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
343
{
344
    bool result = true;
345
346
347
348
349
350
    Call *call = getCallFromCallID(call_id);

    if (call == NULL) {
        ERROR("Call %s is NULL", call_id.c_str());
        return false;
    }
351

352
    // If sflphone is ringing
353
    stopTone();
354

355
    // set playback mode to VOICE
356
    AudioLayer *al = getAudioDriver();
357
358
    if(al) al->setPlaybackMode(AudioLayer::VOICE);

359
    // store the current call id
360
    std::string current_call_id(getCurrentCallId());
361

362
363
    // in any cases we have to detach from current communication
    if (hasCurrentCall()) {
364

365
        DEBUG("Currently conversing with %s", current_call_id.c_str());
366

367
        if (not isConference(current_call_id) and not isConferenceParticipant(current_call_id)) {
368
            DEBUG("Answer call: Put the current call (%s) on hold", current_call_id.c_str());
369
            onHoldCall(current_call_id);
370
        } else if (isConference(current_call_id) and not isConferenceParticipant(call_id)) {
371
            // if we are talking to a conference and we are answering an incoming call
372
            DEBUG("Detach main participant from conference");
373
            detachParticipant(MainBuffer::DEFAULT_ID);
374
375
        }
    }
376

377
    try {
378
        VoIPLink *link = getAccountLink(call->getAccountId());
379
380
        if (link)
            link->answer(call);
381
    } catch (const std::runtime_error &e) {
382
        ERROR("%s", e.what());
383
        result = false;
384
    }
Alexandre Savard's avatar
Alexandre Savard committed
385

386
    // if it was waiting, it's waiting no more
387
    removeWaitingCall(call_id);
388

389
    // if we dragged this call into a conference already
390
    if (isConferenceParticipant(call_id))
391
        switchCall(call->getConfId());
392
    else
393
        switchCall(call_id);
394

395
    // Connect streams
396
    addStream(call_id);
397

398
    getMainBuffer().dumpInfo();
Alexandre Savard's avatar
Alexandre Savard committed
399

400
    // Start recording if set in preference
401
    if (audioPreference.getIsAlwaysRecording())
402
        toggleRecordingCall(call_id);
403

404
    client_.getCallManager()->callStateChanged(call_id, "CURRENT");
405

406
    return result;
jpbl's avatar
jpbl committed
407
408
}

409
410
411
412
413
414
415
void ManagerImpl::checkAudio()
{
    if (getCallList().empty()) {
        sfl::ScopedLock lock(audioLayerMutex_);
        if (audiodriver_)
            audiodriver_->stopStream();
    }
416

jpbl's avatar
jpbl committed
417
418
}

yanmorin's avatar
   
yanmorin committed
419
//THREAD=Main
420
bool ManagerImpl::hangupCall(const std::string& callId)
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
421
{
422
    // store the current call id
423
    std::string currentCallId(getCurrentCallId());
yanmorin's avatar
   
yanmorin committed
424

425
    stopTone();
426

427
    // set playback mode to NONE
428
    AudioLayer *al = getAudioDriver();
429
430
    if(al) al->setPlaybackMode(AudioLayer::NONE);

431
    DEBUG("Send call state change (HUNGUP) for id %s", callId.c_str());
432
    client_.getCallManager()->callStateChanged(callId, "HUNGUP");
433

434
    /* We often get here when the call was hungup before being created */
435
    if (not isValidCall(callId) and not isIPToIP(callId)) {
436
        DEBUG("Could not hang up call %s, call not valid", callId.c_str());
437
        checkAudio();
438
        return false;
439
440
    }

441
442
443
    // Disconnect streams
    removeStream(callId);

444
    if (isConferenceParticipant(callId)) {
445
        removeParticipant(callId);
446
    } else {
447
448
        // we are not participating in a conference, current call switched to ""
        if (not isConference(currentCallId))
449
            unsetCurrentCall();
450
    }
451

452
    if (isIPToIP(callId)) {
453
        /* Direct IP to IP call */
454
        try {
455
            Call * call = SIPVoIPLink::instance()->getSipCall(callId);
456
457
            if (call) {
                history_.addCall(call, preferences.getHistoryLimit());
458
                SIPVoIPLink::instance()->hangup(callId, 0);
459
                checkAudio();
460
461
                saveHistory();
            }
462
        } catch (const VoipLinkException &e) {
463
            ERROR("%s", e.what());
464
            return false;
465
        }
466
    } else {
467
        Call * call = getCallFromCallID(callId);
468
469
        if (call) {
            history_.addCall(call, preferences.getHistoryLimit());
470
            VoIPLink *link = getAccountLink(call->getAccountId());
471
            link->hangup(callId, 0);
472
            checkAudio();
473
474
            saveHistory();
        }
475
    }
476

477
    getMainBuffer().dumpInfo();
478
    return true;
jpbl's avatar
jpbl committed
479
480
}

481
bool ManagerImpl::hangupConference(const std::string& id)
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
482
{
483
    DEBUG("Hangup conference %s", id.c_str());
Alexandre Savard's avatar
Alexandre Savard committed
484

485
    ConferenceMap::iterator iter_conf = conferenceMap_.find(id);
Alexandre Savard's avatar
Alexandre Savard committed
486

487
    if (iter_conf != conferenceMap_.end()) {
488
        Conference *conf = iter_conf->second;
Alexandre Savard's avatar
Alexandre Savard committed
489

490
491
        if (conf) {
            ParticipantSet participants(conf->getParticipantList());
492

493
494
495
            for (ParticipantSet::const_iterator iter = participants.begin();
                    iter != participants.end(); ++iter)
                hangupCall(*iter);
496
        } else {
497
            ERROR("No such conference %s", id.c_str());
498
499
            return false;
        }
500
    }
Alexandre Savard's avatar
Alexandre Savard committed
501

502
    unsetCurrentCall();
503

504
    getMainBuffer().dumpInfo();
505

506
    return true;
Alexandre Savard's avatar
Alexandre Savard committed
507
508
}

jpbl's avatar
jpbl committed
509

yanmorin's avatar
   
yanmorin committed
510
//THREAD=Main
511
bool ManagerImpl::onHoldCall(const std::string& callId)
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
512
{
513
    bool result = true;
514

515
    stopTone();
516

517
    std::string current_call_id(getCurrentCallId());
518

519
    try {
520
        if (isIPToIP(callId)) {
521
            SIPVoIPLink::instance()->onhold(callId);
522
523
524
        } else {
            /* Classic call, attached to an account */
            std::string account_id(getAccountFromCall(callId));
525

526
            if (account_id.empty()) {
527
                DEBUG("Account ID %s or callid %s doesn't exist in call onHold", account_id.c_str(), callId.c_str());
528
                return false;
529
            }
530
531
532
533

            getAccountLink(account_id)->onhold(callId);
        }
    } catch (const VoipLinkException &e) {
534
        ERROR("%s", e.what());
535
        result = false;
536
    }
537

538
    // Unbind calls in main buffer
539
    removeStream(callId);
540

541
    // Remove call from teh queue if it was still there
542
    removeWaitingCall(callId);
543

544
    // keeps current call id if the action is not holding this call or a new outgoing call
545
    // this could happen in case of a conference
546
    if (current_call_id == callId)
547
        unsetCurrentCall();
548

549
    client_.getCallManager()->callStateChanged(callId, "HOLD");
550

551
    getMainBuffer().dumpInfo();
552
    return result;
yanmorin's avatar
   
yanmorin committed
553
554
555
}

//THREAD=Main
556
bool ManagerImpl::offHoldCall(const std::string& callId)
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
557
{
558
    bool result = true;
559

560
    stopTone();
561

Tristan Matthews's avatar
Tristan Matthews committed
562
    const std::string currentCallId(getCurrentCallId());
563

Tristan Matthews's avatar
Tristan Matthews committed
564
    // Place current call on hold if it isn't
565
    if (hasCurrentCall()) {
Tristan Matthews's avatar
Tristan Matthews committed
566
        if (not isConference(currentCallId) and not isConferenceParticipant(currentCallId)) {
567
            DEBUG("Has current call (%s), put on hold", currentCallId.c_str());
568
            onHoldCall(currentCallId);
569
570
        } else if (isConference(currentCallId) && callId != currentCallId) {
            holdConference(currentCallId);
571
        } else if (isConference(currentCallId) and not isConferenceParticipant(callId))
572
            detachParticipant(MainBuffer::DEFAULT_ID);
573
    }
alexandresavard's avatar
alexandresavard committed
574

575
    if (isIPToIP(callId))
Tristan Matthews's avatar
Tristan Matthews committed
576
        SIPVoIPLink::instance()->offhold(callId);
577
    else {
578
        /* Classic call, attached to an account */
579
        Call * call = getCallFromCallID(callId);
580

581
        if (call)
582
            getAccountLink(call->getAccountId())->offhold(callId);
583
584
        else
            result = false;
585
    }
586

587
    client_.getCallManager()->callStateChanged(callId, "UNHOLD");
588

589
    if (isConferenceParticipant(callId)) {
590
        Call *call = getCallFromCallID(callId);
591
        if (call)
592
            switchCall(call->getConfId());
593
594
        else
            result = false;
595
    } else
596
        switchCall(callId);
597

598
599
    addStream(callId);

600
    getMainBuffer().dumpInfo();
601
    return result;
jpbl's avatar
jpbl committed
602
603
}

yanmorin's avatar
   
yanmorin committed
604
//THREAD=Main
605
bool ManagerImpl::transferCall(const std::string& callId, const std::string& to)
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
606
{
607
    if (isConferenceParticipant(callId)) {
608
        removeParticipant(callId);
Tristan Matthews's avatar
cleanup    
Tristan Matthews committed
609
    } else if (not isConference(getCurrentCallId()))
610
        unsetCurrentCall();
611

612
    // Direct IP to IP call
613
    if (isIPToIP(callId)) {
614
        SIPVoIPLink::instance()->transfer(callId, to);
615
616
    } else {
        std::string accountID(getAccountFromCall(callId));
617

618
        if (accountID.empty())
619
            return false;
620

621
622
        VoIPLink *link = getAccountLink(accountID);
        link->transfer(callId, to);
623
    }
624

625
    // remove waiting call in case we make transfer without even answer
626
    removeWaitingCall(callId);
627

628
    getMainBuffer().dumpInfo();
Alexandre Savard's avatar
Alexandre Savard committed
629

630
    return true;
jpbl's avatar
jpbl committed
631
632
}

633
void ManagerImpl::transferFailed()
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
634
{
635
    client_.getCallManager()->transferFailed();
636
637
}

638
void ManagerImpl::transferSucceeded()
Emmanuel Milou's avatar
[#2402]    
Emmanuel Milou committed
639
{
640
    client_.getCallManager()->transferSucceeded();
641
642
}

643
bool ManagerImpl::attendedTransfer(const std::string& transferID, const std::string& targetID)
644
{
645
    if (isIPToIP(transferID))
Rafaël Carré's avatar
Rafaël Carré committed
646
        return SIPVoIPLink::instance()->attendedTransfer(transferID, targetID);
Alexandre Savard's avatar