LCOV - code coverage report
Current view: top level - foo/src - manager.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 882 1744 50.6 %
Date: 2026-04-01 09:29:43 Functions: 188 466 40.3 %

          Line data    Source code
       1             : /*
       2             :  * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
       3             :  *
       4             :  * This program is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 3 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * This program is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with this program. If not, see <https://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #ifdef HAVE_CONFIG_H
      19             : #include "config.h"
      20             : #endif
      21             : 
      22             : #include "manager.h"
      23             : 
      24             : #include "logger.h"
      25             : #include "account_schema.h"
      26             : 
      27             : #include "fileutils.h"
      28             : #include "gittransport.h"
      29             : #include "jami.h"
      30             : #include "media_attribute.h"
      31             : #include "account.h"
      32             : #include "string_utils.h"
      33             : #include "jamidht/jamiaccount.h"
      34             : #include "account.h"
      35             : #include <opendht/rng.h>
      36             : 
      37             : #include "call_factory.h"
      38             : 
      39             : #include "sip/sipvoiplink.h"
      40             : #include "sip/sipaccount_config.h"
      41             : 
      42             : #include "im/instant_messaging.h"
      43             : 
      44             : #include "config/yamlparser.h"
      45             : 
      46             : #if HAVE_ALSA
      47             : #include "audio/alsa/alsalayer.h"
      48             : #endif
      49             : 
      50             : #include "audio/sound/dtmf.h"
      51             : #include "audio/ringbufferpool.h"
      52             : 
      53             : #ifdef ENABLE_PLUGIN
      54             : #include "plugin/jamipluginmanager.h"
      55             : #include "plugin/streamdata.h"
      56             : #endif
      57             : 
      58             : #include "client/videomanager.h"
      59             : 
      60             : #include "conference.h"
      61             : 
      62             : #include "client/jami_signal.h"
      63             : #include "jami/call_const.h"
      64             : 
      65             : #include "libav_utils.h"
      66             : #ifdef ENABLE_VIDEO
      67             : #include "video/sinkclient.h"
      68             : #include "video/video_base.h"
      69             : #include "media/video/video_mixer.h"
      70             : #endif
      71             : #include "audio/tonecontrol.h"
      72             : 
      73             : #include <dhtnet/ice_transport_factory.h>
      74             : #include <dhtnet/ice_transport.h>
      75             : #include <dhtnet/upnp/upnp_context.h>
      76             : 
      77             : #include <libavutil/ffversion.h>
      78             : 
      79             : #include <opendht/thread_pool.h>
      80             : 
      81             : #include <asio/io_context.hpp>
      82             : #include <asio/executor_work_guard.hpp>
      83             : 
      84             : #include <git2.h>
      85             : 
      86             : #ifndef WIN32
      87             : #include <sys/time.h>
      88             : #include <sys/resource.h>
      89             : #endif
      90             : 
      91             : #ifdef TARGET_OS_IOS
      92             : #include <CoreFoundation/CoreFoundation.h>
      93             : #endif
      94             : 
      95             : #include <cerrno>
      96             : #include <ctime>
      97             : #include <cstdlib>
      98             : #include <iostream>
      99             : #include <fstream>
     100             : #include <algorithm>
     101             : #include <memory>
     102             : #include <mutex>
     103             : #include <list>
     104             : #include <random>
     105             : 
     106             : #ifndef JAMI_DATADIR
     107             : #error "Define the JAMI_DATADIR macro as the data installation prefix of the package"
     108             : #endif
     109             : 
     110             : namespace jami {
     111             : 
     112             : /** To store uniquely a list of Call ids */
     113             : using CallIDSet = std::set<std::string>;
     114             : 
     115             : static constexpr const char* PACKAGE_OLD = "ring";
     116             : 
     117             : std::atomic_bool Manager::initialized = {false};
     118             : 
     119             : #if TARGET_OS_IOS
     120             : bool Manager::isIOSExtension = {false};
     121             : #endif
     122             : 
     123             : bool Manager::syncOnRegister = {true};
     124             : 
     125             : bool Manager::autoLoad = {true};
     126             : 
     127             : static void
     128          32 : copy_over(const std::filesystem::path& srcPath, const std::filesystem::path& destPath)
     129             : {
     130          32 :     std::ifstream src(srcPath);
     131          32 :     std::ofstream dest(destPath);
     132          32 :     dest << src.rdbuf();
     133          32 :     src.close();
     134          32 :     dest.close();
     135          32 : }
     136             : 
     137             : // Creates a backup of the file at "path" with a .bak suffix appended
     138             : static void
     139          29 : make_backup(const std::filesystem::path& path)
     140             : {
     141          29 :     auto backup_path = path;
     142          29 :     backup_path.replace_extension(".bak");
     143          29 :     copy_over(path, backup_path);
     144          29 : }
     145             : 
     146             : // Restore last backup of the configuration file
     147             : static void
     148           3 : restore_backup(const std::filesystem::path& path)
     149             : {
     150           3 :     auto backup_path = path;
     151           3 :     backup_path.replace_extension(".bak");
     152           3 :     copy_over(backup_path, path);
     153           3 : }
     154             : 
     155             : static void
     156          96 : check_rename(const std::filesystem::path& old_dir, const std::filesystem::path& new_dir)
     157             : {
     158          96 :     if (old_dir == new_dir or not std::filesystem::is_directory(old_dir))
     159          64 :         return;
     160             : 
     161          32 :     std::error_code ec;
     162          32 :     if (not std::filesystem::is_directory(new_dir)) {
     163           0 :         JAMI_WARNING("Migrating {} to {}", old_dir, new_dir);
     164           0 :         std::filesystem::rename(old_dir, new_dir, ec);
     165           0 :         if (ec)
     166           0 :             JAMI_ERROR("Failed to rename {} to {}: {}", old_dir, new_dir, ec.message());
     167             :     } else {
     168          96 :         for (const auto& file_iterator : std::filesystem::directory_iterator(old_dir, ec)) {
     169           0 :             const auto& file_path = file_iterator.path();
     170           0 :             auto new_path = new_dir / file_path.filename();
     171           0 :             if (file_iterator.is_directory() and std::filesystem::is_directory(new_path)) {
     172           0 :                 check_rename(file_path, new_path);
     173             :             } else {
     174           0 :                 JAMI_WARNING("Migrating {} to {}", old_dir, new_path);
     175           0 :                 std::filesystem::rename(file_path, new_path, ec);
     176           0 :                 if (ec)
     177           0 :                     JAMI_ERROR("Failed to rename {} to {}: {}", file_path, new_path, ec.message());
     178             :             }
     179          32 :         }
     180          32 :         std::filesystem::remove_all(old_dir, ec);
     181             :     }
     182             : }
     183             : 
     184             : /**
     185             :  * Set OpenDHT's log level based on the JAMI_LOG_DHT environment variable.
     186             :  * JAMI_LOG_DHT = 0 minimum logging (=disable)
     187             :  * JAMI_LOG_DHT = 1 logging enabled
     188             :  */
     189             : static unsigned
     190          32 : getDhtLogLevel()
     191             : {
     192          32 :     if (auto* envvar = getenv("JAMI_LOG_DHT")) {
     193           0 :         return std::clamp(to_int<unsigned>(envvar, 0), 0u, 1u);
     194             :     }
     195          32 :     return 0;
     196             : }
     197             : 
     198             : static unsigned
     199          32 : getDhtnetLogLevel()
     200             : {
     201          32 :     if (auto* envvar = getenv("JAMI_LOG_DHTNET")) {
     202           0 :         return std::clamp(to_int<unsigned>(envvar, 0), 0u, 1u);
     203             :     }
     204          32 :     return 0;
     205             : }
     206             : 
     207             : /**
     208             :  * Set pjsip's log level based on the JAMI_LOG_SIP environment variable.
     209             :  * JAMI_LOG_SIP = 0 minimum logging
     210             :  * JAMI_LOG_SIP = 6 maximum logging
     211             :  */
     212             : static void
     213          32 : setSipLogLevel()
     214             : {
     215          32 :     int level = 0;
     216          32 :     if (auto* envvar = getenv("JAMI_LOG_SIP")) {
     217           0 :         level = std::clamp(to_int<int>(envvar, 0), 0, 6);
     218             :     }
     219             : 
     220          32 :     pj_log_set_level(level);
     221          32 :     pj_log_set_log_func([](int level, const char* data, int len) {
     222           0 :         auto msg = std::string_view(data, len);
     223           0 :         if (level < 2)
     224           0 :             JAMI_XERR("{}", msg);
     225           0 :         else if (level < 4)
     226           0 :             JAMI_XWARN("{}", msg);
     227             :         else
     228           0 :             JAMI_XDBG("{}", msg);
     229           0 :     });
     230          32 : }
     231             : 
     232             : /**
     233             :  * Set gnutls's log level based on the JAMI_LOG_TLS environment variable.
     234             :  * JAMI_LOG_TLS = 0 minimum logging (default)
     235             :  * JAMI_LOG_TLS = 9 maximum logging
     236             :  */
     237             : static void
     238          32 : setGnuTlsLogLevel()
     239             : {
     240          32 :     int level = 0;
     241          32 :     if (auto* envvar = getenv("JAMI_LOG_TLS")) {
     242           0 :         level = to_int<int>(envvar, 0);
     243           0 :         level = std::clamp(level, 0, 9);
     244             :     }
     245             : 
     246          32 :     gnutls_global_set_log_level(level);
     247          44 :     gnutls_global_set_log_function([](int level, const char* msg) { JAMI_XDBG("[{:d}]GnuTLS: {:s}", level, msg); });
     248          32 : }
     249             : 
     250             : //==============================================================================
     251             : 
     252             : struct Manager::ManagerPimpl
     253             : {
     254             :     explicit ManagerPimpl(Manager& base);
     255             : 
     256             :     bool parseConfiguration();
     257             : 
     258             :     /*
     259             :      * Play one tone
     260             :      * @return false if the driver is uninitialize
     261             :      */
     262             :     void playATone(Tone::ToneId toneId);
     263             : 
     264             :     int getCurrentDeviceIndex(AudioDeviceType type);
     265             : 
     266             :     /**
     267             :      * Process remaining participant given a conference and the current call ID.
     268             :      * Mainly called when a participant is detached or call ended (hang up).
     269             :      * @param current call id
     270             :      * @param conference pointer
     271             :      */
     272             :     void processRemainingParticipants(Conference& conf);
     273             : 
     274             :     /**
     275             :      * Create config directory in home user and return configuration file path
     276             :      */
     277             :     std::filesystem::path retrieveConfigPath() const;
     278             : 
     279             :     void unsetCurrentCall();
     280             : 
     281             :     void switchCall(const std::string& id);
     282             : 
     283             :     /**
     284             :      * Add incoming callid to the waiting list
     285             :      * @param id std::string to add
     286             :      */
     287             :     void addWaitingCall(const std::string& id);
     288             : 
     289             :     /**
     290             :      * Remove incoming callid to the waiting list
     291             :      * @param id std::string to remove
     292             :      */
     293             :     void removeWaitingCall(const std::string& id);
     294             : 
     295             :     void loadAccount(const YAML::Node& item, int& errorCount);
     296             :     void cleanupAccountStorage(const std::string& accountId);
     297             : 
     298             :     void sendTextMessageToConference(const Conference& conf,
     299             :                                      const std::map<std::string, std::string>& messages,
     300             :                                      const std::string& from) const noexcept;
     301             : 
     302             :     void bindCallToConference(Call& call, Conference& conf);
     303             : 
     304             :     void addMainParticipant(Conference& conf);
     305             : 
     306             :     bool hangupConference(Conference& conf);
     307             : 
     308             :     template<class T>
     309             :     std::shared_ptr<T> findAccount(const std::function<bool(const std::shared_ptr<T>&)>&);
     310             : 
     311             :     void initAudioDriver();
     312             : 
     313             :     void processIncomingCall(const std::string& accountId, Call& incomCall);
     314             :     static void stripSipPrefix(Call& incomCall);
     315             : 
     316             :     Manager& base_; // pimpl back-pointer
     317             : 
     318             :     std::shared_ptr<asio::io_context> ioContext_;
     319             :     std::thread ioContextRunner_;
     320             : 
     321             :     std::shared_ptr<dhtnet::upnp::UPnPContext> upnpContext_;
     322             : 
     323             :     std::atomic_bool autoAnswer_ {false};
     324             : 
     325             :     /** Application wide tone controller */
     326             :     ToneControl toneCtrl_;
     327             :     std::unique_ptr<AudioDeviceGuard> toneDeviceGuard_;
     328             : 
     329             :     /** Current Call ID */
     330             :     std::string currentCall_;
     331             : 
     332             :     /** Protected current call access */
     333             :     std::mutex currentCallMutex_;
     334             : 
     335             :     /** Protected sinks access */
     336             :     std::mutex sinksMutex_;
     337             : 
     338             :     /** Audio layer */
     339             :     std::shared_ptr<AudioLayer> audiodriver_ {nullptr};
     340             :     std::array<std::atomic_uint, 3> audioStreamUsers_ {};
     341             : 
     342             :     /* Audio device users */
     343             :     std::mutex audioDeviceUsersMutex_ {};
     344             :     std::map<std::string, unsigned> audioDeviceUsers_ {};
     345             : 
     346             :     // Main thread
     347             :     std::unique_ptr<DTMF> dtmfKey_;
     348             : 
     349             :     /** Buffer to generate DTMF */
     350             :     std::shared_ptr<AudioFrame> dtmfBuf_;
     351             : 
     352             :     std::shared_ptr<asio::steady_timer> dtmfTimer_;
     353             : 
     354             :     // To handle volume control
     355             :     // short speakerVolume_;
     356             :     // short micVolume_;
     357             :     // End of sound variable
     358             : 
     359             :     /**
     360             :      * Mutex used to protect audio layer
     361             :      */
     362             :     std::mutex audioLayerMutex_;
     363             : 
     364             :     /**
     365             :      * Waiting Call Vectors
     366             :      */
     367             :     CallIDSet waitingCalls_;
     368             : 
     369             :     /**
     370             :      * Protect waiting call list, access by many VoIP/audio threads
     371             :      */
     372             :     std::mutex waitingCallsMutex_;
     373             : 
     374             :     /**
     375             :      * Path of the ConfigFile
     376             :      */
     377             :     std::filesystem::path path_;
     378             : 
     379             :     /**
     380             :      * Instance of the RingBufferPool for the whole application
     381             :      *
     382             :      * In order to send signal to other parts of the application, one must pass through the
     383             :      * RingBufferMananger. Audio instances must be registered into the RingBufferMananger and bound
     384             :      * together via the Manager.
     385             :      *
     386             :      */
     387             :     std::unique_ptr<RingBufferPool> ringbufferpool_;
     388             : 
     389             :     std::atomic_bool finished_ {false};
     390             : 
     391             :     /* ICE support */
     392             :     std::shared_ptr<dhtnet::IceTransportFactory> ice_tf_;
     393             : 
     394             :     /* Sink ID mapping */
     395             :     std::map<std::string, std::weak_ptr<video::SinkClient>> sinkMap_;
     396             : 
     397             :     std::unique_ptr<VideoManager> videoManager_;
     398             : 
     399             :     std::unique_ptr<SIPVoIPLink> sipLink_;
     400             : #ifdef ENABLE_PLUGIN
     401             :     /* Jami Plugin Manager */
     402             :     std::unique_ptr<JamiPluginManager> jami_plugin_manager;
     403             : #endif
     404             : 
     405             :     std::mutex gitTransportsMtx_ {};
     406             :     std::map<git_smart_subtransport*, std::unique_ptr<P2PSubTransport>> gitTransports_ {};
     407             : };
     408             : 
     409          38 : Manager::ManagerPimpl::ManagerPimpl(Manager& base)
     410          38 :     : base_(base)
     411          38 :     , ioContext_(std::make_shared<asio::io_context>())
     412          38 :     , upnpContext_(std::make_shared<dhtnet::upnp::UPnPContext>(nullptr, Logger::dhtLogger()))
     413          38 :     , toneCtrl_(base.preferences)
     414          38 :     , dtmfBuf_(std::make_shared<AudioFrame>())
     415          38 :     , ringbufferpool_(new RingBufferPool)
     416             : #ifdef ENABLE_VIDEO
     417         190 :     , videoManager_(nullptr)
     418             : #endif
     419             : {
     420          38 :     jami::libav_utils::av_init();
     421          38 : }
     422             : 
     423             : bool
     424          35 : Manager::ManagerPimpl::parseConfiguration()
     425             : {
     426          35 :     bool result = true;
     427             : 
     428             :     try {
     429          35 :         std::ifstream file(path_);
     430          35 :         YAML::Node parsedFile = YAML::Load(file);
     431          35 :         file.close();
     432          35 :         const int error_count = base_.loadAccountMap(parsedFile);
     433             : 
     434          35 :         if (error_count > 0) {
     435          24 :             JAMI_WARNING("[config] Error while parsing {}", path_);
     436           6 :             result = false;
     437             :         }
     438          35 :     } catch (const YAML::BadFile& e) {
     439           0 :         JAMI_WARNING("[config] Unable to open configuration file");
     440           0 :         result = false;
     441           0 :     }
     442             : 
     443          35 :     return result;
     444             : }
     445             : 
     446             : /**
     447             :  * Multi Thread
     448             :  */
     449             : void
     450         114 : Manager::ManagerPimpl::playATone(Tone::ToneId toneId)
     451             : {
     452         114 :     if (not base_.voipPreferences.getPlayTones())
     453           0 :         return;
     454             : 
     455         114 :     std::lock_guard lock(audioLayerMutex_);
     456         114 :     if (not audiodriver_) {
     457           0 :         JAMI_ERROR("[audio] Uninitialized audio layer");
     458           0 :         return;
     459             :     }
     460             : 
     461         114 :     auto oldGuard = std::move(toneDeviceGuard_);
     462         114 :     toneDeviceGuard_ = base_.startAudioStream(AudioDeviceType::PLAYBACK);
     463         114 :     audiodriver_->flushUrgent();
     464         114 :     toneCtrl_.play(toneId);
     465         114 : }
     466             : 
     467             : int
     468           0 : Manager::ManagerPimpl::getCurrentDeviceIndex(AudioDeviceType type)
     469             : {
     470           0 :     if (not audiodriver_)
     471           0 :         return -1;
     472           0 :     switch (type) {
     473           0 :     case AudioDeviceType::PLAYBACK:
     474           0 :         return audiodriver_->getIndexPlayback();
     475           0 :     case AudioDeviceType::RINGTONE:
     476           0 :         return audiodriver_->getIndexRingtone();
     477           0 :     case AudioDeviceType::CAPTURE:
     478           0 :         return audiodriver_->getIndexCapture();
     479           0 :     default:
     480           0 :         return -1;
     481             :     }
     482             : }
     483             : 
     484             : void
     485          63 : Manager::ManagerPimpl::processRemainingParticipants(Conference& conf)
     486             : {
     487          63 :     const std::string currentCallId(base_.getCurrentCallId());
     488          63 :     CallIdSet subcalls(conf.getSubCalls());
     489          63 :     const size_t n = subcalls.size();
     490         252 :     JAMI_DEBUG("[conf:{}] Processing {} remaining participant(s)", conf.getConfId(), conf.getConferenceInfos().size());
     491             : 
     492          63 :     if (n > 1) {
     493             :         // Reset ringbuffer's readpointers
     494          27 :         for (const auto& p : subcalls) {
     495          18 :             if (auto call = base_.getCallFromCallID(p)) {
     496          18 :                 auto medias = call->getAudioStreams();
     497          36 :                 for (const auto& media : medias) {
     498          72 :                     JAMI_DEBUG("[call:{}] Remove local audio {}", p, media.first);
     499          18 :                     base_.getRingBufferPool().flush(media.first);
     500             :                 }
     501          36 :             }
     502             :         }
     503             : 
     504           9 :         base_.getRingBufferPool().flush(RingBufferPool::DEFAULT_ID);
     505             :     } else {
     506         108 :         if (auto acc = std::dynamic_pointer_cast<JamiAccount>(conf.getAccount())) {
     507             :             // Stay in a conference if 1 participants for swarm and rendezvous
     508          54 :             if (auto* cm = acc->convModule(true)) {
     509          54 :                 if (acc->isRendezVous() || cm->isHosting("", conf.getConfId())) {
     510             :                     // Check if attached
     511          10 :                     if (conf.getState() == Conference::State::ACTIVE_ATTACHED) {
     512           3 :                         return;
     513             :                     }
     514             :                 }
     515             :             }
     516          54 :         }
     517          51 :         if (n == 1) {
     518             :             // this call is the last participant (non swarm-call), hence
     519             :             // the conference is over
     520          28 :             auto p = subcalls.begin();
     521          28 :             if (auto call = base_.getCallFromCallID(*p)) {
     522             :                 // if we are not listening to this conference and not a rendez-vous
     523          28 :                 auto w = call->getAccount();
     524          28 :                 auto account = w.lock();
     525          28 :                 if (!account) {
     526           0 :                     JAMI_ERROR("[conf:{}] Account no longer available", conf.getConfId());
     527           0 :                     return;
     528             :                 }
     529          28 :                 if (currentCallId != conf.getConfId())
     530           5 :                     base_.holdCall(account->getAccountID(), call->getCallId());
     531             :                 else
     532          23 :                     switchCall(call->getCallId());
     533          56 :             }
     534             : 
     535         112 :             JAMI_DEBUG("[conf:{}] Only one participant left, removing conference", conf.getConfId());
     536          28 :             if (auto account = conf.getAccount())
     537          28 :                 account->removeConference(conf.getConfId());
     538             :         } else {
     539          92 :             JAMI_DEBUG("[conf:{}] No remaining participants, removing conference", conf.getConfId());
     540          23 :             if (auto account = conf.getAccount())
     541          23 :                 account->removeConference(conf.getConfId());
     542          23 :             unsetCurrentCall();
     543             :         }
     544             :     }
     545          66 : }
     546             : 
     547             : /**
     548             :  * Initialization: Main Thread
     549             :  */
     550             : std::filesystem::path
     551           0 : Manager::ManagerPimpl::retrieveConfigPath() const
     552             : {
     553             :     // TODO: Migrate config filename from dring.yml to jami.yml.
     554           0 :     return fileutils::get_config_dir() / "dring.yml";
     555             : }
     556             : 
     557             : void
     558         104 : Manager::ManagerPimpl::unsetCurrentCall()
     559             : {
     560         104 :     currentCall_ = "";
     561         104 : }
     562             : 
     563             : void
     564         260 : Manager::ManagerPimpl::switchCall(const std::string& id)
     565             : {
     566         260 :     std::lock_guard m(currentCallMutex_);
     567         260 :     JAMI_DBG("----- Switch current call ID to '%s' -----", not id.empty() ? id.c_str() : "none");
     568         260 :     currentCall_ = id;
     569         260 : }
     570             : 
     571             : void
     572          90 : Manager::ManagerPimpl::addWaitingCall(const std::string& id)
     573             : {
     574          90 :     std::lock_guard m(waitingCallsMutex_);
     575             :     // Enable incoming call beep if needed.
     576          90 :     if (audiodriver_ and waitingCalls_.empty() and not currentCall_.empty())
     577          44 :         audiodriver_->playIncomingCallNotification(true);
     578          90 :     waitingCalls_.insert(id);
     579          90 : }
     580             : 
     581             : void
     582         408 : Manager::ManagerPimpl::removeWaitingCall(const std::string& id)
     583             : {
     584         408 :     std::lock_guard m(waitingCallsMutex_);
     585         408 :     waitingCalls_.erase(id);
     586         408 :     if (audiodriver_ and waitingCalls_.empty())
     587         192 :         audiodriver_->playIncomingCallNotification(false);
     588         408 : }
     589             : 
     590             : void
     591           0 : Manager::ManagerPimpl::loadAccount(const YAML::Node& node, int& errorCount)
     592             : {
     593             :     using yaml_utils::parseValue;
     594             :     using yaml_utils::parseValueOptional;
     595             : 
     596           0 :     std::string accountid;
     597           0 :     parseValue(node, "id", accountid);
     598             : 
     599           0 :     std::string accountType(ACCOUNT_TYPE_SIP);
     600           0 :     parseValueOptional(node, "type", accountType);
     601             : 
     602           0 :     if (accountid.empty())
     603           0 :         return;
     604             : 
     605           0 :     if (base_.preferences.isAccountPending(accountid)) {
     606           0 :         JAMI_INFO("[account:%s] Removing pending account from disk", accountid.c_str());
     607           0 :         base_.removeAccount(accountid, true);
     608           0 :         cleanupAccountStorage(accountid);
     609           0 :         return;
     610             :     }
     611             : 
     612           0 :     if (auto a = base_.accountFactory.createAccount(accountType, accountid)) {
     613           0 :         auto config = a->buildConfig();
     614           0 :         config->unserialize(node);
     615           0 :         a->setConfig(std::move(config));
     616           0 :         return;
     617           0 :     }
     618             : 
     619           0 :     JAMI_ERROR("Failed to create account of type \"{:s}\"", accountType);
     620           0 :     ++errorCount;
     621           0 : }
     622             : 
     623             : void
     624           0 : Manager::ManagerPimpl::cleanupAccountStorage(const std::string& accountId)
     625             : {
     626           0 :     const auto cachePath = fileutils::get_cache_dir() / accountId;
     627           0 :     const auto dataPath = cachePath / "values";
     628           0 :     const auto idPath = fileutils::get_data_dir() / accountId;
     629           0 :     dhtnet::fileutils::removeAll(dataPath);
     630           0 :     dhtnet::fileutils::removeAll(cachePath);
     631           0 :     dhtnet::fileutils::removeAll(idPath, true);
     632           0 : }
     633             : 
     634             : // THREAD=VoIP
     635             : void
     636           0 : Manager::ManagerPimpl::sendTextMessageToConference(const Conference& conf,
     637             :                                                    const std::map<std::string, std::string>& messages,
     638             :                                                    const std::string& from) const noexcept
     639             : {
     640           0 :     CallIdSet subcalls(conf.getSubCalls());
     641           0 :     for (const auto& callId : subcalls) {
     642             :         try {
     643           0 :             auto call = base_.getCallFromCallID(callId);
     644           0 :             if (not call)
     645           0 :                 throw std::runtime_error("No associated call");
     646           0 :             call->sendTextMessage(messages, from);
     647           0 :         } catch (const std::exception& e) {
     648           0 :             JAMI_ERROR("[conf:{}] Failed to send message to participant {}: {}", conf.getConfId(), callId, e.what());
     649           0 :         }
     650             :     }
     651           0 : }
     652             : 
     653             : void
     654           0 : Manager::bindCallToConference(Call& call, Conference& conf)
     655             : {
     656           0 :     pimpl_->bindCallToConference(call, conf);
     657           0 : }
     658             : 
     659             : void
     660          57 : Manager::ManagerPimpl::bindCallToConference(Call& call, Conference& conf)
     661             : {
     662          57 :     const auto& callId = call.getCallId();
     663          57 :     const auto& confId = conf.getConfId();
     664          57 :     const auto& state = call.getStateStr();
     665             : 
     666             :     // ensure that calls are only in one conference at a time
     667          57 :     if (call.isConferenceParticipant())
     668           0 :         base_.detachParticipant(callId);
     669             : 
     670         228 :     JAMI_DEBUG("[call:{}] Bind to conference {} (callState={})", callId, confId, state);
     671             : 
     672          57 :     auto medias = call.getAudioStreams();
     673         114 :     for (const auto& media : medias) {
     674         228 :         JAMI_DEBUG("[call:{}] Remove local audio {}", callId, media.first);
     675          57 :         base_.getRingBufferPool().unBindAll(media.first);
     676             :     }
     677             : 
     678          57 :     conf.addSubCall(callId);
     679             : 
     680          57 :     if (state == "HOLD") {
     681           0 :         base_.resumeCall(call.getAccountId(), callId);
     682          57 :     } else if (state == "INCOMING") {
     683           0 :         base_.acceptCall(call);
     684          57 :     } else if (state == "CURRENT") {
     685           0 :     } else if (state == "INACTIVE") {
     686           0 :         base_.acceptCall(call);
     687             :     } else
     688           0 :         JAMI_WARNING("[call:{}] Call state {} unrecognized for conference", callId, state);
     689          57 : }
     690             : 
     691             : //==============================================================================
     692             : 
     693             : Manager&
     694      113054 : Manager::instance()
     695             : {
     696             :     // Meyers singleton
     697      113054 :     static Manager instance;
     698             : 
     699             :     // This will give a warning that can be ignored the first time instance()
     700             :     // is called… subsequent warnings are more serious
     701      113054 :     if (not Manager::initialized)
     702         529 :         JAMI_WARNING("Manager accessed before initialization");
     703             : 
     704      113049 :     return instance;
     705             : }
     706             : 
     707          38 : Manager::Manager()
     708          38 :     : rand_(dht::crypto::getSeededRandomEngine<std::mt19937_64>())
     709          38 :     , preferences()
     710          38 :     , voipPreferences()
     711          38 :     , audioPreference()
     712             : #ifdef ENABLE_PLUGIN
     713          38 :     , pluginPreferences()
     714             : #endif
     715             : #ifdef ENABLE_VIDEO
     716          38 :     , videoPreferences()
     717             : #endif
     718          38 :     , callFactory(rand_)
     719          76 :     , accountFactory()
     720             : {
     721             : #if defined _MSC_VER
     722             :     gnutls_global_init();
     723             : #endif
     724          38 :     pimpl_ = std::make_unique<ManagerPimpl>(*this);
     725          38 : }
     726             : 
     727          38 : Manager::~Manager() {}
     728             : 
     729             : void
     730         306 : Manager::setAutoAnswer(bool enable)
     731             : {
     732         306 :     pimpl_->autoAnswer_ = enable;
     733         306 : }
     734             : 
     735             : void
     736          32 : Manager::init(const std::filesystem::path& config_file, libjami::InitFlag flags)
     737             : {
     738             :     // FIXME: this is no good
     739          32 :     initialized = true;
     740             : 
     741          32 :     git_libgit2_init();
     742          32 :     auto res = git_transport_register("git", p2p_transport_cb, nullptr);
     743          32 :     if (res < 0) {
     744           0 :         const git_error* error = giterr_last();
     745           0 :         JAMI_ERROR("Unable to initialize git transport: {}", error ? error->message : "(unknown)");
     746             :     }
     747             : 
     748             : #ifndef WIN32
     749             :     // Set the max number of open files.
     750             :     struct rlimit nofiles;
     751          32 :     if (getrlimit(RLIMIT_NOFILE, &nofiles) == 0) {
     752          32 :         if (nofiles.rlim_cur < nofiles.rlim_max && nofiles.rlim_cur <= 1024u) {
     753           0 :             nofiles.rlim_cur = std::min<rlim_t>(nofiles.rlim_max, 8192u);
     754           0 :             setrlimit(RLIMIT_NOFILE, &nofiles);
     755             :         }
     756             :     }
     757             : #endif
     758             : 
     759             : #define PJSIP_TRY(ret) \
     760             :     do { \
     761             :         if ((ret) != PJ_SUCCESS) \
     762             :             throw std::runtime_error(#ret " failed"); \
     763             :     } while (0)
     764             : 
     765          32 :     srand(time(nullptr)); // to get random number for RANDOM_PORT
     766             : 
     767             :     // Initialize PJSIP (SIP and ICE implementation)
     768          32 :     PJSIP_TRY(pj_init());
     769          32 :     setSipLogLevel();
     770          32 :     PJSIP_TRY(pjlib_util_init());
     771          32 :     PJSIP_TRY(pjnath_init());
     772             : #undef PJSIP_TRY
     773             : 
     774          32 :     setGnuTlsLogLevel();
     775          32 :     dhtLogLevel = getDhtLogLevel();
     776          32 :     dhtnetLogLevel = getDhtnetLogLevel();
     777          32 :     pimpl_->upnpContext_->setMappingLabel("JAMI-" + fileutils::getOrCreateLocalDeviceId());
     778             : 
     779         128 :     JAMI_LOG("Using PJSIP version: {:s} for {:s}", pj_get_version(), PJ_OS_NAME);
     780         128 :     JAMI_LOG("Using GnuTLS version: {:s}", gnutls_check_version(nullptr));
     781         128 :     JAMI_LOG("Using OpenDHT version: {:s}", dht::version());
     782         128 :     JAMI_LOG("Using FFmpeg version: {:s}", av_version_info());
     783          32 :     int git2_major = 0, git2_minor = 0, git2_rev = 0;
     784          32 :     if (git_libgit2_version(&git2_major, &git2_minor, &git2_rev) == 0) {
     785         128 :         JAMI_LOG("Using libgit2 version: {:d}.{:d}.{:d}", git2_major, git2_minor, git2_rev);
     786             :     }
     787             : 
     788             :     // Manager can restart without being recreated (Unit tests)
     789             :     // So only create the SipLink once
     790          32 :     pimpl_->sipLink_ = std::make_unique<SIPVoIPLink>();
     791             : 
     792          32 :     check_rename(fileutils::get_cache_dir(PACKAGE_OLD), fileutils::get_cache_dir());
     793          32 :     check_rename(fileutils::get_data_dir(PACKAGE_OLD), fileutils::get_data_dir());
     794          32 :     check_rename(fileutils::get_config_dir(PACKAGE_OLD), fileutils::get_config_dir());
     795             : 
     796          32 :     pimpl_->ice_tf_ = std::make_shared<dhtnet::IceTransportFactory>(Logger::dhtLogger());
     797             : 
     798          32 :     pimpl_->path_ = config_file.empty() ? pimpl_->retrieveConfigPath() : config_file;
     799         128 :     JAMI_LOG("Configuration file path: {}", pimpl_->path_);
     800             : 
     801             : #ifdef ENABLE_PLUGIN
     802          32 :     pimpl_->jami_plugin_manager = std::make_unique<JamiPluginManager>();
     803             : #endif
     804             : 
     805          32 :     bool no_errors = true;
     806             : 
     807             :     // manager can restart without being recreated (Unit tests)
     808          32 :     pimpl_->finished_ = false;
     809             : 
     810             :     // Create video manager
     811          32 :     if (!(flags & libjami::LIBJAMI_FLAG_NO_LOCAL_VIDEO)) {
     812          32 :         pimpl_->videoManager_.reset(new VideoManager);
     813             :     }
     814             : 
     815          32 :     if (libjami::LIBJAMI_FLAG_NO_AUTOLOAD & flags) {
     816           0 :         autoLoad = false;
     817           0 :         JAMI_DEBUG("LIBJAMI_FLAG_NO_AUTOLOAD is set, accounts will neither be loaded nor backed up");
     818             :     } else {
     819             :         try {
     820          32 :             no_errors = pimpl_->parseConfiguration();
     821           0 :         } catch (const YAML::Exception& e) {
     822           0 :             JAMI_ERROR("[config] Failed to parse configuration: {}", e.what());
     823           0 :             no_errors = false;
     824           0 :         }
     825             : 
     826             :         // always back up last error-free configuration
     827          32 :         if (no_errors) {
     828          29 :             make_backup(pimpl_->path_);
     829             :         } else {
     830             :             // restore previous configuration
     831          12 :             JAMI_WARNING("Restoring last working configuration");
     832             : 
     833             :             try {
     834             :                 // remove accounts from broken configuration
     835           3 :                 removeAccounts();
     836           3 :                 restore_backup(pimpl_->path_);
     837           3 :                 pimpl_->parseConfiguration();
     838           0 :             } catch (const YAML::Exception& e) {
     839           0 :                 JAMI_ERROR("{}", e.what());
     840           0 :                 JAMI_WARNING("Restoring backup failed");
     841           0 :             }
     842             :         }
     843             :     }
     844             : 
     845          32 :     if (!(flags & libjami::LIBJAMI_FLAG_NO_LOCAL_AUDIO)) {
     846          32 :         std::lock_guard lock(pimpl_->audioLayerMutex_);
     847          32 :         pimpl_->initAudioDriver();
     848          32 :         if (pimpl_->audiodriver_) {
     849          32 :             auto format = pimpl_->audiodriver_->getFormat();
     850          32 :             pimpl_->toneCtrl_.setSampleRate(format.sample_rate, format.sampleFormat);
     851          64 :             pimpl_->dtmfKey_.reset(new DTMF(getRingBufferPool().getInternalSamplingRate(),
     852          32 :                                             getRingBufferPool().getInternalAudioFormat().sampleFormat));
     853             :         }
     854          32 :     }
     855             : 
     856             :     // Start ASIO event loop
     857          64 :     pimpl_->ioContextRunner_ = std::thread([context = pimpl_->ioContext_]() {
     858             :         try {
     859          32 :             auto work = asio::make_work_guard(*context);
     860          32 :             context->run();
     861          32 :         } catch (const std::exception& ex) {
     862           0 :             JAMI_ERROR("[io] Unexpected io_context thread exception: {}", ex.what());
     863           0 :         }
     864          64 :     });
     865             : 
     866          32 :     if (libjami::LIBJAMI_FLAG_NO_AUTOLOAD & flags) {
     867           0 :         JAMI_DEBUG("LIBJAMI_FLAG_NO_AUTOLOAD is set, accounts and conversations will not be loaded");
     868           0 :         return;
     869             :     } else {
     870          32 :         registerAccounts();
     871             :     }
     872             : }
     873             : 
     874             : void
     875         306 : Manager::finish() noexcept
     876             : {
     877         306 :     bool expected = false;
     878         306 :     if (not pimpl_->finished_.compare_exchange_strong(expected, true))
     879         268 :         return;
     880             : 
     881             :     try {
     882             :         // Terminate UPNP context
     883          38 :         upnpContext()->shutdown();
     884             : 
     885             :         // Forbid call creation
     886          38 :         callFactory.forbid();
     887             : 
     888             :         // End all remaining active calls
     889          38 :         JAMI_DBG("End %zu remaining call(s)", callFactory.callCount());
     890          38 :         for (const auto& call : callFactory.getAllCalls())
     891          38 :             hangupCall(call->getAccountId(), call->getCallId());
     892          38 :         callFactory.clear();
     893             : 
     894          38 :         for (const auto& account : getAllAccounts<JamiAccount>()) {
     895           0 :             if (account->getRegistrationState() == RegistrationState::INITIALIZING)
     896           0 :                 removeAccount(account->getAccountID(), true);
     897          38 :         }
     898             : 
     899          38 :         saveConfig();
     900             : 
     901             :         // Disconnect accounts, close link stacks and free allocated ressources
     902          38 :         unregisterAccounts();
     903          38 :         accountFactory.clear();
     904             : 
     905             :         {
     906          38 :             std::lock_guard lock(pimpl_->audioLayerMutex_);
     907          38 :             pimpl_->audiodriver_.reset();
     908          38 :         }
     909             : 
     910         152 :         JAMI_DEBUG("Stopping schedulers and worker threads");
     911             : 
     912             :         // Flush remaining tasks (free lambda' with capture)
     913          38 :         dht::ThreadPool::io().join();
     914          38 :         dht::ThreadPool::computation().join();
     915             : 
     916             :         // IceTransportFactory should be stopped after the io pool
     917             :         // as some ICE are destroyed in a ioPool (see ConnectionManager)
     918             :         // Also, it must be called before pj_shutdown to avoid any problem
     919          38 :         pimpl_->ice_tf_.reset();
     920             : 
     921             :         // NOTE: sipLink_->shutdown() is needed because this will perform
     922             :         // sipTransportBroker->shutdown(); which will call Manager::instance().sipVoIPLink()
     923             :         // so the pointer MUST NOT be resetted at this point
     924          38 :         if (pimpl_->sipLink_) {
     925          32 :             pimpl_->sipLink_->shutdown();
     926          32 :             pimpl_->sipLink_.reset();
     927             :         }
     928             : 
     929          38 :         pj_shutdown();
     930          38 :         pimpl_->gitTransports_.clear();
     931          38 :         git_libgit2_shutdown();
     932             : 
     933          38 :         if (!pimpl_->ioContext_->stopped()) {
     934          38 :             pimpl_->ioContext_->stop(); // make thread stop
     935             :         }
     936          38 :         if (pimpl_->ioContextRunner_.joinable())
     937          32 :             pimpl_->ioContextRunner_.join();
     938             : 
     939             : #if defined _MSC_VER
     940             :         gnutls_global_deinit();
     941             : #endif
     942             : 
     943           0 :     } catch (const VoipLinkException& err) {
     944           0 :         JAMI_ERROR("[voip] {}", err.what());
     945           0 :     }
     946             : }
     947             : 
     948             : void
     949           0 : Manager::monitor(bool continuous)
     950             : {
     951           0 :     Logger::setMonitorLog(true);
     952           0 :     JAMI_DEBUG("############## START MONITORING ##############");
     953           0 :     JAMI_DEBUG("Using PJSIP version: {} for {}", pj_get_version(), PJ_OS_NAME);
     954           0 :     JAMI_DEBUG("Using GnuTLS version: {}", gnutls_check_version(nullptr));
     955           0 :     JAMI_DEBUG("Using OpenDHT version: {}", dht::version());
     956             : 
     957             : #ifdef __linux__
     958             : #if defined(__ANDROID__)
     959             : #else
     960           0 :     auto opened_files = dhtnet::fileutils::readDirectory("/proc/" + std::to_string(getpid()) + "/fd").size();
     961           0 :     JAMI_DEBUG("Opened files: {}", opened_files);
     962             : #endif
     963             : #endif
     964             : 
     965           0 :     for (const auto& call : callFactory.getAllCalls())
     966           0 :         call->monitor();
     967           0 :     for (const auto& account : getAllAccounts())
     968           0 :         if (auto acc = std::dynamic_pointer_cast<JamiAccount>(account))
     969           0 :             acc->monitor();
     970           0 :     JAMI_DEBUG("############## END MONITORING ##############");
     971           0 :     Logger::setMonitorLog(continuous);
     972           0 : }
     973             : 
     974             : std::vector<std::map<std::string, std::string>>
     975           0 : Manager::getConnectionList(const std::string& accountId, const std::string& conversationId)
     976             : {
     977           0 :     std::vector<std::map<std::string, std::string>> connectionsList;
     978             : 
     979           0 :     if (accountId.empty()) {
     980           0 :         for (const auto& account : getAllAccounts<JamiAccount>()) {
     981           0 :             if (account->getRegistrationState() != RegistrationState::INITIALIZING) {
     982           0 :                 const auto& cnl = account->getConnectionList(conversationId);
     983           0 :                 connectionsList.insert(connectionsList.end(), cnl.begin(), cnl.end());
     984           0 :             }
     985           0 :         }
     986             :     } else {
     987           0 :         auto account = getAccount(accountId);
     988           0 :         if (account) {
     989           0 :             if (auto acc = std::dynamic_pointer_cast<JamiAccount>(account)) {
     990           0 :                 if (acc->getRegistrationState() != RegistrationState::INITIALIZING) {
     991           0 :                     const auto& cnl = acc->getConnectionList(conversationId);
     992           0 :                     connectionsList.insert(connectionsList.end(), cnl.begin(), cnl.end());
     993           0 :                 }
     994           0 :             }
     995             :         }
     996           0 :     }
     997             : 
     998           0 :     return connectionsList;
     999           0 : }
    1000             : 
    1001             : std::vector<std::map<std::string, std::string>>
    1002           0 : Manager::getChannelList(const std::string& accountId, const std::string& connectionId)
    1003             : {
    1004             :     // if account id is empty, return all channels
    1005             :     // else return only for specific accountid
    1006           0 :     std::vector<std::map<std::string, std::string>> channelsList;
    1007             : 
    1008           0 :     if (accountId.empty()) {
    1009           0 :         for (const auto& account : getAllAccounts<JamiAccount>()) {
    1010           0 :             if (account->getRegistrationState() != RegistrationState::INITIALIZING) {
    1011             :                 // add to channelsList all channels for this account
    1012           0 :                 const auto& cnl = account->getChannelList(connectionId);
    1013           0 :                 channelsList.insert(channelsList.end(), cnl.begin(), cnl.end());
    1014           0 :             }
    1015           0 :         }
    1016             : 
    1017             :     }
    1018             : 
    1019             :     else {
    1020             :         // get the jamiaccount for this accountid and return its channels
    1021           0 :         auto account = getAccount(accountId);
    1022           0 :         if (account) {
    1023           0 :             if (auto acc = std::dynamic_pointer_cast<JamiAccount>(account)) {
    1024           0 :                 if (acc->getRegistrationState() != RegistrationState::INITIALIZING) {
    1025           0 :                     const auto& cnl = acc->getChannelList(connectionId);
    1026           0 :                     channelsList.insert(channelsList.end(), cnl.begin(), cnl.end());
    1027           0 :                 }
    1028           0 :             }
    1029             :         }
    1030           0 :     }
    1031             : 
    1032           0 :     return channelsList;
    1033           0 : }
    1034             : 
    1035             : bool
    1036         334 : Manager::isCurrentCall(const Call& call) const
    1037             : {
    1038         334 :     return pimpl_->currentCall_ == call.getCallId();
    1039             : }
    1040             : 
    1041             : bool
    1042         277 : Manager::hasCurrentCall() const
    1043             : {
    1044         884 :     for (const auto& call : callFactory.getAllCalls()) {
    1045         770 :         if (!call->isSubcall() && call->getStateStr() == libjami::Call::StateEvent::CURRENT)
    1046         163 :             return true;
    1047         277 :     }
    1048         114 :     return false;
    1049             : }
    1050             : 
    1051             : std::shared_ptr<Call>
    1052         100 : Manager::getCurrentCall() const
    1053             : {
    1054         100 :     return getCallFromCallID(pimpl_->currentCall_);
    1055             : }
    1056             : 
    1057             : const std::string&
    1058          71 : Manager::getCurrentCallId() const
    1059             : {
    1060          71 :     return pimpl_->currentCall_;
    1061             : }
    1062             : 
    1063             : void
    1064          38 : Manager::unregisterAccounts()
    1065             : {
    1066          38 :     for (const auto& account : getAllAccounts()) {
    1067           0 :         if (account->isEnabled()) {
    1068           0 :             account->doUnregister(true);
    1069             :         }
    1070          38 :     }
    1071          38 : }
    1072             : 
    1073             : ///////////////////////////////////////////////////////////////////////////////
    1074             : // Management of events' IP-phone user
    1075             : ///////////////////////////////////////////////////////////////////////////////
    1076             : /* Main Thread */
    1077             : 
    1078             : std::string
    1079         110 : Manager::outgoingCall(const std::string& account_id,
    1080             :                       const std::string& to,
    1081             :                       const std::vector<libjami::MediaMap>& mediaList)
    1082             : {
    1083         110 :     JAMI_DBG() << "Attempt outgoing call to '" << to << "'" << " with account '" << account_id << "'";
    1084             : 
    1085         110 :     std::shared_ptr<Call> call;
    1086             : 
    1087             :     try {
    1088         110 :         call = newOutgoingCall(trim(to), account_id, mediaList);
    1089           0 :     } catch (const std::exception& e) {
    1090           0 :         JAMI_ERROR("{}", e.what());
    1091           0 :         return {};
    1092           0 :     }
    1093             : 
    1094         110 :     if (not call)
    1095          11 :         return {};
    1096             : 
    1097          99 :     stopTone();
    1098             : 
    1099          99 :     pimpl_->switchCall(call->getCallId());
    1100             : 
    1101          99 :     return call->getCallId();
    1102         110 : }
    1103             : 
    1104             : // THREAD=Main : for outgoing Call
    1105             : bool
    1106          76 : Manager::acceptCall(const std::string& accountId,
    1107             :                     const std::string& callId,
    1108             :                     const std::vector<libjami::MediaMap>& mediaList)
    1109             : {
    1110          76 :     if (auto account = getAccount(accountId)) {
    1111          76 :         if (auto call = account->getCall(callId)) {
    1112          76 :             return acceptCall(*call, mediaList);
    1113          76 :         }
    1114          76 :     }
    1115           0 :     return false;
    1116             : }
    1117             : 
    1118             : bool
    1119          90 : Manager::acceptCall(Call& call, const std::vector<libjami::MediaMap>& mediaList)
    1120             : {
    1121         360 :     JAMI_LOG("Answer call {}", call.getCallId());
    1122             : 
    1123          90 :     if (call.getConnectionState() != Call::ConnectionState::RINGING) {
    1124             :         // The call is already answered
    1125           0 :         return true;
    1126             :     }
    1127             : 
    1128             :     // If ringing
    1129          90 :     stopTone();
    1130          90 :     pimpl_->removeWaitingCall(call.getCallId());
    1131             : 
    1132             :     try {
    1133          90 :         call.answer(mediaList);
    1134           0 :     } catch (const std::runtime_error& e) {
    1135           0 :         JAMI_ERROR("[call:{}] Failed to answer: {}", call.getCallId(), e.what());
    1136           0 :         return false;
    1137           0 :     }
    1138             : 
    1139             :     // if we dragged this call into a conference already
    1140          90 :     if (auto conf = call.getConference())
    1141           0 :         pimpl_->switchCall(conf->getConfId());
    1142             :     else
    1143          90 :         pimpl_->switchCall(call.getCallId());
    1144             : 
    1145          90 :     addAudio(call);
    1146             : 
    1147             :     // Start recording if set in preference
    1148          90 :     if (audioPreference.getIsAlwaysRecording()) {
    1149           0 :         auto recResult = call.toggleRecording();
    1150           0 :         emitSignal<libjami::CallSignal::RecordPlaybackFilepath>(call.getCallId(), call.getPath());
    1151           0 :         emitSignal<libjami::CallSignal::RecordingStateChanged>(call.getCallId(), recResult);
    1152             :     }
    1153          90 :     return true;
    1154             : }
    1155             : 
    1156             : // THREAD=Main
    1157             : bool
    1158         112 : Manager::hangupCall(const std::string& accountId, const std::string& callId)
    1159             : {
    1160         112 :     auto account = getAccount(accountId);
    1161         112 :     if (not account)
    1162           0 :         return false;
    1163             :     // store the current call id
    1164         112 :     stopTone();
    1165         112 :     pimpl_->removeWaitingCall(callId);
    1166             : 
    1167             :     /* We often get here when the call was hungup before being created */
    1168         112 :     auto call = account->getCall(callId);
    1169         112 :     if (not call) {
    1170           0 :         JAMI_WARN("Unable to hang up nonexistent call %s", callId.c_str());
    1171           0 :         return false;
    1172             :     }
    1173             : 
    1174             :     // Disconnect streams
    1175         112 :     removeAudio(*call);
    1176             : 
    1177         112 :     if (call->isConferenceParticipant()) {
    1178          50 :         removeParticipant(*call);
    1179             :     } else {
    1180             :         // we are not participating in a conference, current call switched to ""
    1181          62 :         if (isCurrentCall(*call))
    1182          27 :             pimpl_->unsetCurrentCall();
    1183             :     }
    1184             : 
    1185             :     try {
    1186         112 :         call->hangup(0);
    1187           0 :     } catch (const VoipLinkException& e) {
    1188           0 :         JAMI_ERROR("[call:{}] Failed to hangup: {}", call->getCallId(), e.what());
    1189           0 :         return false;
    1190           0 :     }
    1191             : 
    1192         112 :     return true;
    1193         112 : }
    1194             : 
    1195             : bool
    1196          28 : Manager::hangupConference(const std::string& accountId, const std::string& confId)
    1197             : {
    1198          28 :     if (auto account = getAccount(accountId)) {
    1199          28 :         if (auto conference = account->getConference(confId)) {
    1200          27 :             return pimpl_->hangupConference(*conference);
    1201             :         } else {
    1202           4 :             JAMI_ERROR("[conf:{}] Conference not found", confId);
    1203          28 :         }
    1204          28 :     }
    1205           1 :     return false;
    1206             : }
    1207             : 
    1208             : // THREAD=Main
    1209             : bool
    1210           8 : Manager::holdCall(const std::string&, const std::string& callId)
    1211             : {
    1212           8 :     bool result = true;
    1213             : 
    1214           8 :     stopTone();
    1215             : 
    1216           8 :     std::string current_callId(getCurrentCallId());
    1217             : 
    1218           8 :     if (auto call = getCallFromCallID(callId)) {
    1219             :         try {
    1220           8 :             result = call->hold([=](bool ok) {
    1221           8 :                 if (!ok) {
    1222           0 :                     JAMI_ERROR("CallID {} holdCall failed", callId);
    1223           0 :                     return;
    1224             :                 }
    1225           8 :                 removeAudio(*call); // Unbind calls in main buffer
    1226             :                 // Remove call from the queue if it was still there
    1227           8 :                 pimpl_->removeWaitingCall(callId);
    1228             : 
    1229             :                 // Keeps current call ID if the action does not hold this call
    1230             :                 // or a new outgoing call. This could happen in case of a conference
    1231           8 :                 if (current_callId == callId)
    1232           3 :                     pimpl_->unsetCurrentCall();
    1233             :             });
    1234           0 :         } catch (const VoipLinkException& e) {
    1235           0 :             JAMI_ERROR("[call:{}] Failed to hold: {}", callId, e.what());
    1236           0 :             result = false;
    1237           0 :         }
    1238             :     } else {
    1239           0 :         JAMI_LOG("CallID {} doesn't exist in call holdCall", callId);
    1240           0 :         return false;
    1241           8 :     }
    1242             : 
    1243           8 :     return result;
    1244           8 : }
    1245             : 
    1246             : // THREAD=Main
    1247             : bool
    1248           3 : Manager::resumeCall(const std::string&, const std::string& callId)
    1249             : {
    1250           3 :     bool result = true;
    1251             : 
    1252           3 :     stopTone();
    1253             : 
    1254           3 :     std::shared_ptr<Call> call = getCallFromCallID(callId);
    1255           3 :     if (!call)
    1256           0 :         return false;
    1257             : 
    1258             :     try {
    1259           3 :         result = call->resume([=](bool ok) {
    1260           3 :             if (!ok) {
    1261           0 :                 JAMI_ERROR("CallID {} resumeCall failed", callId);
    1262           0 :                 return;
    1263             :             }
    1264             : 
    1265           3 :             if (auto conf = call->getConference())
    1266           0 :                 pimpl_->switchCall(conf->getConfId());
    1267             :             else
    1268           3 :                 pimpl_->switchCall(call->getCallId());
    1269             : 
    1270           3 :             addAudio(*call);
    1271             :         });
    1272           0 :     } catch (const VoipLinkException& e) {
    1273           0 :         JAMI_ERROR("[call] Failed to resume: {}", e.what());
    1274           0 :         return false;
    1275           0 :     }
    1276             : 
    1277           3 :     return result;
    1278           3 : }
    1279             : 
    1280             : // THREAD=Main
    1281             : bool
    1282           2 : Manager::transferCall(const std::string& accountId, const std::string& callId, const std::string& to)
    1283             : {
    1284           2 :     auto account = getAccount(accountId);
    1285           2 :     if (not account)
    1286           0 :         return false;
    1287           2 :     if (auto call = account->getCall(callId)) {
    1288           2 :         if (call->isConferenceParticipant())
    1289           0 :             removeParticipant(*call);
    1290           2 :         call->transfer(to);
    1291             :     } else
    1292           2 :         return false;
    1293             : 
    1294             :     // remove waiting call in case we make transfer without even answer
    1295           2 :     pimpl_->removeWaitingCall(callId);
    1296             : 
    1297           2 :     return true;
    1298           2 : }
    1299             : 
    1300             : void
    1301           0 : Manager::transferFailed()
    1302             : {
    1303           0 :     emitSignal<libjami::CallSignal::TransferFailed>();
    1304           0 : }
    1305             : 
    1306             : void
    1307           0 : Manager::transferSucceeded()
    1308             : {
    1309           0 :     emitSignal<libjami::CallSignal::TransferSucceeded>();
    1310           0 : }
    1311             : 
    1312             : // THREAD=Main : Call:Incoming
    1313             : bool
    1314           2 : Manager::refuseCall(const std::string& accountId, const std::string& id)
    1315             : {
    1316           2 :     if (auto account = getAccount(accountId)) {
    1317           2 :         if (auto call = account->getCall(id)) {
    1318           2 :             stopTone();
    1319           2 :             call->refuse();
    1320           2 :             pimpl_->removeWaitingCall(id);
    1321           2 :             removeAudio(*call);
    1322           2 :             return true;
    1323           2 :         }
    1324           2 :     }
    1325           0 :     return false;
    1326             : }
    1327             : 
    1328             : bool
    1329           0 : Manager::holdConference(const std::string& accountId, const std::string& confId)
    1330             : {
    1331           0 :     JAMI_LOG("[conf:{}] Hold conference", confId);
    1332             : 
    1333           0 :     if (const auto account = getAccount(accountId)) {
    1334           0 :         if (auto conf = account->getConference(confId)) {
    1335           0 :             conf->detachHost();
    1336           0 :             emitSignal<libjami::CallSignal::ConferenceChanged>(accountId, conf->getConfId(), conf->getStateStr());
    1337           0 :             return true;
    1338           0 :         }
    1339           0 :     }
    1340           0 :     return false;
    1341             : }
    1342             : 
    1343             : bool
    1344           0 : Manager::resumeConference(const std::string& accountId, const std::string& confId)
    1345             : {
    1346           0 :     JAMI_DEBUG("[conf:{}] Resume conference", confId);
    1347             : 
    1348           0 :     if (const auto account = getAccount(accountId)) {
    1349           0 :         if (auto conf = account->getConference(confId)) {
    1350             :             // Resume conf only if it was in hold state otherwise…
    1351             :             // all participants are restarted
    1352           0 :             if (conf->getState() == Conference::State::HOLD) {
    1353           0 :                 for (const auto& item : conf->getSubCalls())
    1354           0 :                     resumeCall(accountId, item);
    1355             : 
    1356           0 :                 pimpl_->switchCall(confId);
    1357           0 :                 conf->setState(Conference::State::ACTIVE_ATTACHED);
    1358           0 :                 emitSignal<libjami::CallSignal::ConferenceChanged>(accountId, conf->getConfId(), conf->getStateStr());
    1359           0 :                 return true;
    1360           0 :             } else if (conf->getState() == Conference::State::ACTIVE_DETACHED) {
    1361           0 :                 pimpl_->addMainParticipant(*conf);
    1362             :             }
    1363           0 :         }
    1364           0 :     }
    1365           0 :     return false;
    1366             : }
    1367             : 
    1368             : bool
    1369          11 : Manager::addSubCall(const std::string& accountId,
    1370             :                     const std::string& callId,
    1371             :                     const std::string& account2Id,
    1372             :                     const std::string& conferenceId)
    1373             : {
    1374          11 :     auto account = getAccount(accountId);
    1375          11 :     auto account2 = getAccount(account2Id);
    1376          11 :     if (account && account2) {
    1377          11 :         auto call = account->getCall(callId);
    1378          11 :         auto conf = account2->getConference(conferenceId);
    1379          11 :         if (!call or !conf)
    1380           0 :             return false;
    1381          11 :         auto callConf = call->getConference();
    1382          11 :         if (callConf != conf)
    1383          11 :             return addSubCall(*call, *conf);
    1384          33 :     }
    1385           0 :     return false;
    1386          11 : }
    1387             : 
    1388             : bool
    1389          11 : Manager::addSubCall(Call& call, Conference& conference)
    1390             : {
    1391          44 :     JAMI_DEBUG("[conf:{}] Adding participant {}", conference.getConfId(), call.getCallId());
    1392             : 
    1393             :     // Store the current call ID (it will change in resumeCall or in acceptCall)
    1394          11 :     pimpl_->bindCallToConference(call, conference);
    1395             : 
    1396             :     // Don't attach current user yet
    1397          11 :     if (conference.getState() == Conference::State::ACTIVE_DETACHED) {
    1398           0 :         return true;
    1399             :     }
    1400             : 
    1401             :     // TODO: remove this ugly hack → There should be different calls when double clicking
    1402             :     // a conference to add main participant to it, or (in this case) adding a participant
    1403             :     // to conference
    1404          11 :     pimpl_->unsetCurrentCall();
    1405          11 :     pimpl_->addMainParticipant(conference);
    1406          11 :     pimpl_->switchCall(conference.getConfId());
    1407          11 :     addAudio(call);
    1408             : 
    1409          11 :     return true;
    1410             : }
    1411             : 
    1412             : void
    1413          11 : Manager::ManagerPimpl::addMainParticipant(Conference& conf)
    1414             : {
    1415          44 :     JAMI_DEBUG("[conf:{}] Adding main participant", conf.getConfId());
    1416          11 :     conf.attachHost(conf.getLastMediaList());
    1417          11 :     emitSignal<libjami::CallSignal::ConferenceChanged>(conf.getAccountId(), conf.getConfId(), conf.getStateStr());
    1418          11 :     switchCall(conf.getConfId());
    1419          11 : }
    1420             : 
    1421             : bool
    1422          27 : Manager::ManagerPimpl::hangupConference(Conference& conference)
    1423             : {
    1424         108 :     JAMI_DEBUG("[conf:{}] Hanging up conference", conference.getConfId());
    1425          27 :     CallIdSet subcalls(conference.getSubCalls());
    1426          27 :     conference.detachHost();
    1427          27 :     if (subcalls.empty()) {
    1428           5 :         if (auto account = conference.getAccount())
    1429           5 :             account->removeConference(conference.getConfId());
    1430             :     }
    1431          72 :     for (const auto& callId : subcalls) {
    1432          45 :         if (auto call = base_.getCallFromCallID(callId))
    1433          45 :             base_.hangupCall(call->getAccountId(), callId);
    1434             :     }
    1435          27 :     unsetCurrentCall();
    1436          27 :     return true;
    1437          27 : }
    1438             : 
    1439             : bool
    1440           0 : Manager::addMainParticipant(const std::string& accountId, const std::string& conferenceId)
    1441             : {
    1442           0 :     JAMI_LOG("[conf:{}] Adding main participant", conferenceId);
    1443             : 
    1444           0 :     if (auto account = getAccount(accountId)) {
    1445           0 :         if (auto conf = account->getConference(conferenceId)) {
    1446           0 :             pimpl_->addMainParticipant(*conf);
    1447           0 :             return true;
    1448             :         } else
    1449           0 :             JAMI_WARNING("[conf:{}] Failed to add main participant (conference not found)", conferenceId);
    1450           0 :     }
    1451           0 :     return false;
    1452             : }
    1453             : 
    1454             : std::shared_ptr<Call>
    1455         612 : Manager::getCallFromCallID(const std::string& callID) const
    1456             : {
    1457         612 :     return callFactory.getCall(callID);
    1458             : }
    1459             : 
    1460             : bool
    1461          23 : Manager::joinParticipant(const std::string& accountId,
    1462             :                          const std::string& callId1,
    1463             :                          const std::string& account2Id,
    1464             :                          const std::string& callId2,
    1465             :                          bool attached)
    1466             : {
    1467          92 :     JAMI_DEBUG("Joining participants {} and {}, attached={}", callId1, callId2, attached);
    1468          23 :     auto account = getAccount(accountId);
    1469          23 :     auto account2 = getAccount(account2Id);
    1470          23 :     if (not account or not account2) {
    1471           0 :         return false;
    1472             :     }
    1473             : 
    1474          92 :     JAMI_LOG("Creating conference for participants {} and {}, host attached: {}", callId1, callId2, attached);
    1475             : 
    1476          23 :     if (callId1 == callId2) {
    1477           0 :         JAMI_ERROR("Unable to join participant {} to itself", callId1);
    1478           0 :         return false;
    1479             :     }
    1480             : 
    1481             :     // Set corresponding conference ids for call 1
    1482          23 :     auto call1 = account->getCall(callId1);
    1483          23 :     if (!call1) {
    1484           0 :         JAMI_ERROR("Unable to find call {}", callId1);
    1485           0 :         return false;
    1486             :     }
    1487             : 
    1488             :     // Set corresponding conference details
    1489          23 :     auto call2 = account2->getCall(callId2);
    1490          23 :     if (!call2) {
    1491           0 :         JAMI_ERROR("Unable to find call {}", callId2);
    1492           0 :         return false;
    1493             :     }
    1494             : 
    1495          23 :     auto mediaAttr = call1->getMediaAttributeList();
    1496          23 :     if (mediaAttr.empty()) {
    1497           0 :         JAMI_WARNING("[call:{}] No media attribute found, using media attribute from call [{}]", callId1, callId2);
    1498           0 :         mediaAttr = call2->getMediaAttributeList();
    1499             :     }
    1500             : 
    1501             :     // Filter out secondary audio streams that are muted: these are SDP
    1502             :     // negotiation artifacts (the host answered a participant's extra audio
    1503             :     // offer with a muted slot) and do not represent real host audio sources.
    1504             :     {
    1505          23 :         bool audioFound = false;
    1506          23 :         mediaAttr.erase(std::remove_if(mediaAttr.begin(),
    1507             :                                        mediaAttr.end(),
    1508          88 :                                        [&audioFound](const MediaAttribute& attr) {
    1509          42 :                                            if (attr.type_ == MediaType::MEDIA_AUDIO) {
    1510          23 :                                                if (audioFound && attr.muted_)
    1511           0 :                                                    return true; // remove secondary audio streams
    1512          23 :                                                audioFound = true;
    1513             :                                            }
    1514          42 :                                            return false;
    1515             :                                        }),
    1516          23 :                         mediaAttr.end());
    1517             :     }
    1518             : 
    1519          92 :     JAMI_DEBUG("[call:{}] Media attributes for conference:", callId1);
    1520          65 :     for (const auto& media : mediaAttr) {
    1521         168 :         JAMI_DEBUG("- {}", media.toString(true));
    1522             :     }
    1523             : 
    1524          23 :     auto conf = std::make_shared<Conference>(account);
    1525          23 :     conf->attachHost(MediaAttribute::mediaAttributesToMediaMaps(mediaAttr));
    1526          23 :     account->attach(conf);
    1527          23 :     emitSignal<libjami::CallSignal::ConferenceCreated>(account->getAccountID(), "", conf->getConfId());
    1528             : 
    1529             :     // Bind calls according to their state
    1530          23 :     pimpl_->bindCallToConference(*call1, *conf);
    1531          23 :     pimpl_->bindCallToConference(*call2, *conf);
    1532             : 
    1533             :     // Switch current call id to this conference
    1534          23 :     if (attached) {
    1535          23 :         pimpl_->switchCall(conf->getConfId());
    1536          23 :         conf->setState(Conference::State::ACTIVE_ATTACHED);
    1537             :     } else {
    1538           0 :         conf->detachHost();
    1539             :     }
    1540          23 :     emitSignal<libjami::CallSignal::ConferenceChanged>(account->getAccountID(), conf->getConfId(), conf->getStateStr());
    1541             : 
    1542          23 :     return true;
    1543          23 : }
    1544             : 
    1545             : void
    1546           0 : Manager::createConfFromParticipantList(const std::string& accountId, const std::vector<std::string>& participantList)
    1547             : {
    1548           0 :     auto account = getAccount(accountId);
    1549           0 :     if (not account) {
    1550           0 :         JAMI_WARNING("[account:{}] Account not found", accountId);
    1551           0 :         return;
    1552             :     }
    1553             : 
    1554             :     // we must have at least 2 participant for a conference
    1555           0 :     if (participantList.size() <= 1) {
    1556           0 :         JAMI_ERROR("[conf] Participant number must be greater than or equal to 2");
    1557           0 :         return;
    1558             :     }
    1559             : 
    1560           0 :     auto conf = std::make_shared<Conference>(account);
    1561             :     // attach host with empty medialist
    1562             :     // which will result in a default list set by initSourcesForHost
    1563           0 :     conf->attachHost({});
    1564             : 
    1565           0 :     unsigned successCounter = 0;
    1566           0 :     for (const auto& numberaccount : participantList) {
    1567           0 :         std::string tostr(numberaccount.substr(0, numberaccount.find(',')));
    1568           0 :         std::string account(numberaccount.substr(numberaccount.find(',') + 1, numberaccount.size()));
    1569             : 
    1570           0 :         pimpl_->unsetCurrentCall();
    1571             : 
    1572             :         // Create call
    1573           0 :         auto callId = outgoingCall(account, tostr, {});
    1574           0 :         if (callId.empty())
    1575           0 :             continue;
    1576             : 
    1577             :         // Manager methods may behave differently if the call id participates in a conference
    1578           0 :         conf->addSubCall(callId);
    1579           0 :         successCounter++;
    1580           0 :     }
    1581             : 
    1582             :     // Create the conference if and only if at least 2 calls have been successfully created
    1583           0 :     if (successCounter >= 2) {
    1584           0 :         account->attach(conf);
    1585           0 :         emitSignal<libjami::CallSignal::ConferenceCreated>(accountId, "", conf->getConfId());
    1586             :     }
    1587           0 : }
    1588             : 
    1589             : bool
    1590           0 : Manager::detachHost(const std::shared_ptr<Conference>& conf)
    1591             : {
    1592           0 :     if (not conf)
    1593           0 :         return false;
    1594             : 
    1595           0 :     JAMI_LOG("[conf:{}] Detaching host", conf->getConfId());
    1596           0 :     conf->detachHost();
    1597           0 :     emitSignal<libjami::CallSignal::ConferenceChanged>(conf->getAccountId(), conf->getConfId(), conf->getStateStr());
    1598           0 :     pimpl_->unsetCurrentCall();
    1599           0 :     return true;
    1600             : }
    1601             : 
    1602             : bool
    1603           0 : Manager::detachParticipant(const std::string& callId)
    1604             : {
    1605           0 :     JAMI_DEBUG("Detaching participant {}", callId);
    1606             : 
    1607           0 :     auto call = getCallFromCallID(callId);
    1608           0 :     if (!call) {
    1609           0 :         JAMI_ERROR("Unable to find call {}", callId);
    1610           0 :         return false;
    1611             :     }
    1612             : 
    1613             :     // Don't hold ringing calls when detaching them from conferences
    1614           0 :     if (call->getStateStr() != "RINGING")
    1615           0 :         holdCall(call->getAccountId(), callId);
    1616             : 
    1617           0 :     removeParticipant(*call);
    1618           0 :     return true;
    1619           0 : }
    1620             : 
    1621             : void
    1622          63 : Manager::removeParticipant(Call& call)
    1623             : {
    1624         252 :     JAMI_DEBUG("Removing participant {}", call.getCallId());
    1625             : 
    1626          63 :     auto conf = call.getConference();
    1627          63 :     if (not conf) {
    1628           0 :         JAMI_ERROR("[call:{}] No conference associated, unable to remove participant", call.getCallId());
    1629           0 :         return;
    1630             :     }
    1631             : 
    1632          63 :     conf->removeSubCall(call.getCallId());
    1633             : 
    1634          63 :     removeAudio(call);
    1635             : 
    1636          63 :     emitSignal<libjami::CallSignal::ConferenceChanged>(conf->getAccountId(), conf->getConfId(), conf->getStateStr());
    1637             : 
    1638          63 :     pimpl_->processRemainingParticipants(*conf);
    1639          63 : }
    1640             : 
    1641             : bool
    1642           0 : Manager::joinConference(const std::string& accountId,
    1643             :                         const std::string& confId1,
    1644             :                         const std::string& account2Id,
    1645             :                         const std::string& confId2)
    1646             : {
    1647           0 :     auto account = getAccount(accountId);
    1648           0 :     auto account2 = getAccount(account2Id);
    1649           0 :     if (not account) {
    1650           0 :         JAMI_ERROR("Unable to find account: {}", accountId);
    1651           0 :         return false;
    1652             :     }
    1653           0 :     if (not account2) {
    1654           0 :         JAMI_ERROR("Unable to find account: {}", account2Id);
    1655           0 :         return false;
    1656             :     }
    1657             : 
    1658           0 :     auto conf = account->getConference(confId1);
    1659           0 :     if (not conf) {
    1660           0 :         JAMI_ERROR("[conf:{}] Invalid conference ID", confId1);
    1661           0 :         return false;
    1662             :     }
    1663             : 
    1664           0 :     auto conf2 = account2->getConference(confId2);
    1665           0 :     if (not conf2) {
    1666           0 :         JAMI_ERROR("[conf:{}] Invalid conference ID", confId2);
    1667           0 :         return false;
    1668             :     }
    1669             : 
    1670           0 :     CallIdSet subcalls(conf->getSubCalls());
    1671             : 
    1672           0 :     std::vector<std::shared_ptr<Call>> calls;
    1673           0 :     calls.reserve(subcalls.size());
    1674             : 
    1675             :     // Detach and remove all participant from conf1 before add
    1676             :     // ... to conf2
    1677           0 :     for (const auto& callId : subcalls) {
    1678           0 :         JAMI_DEBUG("Detach participant {}", callId);
    1679           0 :         if (auto call = account->getCall(callId)) {
    1680           0 :             conf->removeSubCall(callId);
    1681           0 :             removeAudio(*call);
    1682           0 :             calls.emplace_back(std::move(call));
    1683             :         } else {
    1684           0 :             JAMI_ERROR("Unable to find call {}", callId);
    1685           0 :         }
    1686             :     }
    1687             :     // Remove conf1
    1688           0 :     account->removeConference(confId1);
    1689             : 
    1690           0 :     for (const auto& c : calls)
    1691           0 :         addSubCall(*c, *conf2);
    1692             : 
    1693           0 :     return true;
    1694           0 : }
    1695             : 
    1696             : void
    1697         200 : Manager::addAudio(Call& call)
    1698             : {
    1699         200 :     if (call.isConferenceParticipant())
    1700          11 :         return;
    1701         189 :     const auto& callId = call.getCallId();
    1702         756 :     JAMI_LOG("Add audio to call {}", callId);
    1703             : 
    1704             :     // bind to main
    1705         189 :     auto medias = call.getAudioStreams();
    1706         381 :     for (const auto& media : medias) {
    1707         768 :         JAMI_DEBUG("[call:{}] Attach audio stream {}", callId, media.first);
    1708         192 :         getRingBufferPool().bindRingBuffers(media.first, RingBufferPool::DEFAULT_ID);
    1709             :     }
    1710         189 :     auto oldGuard = std::move(call.audioGuard);
    1711         189 :     call.audioGuard = startAudioStream(AudioDeviceType::PLAYBACK);
    1712             : 
    1713         189 :     std::lock_guard lock(pimpl_->audioLayerMutex_);
    1714         189 :     if (!pimpl_->audiodriver_) {
    1715           0 :         JAMI_ERROR("Uninitialized audio driver");
    1716           0 :         return;
    1717             :     }
    1718         189 :     pimpl_->audiodriver_->flushUrgent();
    1719         189 :     getRingBufferPool().flushAllBuffers();
    1720         189 : }
    1721             : 
    1722             : void
    1723         379 : Manager::removeAudio(Call& call)
    1724             : {
    1725         379 :     const auto& callId = call.getCallId();
    1726         379 :     auto medias = call.getAudioStreams();
    1727         761 :     for (const auto& media : medias) {
    1728        1528 :         JAMI_DEBUG("[call:{}] Remove local audio {}", callId, media.first);
    1729         382 :         getRingBufferPool().unBindAll(media.first);
    1730             :     }
    1731         379 : }
    1732             : 
    1733             : std::shared_ptr<asio::io_context>
    1734       22326 : Manager::ioContext() const
    1735             : {
    1736       22326 :     return pimpl_->ioContext_;
    1737             : }
    1738             : 
    1739             : std::shared_ptr<dhtnet::upnp::UPnPContext>
    1740         829 : Manager::upnpContext() const
    1741             : {
    1742         829 :     return pimpl_->upnpContext_;
    1743             : }
    1744             : 
    1745             : void
    1746        1007 : Manager::saveConfig(const std::shared_ptr<Account>& acc)
    1747             : {
    1748        1007 :     if (auto account = std::dynamic_pointer_cast<JamiAccount>(acc))
    1749         983 :         account->saveConfig();
    1750             :     else
    1751        1007 :         saveConfig();
    1752        1007 : }
    1753             : 
    1754             : void
    1755        2449 : Manager::saveConfig()
    1756             : {
    1757        9796 :     JAMI_LOG("Saving configuration to '{}'", pimpl_->path_);
    1758             : 
    1759        2449 :     if (pimpl_->audiodriver_) {
    1760        2443 :         audioPreference.setVolumemic(pimpl_->audiodriver_->getCaptureGain());
    1761        2443 :         audioPreference.setVolumespkr(pimpl_->audiodriver_->getPlaybackGain());
    1762        2443 :         audioPreference.setCaptureMuted(pimpl_->audiodriver_->isCaptureMuted());
    1763        2443 :         audioPreference.setPlaybackMuted(pimpl_->audiodriver_->isPlaybackMuted());
    1764             :     }
    1765             : 
    1766             :     try {
    1767        2449 :         YAML::Emitter out;
    1768             : 
    1769             :         // FIXME maybe move this into accountFactory?
    1770        2449 :         out << YAML::BeginMap << YAML::Key << "accounts";
    1771        2449 :         out << YAML::Value << YAML::BeginSeq;
    1772             : 
    1773        8490 :         for (const auto& account : accountFactory.getAllAccounts()) {
    1774        6041 :             if (auto jamiAccount = std::dynamic_pointer_cast<JamiAccount>(account)) {
    1775        5860 :                 auto accountConfig = jamiAccount->getPath() / "config.yml";
    1776        5860 :                 if (not std::filesystem::is_regular_file(accountConfig)) {
    1777           0 :                     saveConfig(jamiAccount);
    1778             :                 }
    1779        5860 :             } else {
    1780         181 :                 account->config().serialize(out);
    1781        6041 :             }
    1782        2449 :         }
    1783        2449 :         out << YAML::EndSeq;
    1784             : 
    1785             :         // FIXME: this is a hack until we get rid of accountOrder
    1786        2449 :         preferences.verifyAccountOrder(getAccountList());
    1787        2449 :         preferences.serialize(out);
    1788        2449 :         voipPreferences.serialize(out);
    1789        2449 :         audioPreference.serialize(out);
    1790             : #ifdef ENABLE_VIDEO
    1791        2449 :         videoPreferences.serialize(out);
    1792             : #endif
    1793             : #ifdef ENABLE_PLUGIN
    1794        2449 :         pluginPreferences.serialize(out);
    1795             : #endif
    1796             : 
    1797        2449 :         std::lock_guard lock(dhtnet::fileutils::getFileLock(pimpl_->path_));
    1798        2449 :         std::ofstream fout(pimpl_->path_);
    1799        2449 :         fout.write(out.c_str(), static_cast<long>(out.size()));
    1800        2449 :     } catch (const YAML::Exception& e) {
    1801           0 :         JAMI_ERROR("[config] YAML error: {}", e.what());
    1802           0 :     } catch (const std::runtime_error& e) {
    1803           0 :         JAMI_ERROR("[config] {}", e.what());
    1804           0 :     }
    1805        2449 : }
    1806             : 
    1807             : // THREAD=Main | VoIPLink
    1808             : void
    1809           0 : Manager::playDtmf(char code)
    1810             : {
    1811           0 :     stopTone();
    1812             : 
    1813           0 :     if (not voipPreferences.getPlayDtmf()) {
    1814           0 :         return;
    1815             :     }
    1816             : 
    1817             :     // length in milliseconds
    1818           0 :     int pulselen = voipPreferences.getPulseLength();
    1819             : 
    1820           0 :     if (pulselen == 0) {
    1821           0 :         return;
    1822             :     }
    1823             : 
    1824           0 :     std::lock_guard lock(pimpl_->audioLayerMutex_);
    1825             : 
    1826             :     // fast return, no sound, so no dtmf
    1827           0 :     if (not pimpl_->audiodriver_ or not pimpl_->dtmfKey_) {
    1828           0 :         return;
    1829             :     }
    1830             : 
    1831           0 :     std::shared_ptr<AudioDeviceGuard> audioGuard = startAudioStream(AudioDeviceType::PLAYBACK);
    1832           0 :     if (not pimpl_->audiodriver_->waitForStart(std::chrono::seconds(1))) {
    1833           0 :         JAMI_ERROR("[audio] Failed to start audio layer for DTMF");
    1834           0 :         return;
    1835             :     }
    1836             : 
    1837             :     // number of data sampling in one pulselen depends on samplerate
    1838             :     // size (n sampling) = time_ms * sampling/s
    1839             :     //                     ---------------------
    1840             :     //                            ms/s
    1841           0 :     unsigned size = (unsigned) ((pulselen * (long) pimpl_->audiodriver_->getSampleRate()) / 1000ul);
    1842           0 :     if (!pimpl_->dtmfBuf_ or pimpl_->dtmfBuf_->getFrameSize() != size)
    1843           0 :         pimpl_->dtmfBuf_ = std::make_shared<AudioFrame>(pimpl_->audiodriver_->getFormat(), size);
    1844             : 
    1845             :     // Handle dtmf
    1846           0 :     pimpl_->dtmfKey_->startTone(code);
    1847             : 
    1848             :     // copy the sound
    1849           0 :     if (pimpl_->dtmfKey_->generateDTMF(pimpl_->dtmfBuf_->pointer())) {
    1850             :         // Put buffer to urgentRingBuffer
    1851             :         // put the size in bytes…
    1852             :         // so size * 1 channel (mono) * sizeof (bytes for the data)
    1853             :         // audiolayer->flushUrgent();
    1854             : 
    1855           0 :         pimpl_->audiodriver_->putUrgent(pimpl_->dtmfBuf_);
    1856             :     }
    1857             : 
    1858           0 :     auto dtmfTimer = std::make_unique<asio::steady_timer>(*pimpl_->ioContext_, std::chrono::milliseconds(pulselen));
    1859           0 :     dtmfTimer->async_wait([this, audioGuard, t = dtmfTimer.get()](const asio::error_code& ec) {
    1860           0 :         if (ec)
    1861           0 :             return;
    1862           0 :         JAMI_DBG("End of dtmf");
    1863           0 :         std::lock_guard lock(pimpl_->audioLayerMutex_);
    1864           0 :         if (pimpl_->dtmfTimer_.get() == t)
    1865           0 :             pimpl_->dtmfTimer_.reset();
    1866           0 :     });
    1867           0 :     if (pimpl_->dtmfTimer_)
    1868           0 :         pimpl_->dtmfTimer_->cancel();
    1869           0 :     pimpl_->dtmfTimer_ = std::move(dtmfTimer);
    1870           0 : }
    1871             : 
    1872             : // Multi-thread
    1873             : bool
    1874         115 : Manager::incomingCallsWaiting()
    1875             : {
    1876         115 :     std::lock_guard m(pimpl_->waitingCallsMutex_);
    1877         230 :     return not pimpl_->waitingCalls_.empty();
    1878         115 : }
    1879             : 
    1880             : void
    1881         100 : Manager::incomingCall(const std::string& accountId, Call& call)
    1882             : {
    1883         100 :     if (not accountId.empty()) {
    1884         100 :         pimpl_->stripSipPrefix(call);
    1885             :     }
    1886             : 
    1887         100 :     auto const& account = getAccount(accountId);
    1888         100 :     if (not account) {
    1889           0 :         JAMI_ERROR("Incoming call {} on unknown account {}", call.getCallId(), accountId);
    1890           0 :         return;
    1891             :     }
    1892             : 
    1893             :     // Process the call.
    1894         100 :     pimpl_->processIncomingCall(accountId, call);
    1895         100 : }
    1896             : 
    1897             : void
    1898           0 : Manager::incomingMessage(const std::string& accountId,
    1899             :                          const std::string& callId,
    1900             :                          const std::string& from,
    1901             :                          const std::map<std::string, std::string>& messages)
    1902             : {
    1903           0 :     auto account = getAccount(accountId);
    1904           0 :     if (not account) {
    1905           0 :         return;
    1906             :     }
    1907           0 :     if (auto call = account->getCall(callId)) {
    1908           0 :         if (call->isConferenceParticipant()) {
    1909           0 :             if (auto conf = call->getConference()) {
    1910             :                 // filter out vcards messages  as they could be resent by master as its own vcard
    1911             :                 // TODO. Implement a protocol to handle vcard messages
    1912           0 :                 bool sendToOtherParicipants = true;
    1913           0 :                 for (auto& message : messages) {
    1914           0 :                     if (message.first.find("x-ring/ring.profile.vcard") != std::string::npos) {
    1915           0 :                         sendToOtherParicipants = false;
    1916             :                     }
    1917             :                 }
    1918           0 :                 if (sendToOtherParicipants) {
    1919           0 :                     pimpl_->sendTextMessageToConference(*conf, messages, from);
    1920             :                 }
    1921             : 
    1922             :                 // in case of a conference we must notify client using conference id
    1923           0 :                 emitSignal<libjami::CallSignal::IncomingMessage>(accountId, conf->getConfId(), from, messages);
    1924             :             } else {
    1925           0 :                 JAMI_ERROR("[call:{}] No conference associated to call", callId);
    1926           0 :             }
    1927             :         } else {
    1928           0 :             emitSignal<libjami::CallSignal::IncomingMessage>(accountId, callId, from, messages);
    1929             :         }
    1930           0 :     }
    1931           0 : }
    1932             : 
    1933             : void
    1934           0 : Manager::sendCallTextMessage(const std::string& accountId,
    1935             :                              const std::string& callID,
    1936             :                              const std::map<std::string, std::string>& messages,
    1937             :                              const std::string& from,
    1938             :                              bool /*isMixed TODO: use it */)
    1939             : {
    1940           0 :     auto account = getAccount(accountId);
    1941           0 :     if (not account) {
    1942           0 :         return;
    1943             :     }
    1944             : 
    1945           0 :     if (auto conf = account->getConference(callID)) {
    1946           0 :         pimpl_->sendTextMessageToConference(*conf, messages, from);
    1947           0 :     } else if (auto call = account->getCall(callID)) {
    1948           0 :         if (call->isConferenceParticipant()) {
    1949           0 :             if (auto conf = call->getConference()) {
    1950           0 :                 pimpl_->sendTextMessageToConference(*conf, messages, from);
    1951             :             } else {
    1952           0 :                 JAMI_ERROR("[call:{}] No conference associated to call", callID);
    1953           0 :             }
    1954             :         } else {
    1955             :             try {
    1956           0 :                 call->sendTextMessage(messages, from);
    1957           0 :             } catch (const im::InstantMessageException& e) {
    1958           0 :                 JAMI_ERR("Failed to send message to call %s: %s", call->getCallId().c_str(), e.what());
    1959           0 :             }
    1960             :         }
    1961             :     } else {
    1962           0 :         JAMI_ERR("Failed to send message to %s: nonexistent call ID", callID.c_str());
    1963           0 :     }
    1964           0 : }
    1965             : 
    1966             : // THREAD=VoIP CALL=Outgoing
    1967             : void
    1968          89 : Manager::peerAnsweredCall(Call& call)
    1969             : {
    1970          89 :     const auto& callId = call.getCallId();
    1971          89 :     JAMI_DBG("[call:%s] Peer answered", callId.c_str());
    1972             : 
    1973             :     // The if statement is useful only if we sent two calls at the same time.
    1974          89 :     if (isCurrentCall(call))
    1975           0 :         stopTone();
    1976             : 
    1977          89 :     addAudio(call);
    1978             : 
    1979          89 :     if (pimpl_->audiodriver_) {
    1980          89 :         std::lock_guard lock(pimpl_->audioLayerMutex_);
    1981          89 :         getRingBufferPool().flushAllBuffers();
    1982          89 :         pimpl_->audiodriver_->flushUrgent();
    1983          89 :     }
    1984             : 
    1985          89 :     if (audioPreference.getIsAlwaysRecording()) {
    1986           0 :         auto result = call.toggleRecording();
    1987           0 :         emitSignal<libjami::CallSignal::RecordPlaybackFilepath>(callId, call.getPath());
    1988           0 :         emitSignal<libjami::CallSignal::RecordingStateChanged>(callId, result);
    1989             :     }
    1990          89 : }
    1991             : 
    1992             : // THREAD=VoIP Call=Outgoing
    1993             : void
    1994         187 : Manager::peerRingingCall(Call& call)
    1995             : {
    1996         748 :     JAMI_LOG("[call:{}] Peer ringing", call.getCallId());
    1997         187 :     if (!hasCurrentCall())
    1998          60 :         ringback();
    1999         187 : }
    2000             : 
    2001             : // THREAD=VoIP Call=Outgoing/Ingoing
    2002             : void
    2003          99 : Manager::peerHungupCall(Call& call)
    2004             : {
    2005          99 :     const auto& callId = call.getCallId();
    2006         396 :     JAMI_LOG("[call:{}] Peer hung up", callId);
    2007             : 
    2008          99 :     if (call.isConferenceParticipant()) {
    2009          11 :         removeParticipant(call);
    2010          88 :     } else if (isCurrentCall(call)) {
    2011           9 :         stopTone();
    2012           9 :         pimpl_->unsetCurrentCall();
    2013             :     }
    2014             : 
    2015          99 :     call.peerHungup();
    2016             : 
    2017          99 :     pimpl_->removeWaitingCall(callId);
    2018          99 :     if (not incomingCallsWaiting())
    2019          67 :         stopTone();
    2020             : 
    2021          99 :     removeAudio(call);
    2022          99 : }
    2023             : 
    2024             : // THREAD=VoIP
    2025             : void
    2026           0 : Manager::callBusy(Call& call)
    2027             : {
    2028           0 :     JAMI_LOG("[call:{}] Busy", call.getCallId());
    2029             : 
    2030           0 :     if (isCurrentCall(call)) {
    2031           0 :         pimpl_->unsetCurrentCall();
    2032             :     }
    2033             : 
    2034           0 :     pimpl_->removeWaitingCall(call.getCallId());
    2035           0 :     if (not incomingCallsWaiting())
    2036           0 :         stopTone();
    2037           0 : }
    2038             : 
    2039             : // THREAD=VoIP
    2040             : void
    2041          95 : Manager::callFailure(Call& call)
    2042             : {
    2043         380 :     JAMI_LOG("[call:{}] {} failed", call.getCallId(), call.isSubcall() ? "Sub-call" : "Parent call");
    2044             : 
    2045          95 :     if (isCurrentCall(call)) {
    2046           4 :         pimpl_->unsetCurrentCall();
    2047             :     }
    2048             : 
    2049          95 :     if (call.isConferenceParticipant()) {
    2050           8 :         JAMI_LOG("[call:{}] Participating in conference, removing participant", call.getCallId());
    2051             :         // remove this participant
    2052           2 :         removeParticipant(call);
    2053             :     }
    2054             : 
    2055          95 :     pimpl_->removeWaitingCall(call.getCallId());
    2056          95 :     if (not call.isSubcall() && not incomingCallsWaiting())
    2057           6 :         stopTone();
    2058          95 :     removeAudio(call);
    2059          95 : }
    2060             : 
    2061             : /**
    2062             :  * Multi Thread
    2063             :  */
    2064             : void
    2065         496 : Manager::stopTone()
    2066             : {
    2067         496 :     if (not voipPreferences.getPlayTones())
    2068           0 :         return;
    2069             : 
    2070         496 :     pimpl_->toneCtrl_.stop();
    2071         496 :     pimpl_->toneDeviceGuard_.reset();
    2072             : }
    2073             : 
    2074             : /**
    2075             :  * Multi Thread
    2076             :  */
    2077             : void
    2078           0 : Manager::playTone()
    2079             : {
    2080           0 :     pimpl_->playATone(Tone::ToneId::DIALTONE);
    2081           0 : }
    2082             : 
    2083             : /**
    2084             :  * Multi Thread
    2085             :  */
    2086             : void
    2087           0 : Manager::playToneWithMessage()
    2088             : {
    2089           0 :     pimpl_->playATone(Tone::ToneId::CONGESTION);
    2090           0 : }
    2091             : 
    2092             : /**
    2093             :  * Multi Thread
    2094             :  */
    2095             : void
    2096           0 : Manager::congestion()
    2097             : {
    2098           0 :     pimpl_->playATone(Tone::ToneId::CONGESTION);
    2099           0 : }
    2100             : 
    2101             : /**
    2102             :  * Multi Thread
    2103             :  */
    2104             : void
    2105         114 : Manager::ringback()
    2106             : {
    2107         114 :     pimpl_->playATone(Tone::ToneId::RINGTONE);
    2108         114 : }
    2109             : 
    2110             : /**
    2111             :  * Multi Thread
    2112             :  */
    2113             : void
    2114          54 : Manager::playRingtone(const std::string& accountID)
    2115             : {
    2116          54 :     const auto account = getAccount(accountID);
    2117          54 :     if (!account) {
    2118           0 :         JAMI_WARNING("[account:{}] Invalid account for ringtone", accountID);
    2119           0 :         return;
    2120             :     }
    2121             : 
    2122          54 :     if (!account->getRingtoneEnabled()) {
    2123           0 :         ringback();
    2124           0 :         return;
    2125             :     }
    2126             : 
    2127             :     {
    2128          54 :         std::lock_guard lock(pimpl_->audioLayerMutex_);
    2129             : 
    2130          54 :         if (not pimpl_->audiodriver_) {
    2131           0 :             JAMI_ERROR("[audio] No audio layer for ringtone");
    2132           0 :             return;
    2133             :         }
    2134             :         // start audio if not started AND flush all buffers (main and urgent)
    2135          54 :         auto oldGuard = std::move(pimpl_->toneDeviceGuard_);
    2136          54 :         pimpl_->toneDeviceGuard_ = startAudioStream(AudioDeviceType::RINGTONE);
    2137          54 :         auto format = pimpl_->audiodriver_->getFormat();
    2138          54 :         pimpl_->toneCtrl_.setSampleRate(format.sample_rate, format.sampleFormat);
    2139          54 :     }
    2140             : 
    2141          54 :     if (not pimpl_->toneCtrl_.setAudioFile(account->getRingtonePath().string()))
    2142          54 :         ringback();
    2143          54 : }
    2144             : 
    2145             : std::shared_ptr<AudioLoop>
    2146           0 : Manager::getTelephoneTone()
    2147             : {
    2148           0 :     return pimpl_->toneCtrl_.getTelephoneTone();
    2149             : }
    2150             : 
    2151             : std::shared_ptr<AudioLoop>
    2152           0 : Manager::getTelephoneFile()
    2153             : {
    2154           0 :     return pimpl_->toneCtrl_.getTelephoneFile();
    2155             : }
    2156             : 
    2157             : /**
    2158             :  * Set input audio plugin
    2159             :  */
    2160             : void
    2161           0 : Manager::setAudioPlugin(const std::string& audioPlugin)
    2162             : {
    2163             :     {
    2164           0 :         std::lock_guard lock(pimpl_->audioLayerMutex_);
    2165           0 :         audioPreference.setAlsaPlugin(audioPlugin);
    2166           0 :         pimpl_->audiodriver_.reset();
    2167           0 :         pimpl_->initAudioDriver();
    2168           0 :     }
    2169             :     // Recreate audio driver with new settings
    2170           0 :     saveConfig();
    2171           0 : }
    2172             : 
    2173             : /**
    2174             :  * Set audio output device
    2175             :  */
    2176             : void
    2177           0 : Manager::setAudioDevice(int index, AudioDeviceType type)
    2178             : {
    2179           0 :     std::lock_guard lock(pimpl_->audioLayerMutex_);
    2180             : 
    2181           0 :     if (not pimpl_->audiodriver_) {
    2182           0 :         JAMI_ERROR("[audio] Uninitialized audio driver");
    2183           0 :         return;
    2184             :     }
    2185           0 :     if (pimpl_->getCurrentDeviceIndex(type) == index) {
    2186           0 :         JAMI_DEBUG("[audio] Audio device already selected, doing nothing");
    2187           0 :         return;
    2188             :     }
    2189             : 
    2190           0 :     pimpl_->audiodriver_->updatePreference(audioPreference, index, type);
    2191             : 
    2192             :     // Recreate audio driver with new settings
    2193           0 :     pimpl_->audiodriver_.reset();
    2194           0 :     pimpl_->initAudioDriver();
    2195           0 :     saveConfig();
    2196           0 : }
    2197             : 
    2198             : /**
    2199             :  * Get list of supported audio output device
    2200             :  */
    2201             : std::vector<std::string>
    2202           0 : Manager::getAudioOutputDeviceList()
    2203             : {
    2204           0 :     std::lock_guard lock(pimpl_->audioLayerMutex_);
    2205             : 
    2206           0 :     if (not pimpl_->audiodriver_) {
    2207           0 :         JAMI_ERROR("[audio] Uninitialized audio layer");
    2208           0 :         return {};
    2209             :     }
    2210             : 
    2211           0 :     return pimpl_->audiodriver_->getPlaybackDeviceList();
    2212           0 : }
    2213             : 
    2214             : /**
    2215             :  * Get list of supported audio input device
    2216             :  */
    2217             : std::vector<std::string>
    2218           0 : Manager::getAudioInputDeviceList()
    2219             : {
    2220           0 :     std::lock_guard lock(pimpl_->audioLayerMutex_);
    2221             : 
    2222           0 :     if (not pimpl_->audiodriver_) {
    2223           0 :         JAMI_ERROR("[audio] Uninitialized audio layer");
    2224           0 :         return {};
    2225             :     }
    2226             : 
    2227           0 :     return pimpl_->audiodriver_->getCaptureDeviceList();
    2228           0 : }
    2229             : 
    2230             : /**
    2231             :  * Get string array representing integer indexes of output and input device
    2232             :  */
    2233             : std::vector<std::string>
    2234           0 : Manager::getCurrentAudioDevicesIndex()
    2235             : {
    2236           0 :     std::lock_guard lock(pimpl_->audioLayerMutex_);
    2237           0 :     if (not pimpl_->audiodriver_) {
    2238           0 :         JAMI_ERROR("[audio] Uninitialized audio layer");
    2239           0 :         return {};
    2240             :     }
    2241             : 
    2242           0 :     return {std::to_string(pimpl_->audiodriver_->getIndexPlayback()),
    2243           0 :             std::to_string(pimpl_->audiodriver_->getIndexCapture()),
    2244           0 :             std::to_string(pimpl_->audiodriver_->getIndexRingtone())};
    2245           0 : }
    2246             : 
    2247         603 : AudioDeviceGuard::AudioDeviceGuard(Manager& manager, AudioDeviceType type)
    2248         603 :     : manager_(manager)
    2249         603 :     , type_(type)
    2250             : {
    2251         603 :     auto streamId = (unsigned) type;
    2252         603 :     if (streamId >= manager_.pimpl_->audioStreamUsers_.size())
    2253           0 :         throw std::invalid_argument("Invalid audio device type");
    2254         603 :     if (manager_.pimpl_->audioStreamUsers_[streamId]++ == 0) {
    2255         227 :         if (auto layer = manager_.getAudioDriver())
    2256         227 :             layer->startStream(type);
    2257             :     }
    2258         603 : }
    2259             : 
    2260           0 : AudioDeviceGuard::AudioDeviceGuard(Manager& manager, const std::string& captureDevice)
    2261           0 :     : manager_(manager)
    2262           0 :     , type_(AudioDeviceType::CAPTURE)
    2263           0 :     , captureDevice_(captureDevice)
    2264             : {
    2265           0 :     std::lock_guard lock(manager_.pimpl_->audioDeviceUsersMutex_);
    2266           0 :     auto& users = manager_.pimpl_->audioDeviceUsers_[captureDevice];
    2267           0 :     if (users++ == 0) {
    2268           0 :         if (auto layer = manager_.getAudioDriver()) {
    2269           0 :             layer->startCaptureStream(captureDevice);
    2270           0 :         }
    2271             :     }
    2272           0 : }
    2273             : 
    2274        1206 : AudioDeviceGuard::~AudioDeviceGuard()
    2275             : {
    2276         603 :     if (captureDevice_.empty()) {
    2277         603 :         auto streamId = (unsigned) type_;
    2278         603 :         if (--manager_.pimpl_->audioStreamUsers_[streamId] == 0) {
    2279         227 :             if (auto layer = manager_.getAudioDriver())
    2280         227 :                 layer->stopStream(type_);
    2281             :         }
    2282             :     } else {
    2283           0 :         std::lock_guard lock(manager_.pimpl_->audioDeviceUsersMutex_);
    2284           0 :         auto it = manager_.pimpl_->audioDeviceUsers_.find(captureDevice_);
    2285           0 :         if (it != manager_.pimpl_->audioDeviceUsers_.end()) {
    2286           0 :             if (--it->second == 0) {
    2287           0 :                 if (auto layer = manager_.getAudioDriver())
    2288           0 :                     layer->stopCaptureStream(captureDevice_);
    2289           0 :                 manager_.pimpl_->audioDeviceUsers_.erase(it);
    2290             :             }
    2291             :         }
    2292           0 :     }
    2293         603 : }
    2294             : 
    2295             : bool
    2296           0 : Manager::getIsAlwaysRecording() const
    2297             : {
    2298           0 :     return audioPreference.getIsAlwaysRecording();
    2299             : }
    2300             : 
    2301             : void
    2302           0 : Manager::setIsAlwaysRecording(bool isAlwaysRec)
    2303             : {
    2304           0 :     audioPreference.setIsAlwaysRecording(isAlwaysRec);
    2305           0 :     saveConfig();
    2306           0 : }
    2307             : 
    2308             : bool
    2309           4 : Manager::toggleRecordingCall(const std::string& accountId, const std::string& id)
    2310             : {
    2311           4 :     bool result = false;
    2312           4 :     if (auto account = getAccount(accountId)) {
    2313           4 :         std::shared_ptr<Recordable> rec;
    2314           4 :         if (auto conf = account->getConference(id)) {
    2315           8 :             JAMI_DEBUG("[conf:{}] Toggling recording", id);
    2316           2 :             rec = conf;
    2317           2 :         } else if (auto call = account->getCall(id)) {
    2318           8 :             JAMI_DEBUG("[call:{}] Toggling recording", id);
    2319           2 :             rec = call;
    2320             :         } else {
    2321           0 :             JAMI_ERROR("Unable to find recordable instance {}", id);
    2322           0 :             return false;
    2323           6 :         }
    2324           4 :         result = rec->toggleRecording();
    2325           4 :         emitSignal<libjami::CallSignal::RecordPlaybackFilepath>(id, rec->getPath());
    2326           4 :         emitSignal<libjami::CallSignal::RecordingStateChanged>(id, result);
    2327           8 :     }
    2328           4 :     return result;
    2329             : }
    2330             : 
    2331             : bool
    2332           0 : Manager::startRecordedFilePlayback(const std::string& filepath)
    2333             : {
    2334           0 :     JAMI_DEBUG("[audio] Start recorded file playback: {}", filepath);
    2335             : 
    2336             :     {
    2337           0 :         std::lock_guard lock(pimpl_->audioLayerMutex_);
    2338             : 
    2339           0 :         if (not pimpl_->audiodriver_) {
    2340           0 :             JAMI_ERROR("[audio] No audio layer for recorded file playback");
    2341           0 :             return false;
    2342             :         }
    2343             : 
    2344           0 :         auto oldGuard = std::move(pimpl_->toneDeviceGuard_);
    2345           0 :         pimpl_->toneDeviceGuard_ = startAudioStream(AudioDeviceType::RINGTONE);
    2346           0 :         auto format = pimpl_->audiodriver_->getFormat();
    2347           0 :         pimpl_->toneCtrl_.setSampleRate(format.sample_rate, format.sampleFormat);
    2348           0 :     }
    2349             : 
    2350           0 :     return pimpl_->toneCtrl_.setAudioFile(filepath);
    2351             : }
    2352             : 
    2353             : void
    2354           0 : Manager::recordingPlaybackSeek(const double value)
    2355             : {
    2356           0 :     pimpl_->toneCtrl_.seek(value);
    2357           0 : }
    2358             : 
    2359             : void
    2360           0 : Manager::stopRecordedFilePlayback()
    2361             : {
    2362           0 :     JAMI_DEBUG("[audio] Stop recorded file playback");
    2363             : 
    2364           0 :     pimpl_->toneCtrl_.stopAudioFile();
    2365           0 :     pimpl_->toneDeviceGuard_.reset();
    2366           0 : }
    2367             : 
    2368             : void
    2369           0 : Manager::setHistoryLimit(int days)
    2370             : {
    2371           0 :     JAMI_DEBUG("[config] Set history limit to {} days", days);
    2372           0 :     preferences.setHistoryLimit(days);
    2373           0 :     saveConfig();
    2374           0 : }
    2375             : 
    2376             : int
    2377           0 : Manager::getHistoryLimit() const
    2378             : {
    2379           0 :     return preferences.getHistoryLimit();
    2380             : }
    2381             : 
    2382             : void
    2383           0 : Manager::setRingingTimeout(std::chrono::seconds timeout)
    2384             : {
    2385           0 :     JAMI_DEBUG("[config] Set ringing timeout to {} seconds", timeout);
    2386           0 :     preferences.setRingingTimeout(timeout);
    2387           0 :     saveConfig();
    2388           0 : }
    2389             : 
    2390             : std::chrono::seconds
    2391         100 : Manager::getRingingTimeout() const
    2392             : {
    2393         100 :     return preferences.getRingingTimeout();
    2394             : }
    2395             : 
    2396             : bool
    2397           0 : Manager::setAudioManager(const std::string& api)
    2398             : {
    2399             :     {
    2400           0 :         std::lock_guard lock(pimpl_->audioLayerMutex_);
    2401             : 
    2402           0 :         if (not pimpl_->audiodriver_)
    2403           0 :             return false;
    2404             : 
    2405           0 :         if (api == audioPreference.getAudioApi()) {
    2406           0 :             JAMI_DEBUG("[audio] Audio manager '{}' already in use", api);
    2407           0 :             return true;
    2408             :         }
    2409           0 :     }
    2410             : 
    2411             :     {
    2412           0 :         std::lock_guard lock(pimpl_->audioLayerMutex_);
    2413           0 :         audioPreference.setAudioApi(api);
    2414           0 :         pimpl_->audiodriver_.reset();
    2415           0 :         pimpl_->initAudioDriver();
    2416           0 :     }
    2417             : 
    2418           0 :     saveConfig();
    2419             : 
    2420             :     // ensure that we completed the transition (i.e. no fallback was used)
    2421           0 :     return api == audioPreference.getAudioApi();
    2422             : }
    2423             : 
    2424             : std::string
    2425           0 : Manager::getAudioManager() const
    2426             : {
    2427           0 :     return audioPreference.getAudioApi();
    2428             : }
    2429             : 
    2430             : int
    2431           0 : Manager::getAudioInputDeviceIndex(const std::string& name)
    2432             : {
    2433           0 :     std::lock_guard lock(pimpl_->audioLayerMutex_);
    2434             : 
    2435           0 :     if (not pimpl_->audiodriver_) {
    2436           0 :         JAMI_ERROR("[audio] Uninitialized audio layer");
    2437           0 :         return 0;
    2438             :     }
    2439             : 
    2440           0 :     return pimpl_->audiodriver_->getAudioDeviceIndex(name, AudioDeviceType::CAPTURE);
    2441           0 : }
    2442             : 
    2443             : int
    2444           0 : Manager::getAudioOutputDeviceIndex(const std::string& name)
    2445             : {
    2446           0 :     std::lock_guard lock(pimpl_->audioLayerMutex_);
    2447             : 
    2448           0 :     if (not pimpl_->audiodriver_) {
    2449           0 :         JAMI_ERROR("[audio] Uninitialized audio layer");
    2450           0 :         return 0;
    2451             :     }
    2452             : 
    2453           0 :     return pimpl_->audiodriver_->getAudioDeviceIndex(name, AudioDeviceType::PLAYBACK);
    2454           0 : }
    2455             : 
    2456             : std::string
    2457           0 : Manager::getCurrentAudioOutputPlugin() const
    2458             : {
    2459           0 :     return audioPreference.getAlsaPlugin();
    2460             : }
    2461             : 
    2462             : std::string
    2463           0 : Manager::getNoiseSuppressState() const
    2464             : {
    2465           0 :     return audioPreference.getNoiseReduce();
    2466             : }
    2467             : 
    2468             : void
    2469           0 : Manager::setNoiseSuppressState(const std::string& state)
    2470             : {
    2471           0 :     audioPreference.setNoiseReduce(state);
    2472           0 : }
    2473             : 
    2474             : std::string
    2475           0 : Manager::getEchoCancellationState() const
    2476             : {
    2477           0 :     return audioPreference.getEchoCanceller();
    2478             : }
    2479             : 
    2480             : void
    2481           0 : Manager::setEchoCancellationState(const std::string& state)
    2482             : {
    2483           0 :     audioPreference.setEchoCancel(state);
    2484           0 : }
    2485             : 
    2486             : bool
    2487           0 : Manager::getVoiceActivityDetectionState() const
    2488             : {
    2489           0 :     return audioPreference.getVadEnabled();
    2490             : }
    2491             : 
    2492             : void
    2493           0 : Manager::setVoiceActivityDetectionState(bool state)
    2494             : {
    2495           0 :     audioPreference.setVad(state);
    2496           0 : }
    2497             : 
    2498             : bool
    2499           0 : Manager::isAGCEnabled() const
    2500             : {
    2501           0 :     return audioPreference.isAGCEnabled();
    2502             : }
    2503             : 
    2504             : void
    2505           0 : Manager::setAGCState(bool state)
    2506             : {
    2507           0 :     audioPreference.setAGCState(state);
    2508           0 : }
    2509             : 
    2510             : /**
    2511             :  * Initialization: Main Thread
    2512             :  */
    2513             : void
    2514          32 : Manager::ManagerPimpl::initAudioDriver()
    2515             : {
    2516          32 :     audiodriver_.reset(base_.audioPreference.createAudioLayer());
    2517          32 :     constexpr std::array<AudioDeviceType, 3> TYPES {AudioDeviceType::CAPTURE,
    2518             :                                                     AudioDeviceType::PLAYBACK,
    2519             :                                                     AudioDeviceType::RINGTONE};
    2520         128 :     for (const auto& type : TYPES)
    2521          96 :         if (audioStreamUsers_[(unsigned) type])
    2522           0 :             audiodriver_->startStream(type);
    2523          32 : }
    2524             : 
    2525             : // Internal helper method
    2526             : void
    2527         100 : Manager::ManagerPimpl::stripSipPrefix(Call& incomCall)
    2528             : {
    2529             :     // strip sip: which is not required and causes confusion with IP-to-IP calls
    2530             :     // when placing new call from history.
    2531         100 :     std::string peerNumber(incomCall.getPeerNumber());
    2532             : 
    2533         100 :     const char SIP_PREFIX[] = "sip:";
    2534         100 :     size_t startIndex = peerNumber.find(SIP_PREFIX);
    2535             : 
    2536         100 :     if (startIndex != std::string::npos)
    2537           0 :         incomCall.setPeerNumber(peerNumber.substr(startIndex + sizeof(SIP_PREFIX) - 1));
    2538         100 : }
    2539             : 
    2540             : // Internal helper method
    2541             : void
    2542         100 : Manager::ManagerPimpl::processIncomingCall(const std::string& accountId, Call& incomCall)
    2543             : {
    2544         100 :     base_.stopTone();
    2545             : 
    2546         100 :     auto incomCallId = incomCall.getCallId();
    2547         100 :     auto currentCall = base_.getCurrentCall();
    2548             : 
    2549         100 :     auto account = incomCall.getAccount().lock();
    2550         100 :     if (!account) {
    2551           0 :         JAMI_ERROR("[call:{}] No account detected", incomCallId);
    2552           0 :         return;
    2553             :     }
    2554             : 
    2555         100 :     auto username = incomCall.toUsername();
    2556         100 :     if (account->getAccountType() == ACCOUNT_TYPE_JAMI && username.find('/') != std::string::npos) {
    2557             :         // Avoid to do heavy stuff in SIPVoIPLink's transaction_request_cb
    2558          10 :         dht::ThreadPool::io().run([account, incomCallId, username]() {
    2559          10 :             if (auto jamiAccount = std::dynamic_pointer_cast<JamiAccount>(account))
    2560          10 :                 jamiAccount->handleIncomingConversationCall(incomCallId, username);
    2561          10 :         });
    2562          10 :         return;
    2563             :     }
    2564             : 
    2565          90 :     auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(incomCall.getMediaAttributeList());
    2566             : 
    2567          90 :     if (mediaList.empty())
    2568           0 :         JAMI_WARNING("Incoming call {} has an empty media list", incomCallId);
    2569             : 
    2570         360 :     JAMI_DEBUG("Incoming call {} on account {} with {} media", incomCallId, accountId, mediaList.size());
    2571             : 
    2572          90 :     emitSignal<libjami::CallSignal::IncomingCall>(accountId, incomCallId, incomCall.getPeerNumber(), mediaList);
    2573             : 
    2574          90 :     if (not base_.hasCurrentCall()) {
    2575          54 :         incomCall.setState(Call::ConnectionState::RINGING);
    2576             : #if !(defined(TARGET_OS_IOS) && TARGET_OS_IOS)
    2577          54 :         if (not account->isRendezVous())
    2578          54 :             base_.playRingtone(accountId);
    2579             : #endif
    2580             :     } else {
    2581          36 :         if (account->isDenySecondCallEnabled()) {
    2582           0 :             base_.refuseCall(account->getAccountID(), incomCallId);
    2583           0 :             return;
    2584             :         }
    2585             :     }
    2586             : 
    2587          90 :     addWaitingCall(incomCallId);
    2588             : 
    2589          90 :     if (account->isRendezVous()) {
    2590           0 :         dht::ThreadPool::io().run([this, account, incomCall = incomCall.shared_from_this()] {
    2591           0 :             base_.acceptCall(*incomCall);
    2592             : 
    2593           0 :             for (const auto& callId : account->getCallList()) {
    2594           0 :                 if (auto call = account->getCall(callId)) {
    2595           0 :                     if (call->getState() != Call::CallState::ACTIVE)
    2596           0 :                         continue;
    2597           0 :                     if (call != incomCall) {
    2598           0 :                         if (auto conf = call->getConference()) {
    2599           0 :                             base_.addSubCall(*incomCall, *conf);
    2600             :                         } else {
    2601           0 :                             base_.joinParticipant(account->getAccountID(),
    2602           0 :                                                   incomCall->getCallId(),
    2603           0 :                                                   account->getAccountID(),
    2604             :                                                   call->getCallId(),
    2605             :                                                   false);
    2606           0 :                         }
    2607           0 :                         return;
    2608             :                     }
    2609           0 :                 }
    2610           0 :             }
    2611             : 
    2612             :             // First call
    2613           0 :             auto conf = std::make_shared<Conference>(account);
    2614           0 :             account->attach(conf);
    2615           0 :             emitSignal<libjami::CallSignal::ConferenceCreated>(account->getAccountID(), "", conf->getConfId());
    2616             : 
    2617             :             // Bind calls according to their state
    2618           0 :             bindCallToConference(*incomCall, *conf);
    2619           0 :             conf->detachHost();
    2620           0 :             emitSignal<libjami::CallSignal::ConferenceChanged>(account->getAccountID(),
    2621           0 :                                                                conf->getConfId(),
    2622             :                                                                conf->getStateStr());
    2623           0 :         });
    2624          90 :     } else if (autoAnswer_ || account->isAutoAnswerEnabled()) {
    2625           8 :         dht::ThreadPool::io().run([this, incomCall = incomCall.shared_from_this()] { base_.acceptCall(*incomCall); });
    2626          86 :     } else if (currentCall && currentCall->getCallId() != incomCallId) {
    2627             :         // Test if already calling this person
    2628          82 :         auto peerNumber = incomCall.getPeerNumber();
    2629          82 :         auto currentPeerNumber = currentCall->getPeerNumber();
    2630          82 :         string_replace(peerNumber, "@ring.dht", "");
    2631          82 :         string_replace(currentPeerNumber, "@ring.dht", "");
    2632          82 :         if (currentCall->getAccountId() == account->getAccountID() && currentPeerNumber == peerNumber) {
    2633           0 :             auto answerToCall = false;
    2634           0 :             auto downgradeToAudioOnly = currentCall->isAudioOnly() != incomCall.isAudioOnly();
    2635           0 :             if (downgradeToAudioOnly)
    2636             :                 // Accept the incoming audio only
    2637           0 :                 answerToCall = incomCall.isAudioOnly();
    2638             :             else
    2639             :                 // Accept the incoming call from the higher id number
    2640           0 :                 answerToCall = (account->getUsername().compare(peerNumber) < 0);
    2641             : 
    2642           0 :             if (answerToCall) {
    2643           0 :                 runOnMainThread([accountId = currentCall->getAccountId(),
    2644           0 :                                  currentCallID = currentCall->getCallId(),
    2645             :                                  incomCall = incomCall.shared_from_this()] {
    2646           0 :                     auto& mgr = Manager::instance();
    2647           0 :                     mgr.acceptCall(*incomCall);
    2648           0 :                     mgr.hangupCall(accountId, currentCallID);
    2649           0 :                 });
    2650             :             }
    2651             :         }
    2652          82 :     }
    2653         130 : }
    2654             : 
    2655             : AudioFormat
    2656         105 : Manager::hardwareAudioFormatChanged(AudioFormat format)
    2657             : {
    2658         105 :     return audioFormatUsed(format);
    2659             : }
    2660             : 
    2661             : AudioFormat
    2662         105 : Manager::audioFormatUsed(AudioFormat format)
    2663             : {
    2664         105 :     AudioFormat currentFormat = pimpl_->ringbufferpool_->getInternalAudioFormat();
    2665         105 :     if (currentFormat == format)
    2666         105 :         return format;
    2667             : 
    2668           0 :     JAMI_DEBUG("Audio format changed: {} → {}", currentFormat.toString(), format.toString());
    2669             : 
    2670           0 :     pimpl_->ringbufferpool_->setInternalAudioFormat(format);
    2671           0 :     pimpl_->toneCtrl_.setSampleRate(format.sample_rate, format.sampleFormat);
    2672           0 :     pimpl_->dtmfKey_.reset(new DTMF(format.sample_rate, format.sampleFormat));
    2673             : 
    2674           0 :     return format;
    2675             : }
    2676             : 
    2677             : void
    2678           0 : Manager::setAccountsOrder(const std::string& order)
    2679             : {
    2680           0 :     JAMI_LOG("Set accounts order: {}", order);
    2681           0 :     preferences.setAccountOrder(order);
    2682           0 :     saveConfig();
    2683           0 :     emitSignal<libjami::ConfigurationSignal::AccountsChanged>();
    2684           0 : }
    2685             : 
    2686             : std::vector<std::string>
    2687        3522 : Manager::getAccountList() const
    2688             : {
    2689             :     // Concatenate all account pointers in a single map
    2690        3522 :     std::vector<std::string> v;
    2691        3522 :     v.reserve(accountCount());
    2692       11501 :     for (const auto& account : getAllAccounts()) {
    2693        7979 :         v.emplace_back(account->getAccountID());
    2694        3522 :     }
    2695             : 
    2696        3522 :     return v;
    2697           0 : }
    2698             : 
    2699             : std::map<std::string, std::string>
    2700           1 : Manager::getAccountDetails(const std::string& accountID) const
    2701             : {
    2702           1 :     const auto account = getAccount(accountID);
    2703             : 
    2704           1 :     if (account) {
    2705           1 :         return account->getAccountDetails();
    2706             :     } else {
    2707           0 :         JAMI_ERROR("[account:{}] Unable to get account details on nonexistent account", accountID);
    2708             :         // return an empty map since unable to throw an exception to D-Bus
    2709           0 :         return {};
    2710             :     }
    2711           1 : }
    2712             : 
    2713             : std::map<std::string, std::string>
    2714           2 : Manager::getVolatileAccountDetails(const std::string& accountID) const
    2715             : {
    2716           2 :     const auto account = getAccount(accountID);
    2717             : 
    2718           2 :     if (account) {
    2719           2 :         return account->getVolatileAccountDetails();
    2720             :     } else {
    2721           0 :         JAMI_ERROR("[account:{}] Unable to get volatile account details on nonexistent account", accountID);
    2722           0 :         return {};
    2723             :     }
    2724           2 : }
    2725             : 
    2726             : void
    2727          16 : Manager::setAccountDetails(const std::string& accountID, const std::map<std::string, std::string>& details)
    2728             : {
    2729          64 :     JAMI_DEBUG("[account:{}] Set account details", accountID);
    2730             : 
    2731          16 :     auto account = getAccount(accountID);
    2732          16 :     if (not account) {
    2733           0 :         JAMI_ERROR("[account:{}] Unable to find account", accountID);
    2734           0 :         return;
    2735             :     }
    2736             : 
    2737             :     // Ignore if nothing has changed
    2738          16 :     if (details == account->getAccountDetails())
    2739           0 :         return;
    2740             : 
    2741             :     // Unregister before modifying any account information
    2742          16 :     account->doUnregister();
    2743             : 
    2744          16 :     account->setAccountDetails(details);
    2745             : 
    2746          16 :     if (account->isUsable())
    2747          16 :         account->doRegister();
    2748             :     else
    2749           0 :         account->doUnregister();
    2750             : 
    2751             :     // Update account details to the client side
    2752          16 :     emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(accountID, details);
    2753          16 : }
    2754             : 
    2755             : std::mt19937_64
    2756        1590 : Manager::getSeededRandomEngine()
    2757             : {
    2758        1590 :     std::lock_guard l(randMutex_);
    2759        3180 :     return dht::crypto::getDerivedRandomEngine(rand_);
    2760        1590 : }
    2761             : 
    2762             : std::string
    2763         793 : Manager::getNewAccountId()
    2764             : {
    2765         793 :     std::string random_id;
    2766             :     do {
    2767         793 :         random_id = to_hex_string(std::uniform_int_distribution<uint64_t>()(rand_));
    2768         793 :     } while (getAccount(random_id));
    2769         793 :     return random_id;
    2770           0 : }
    2771             : 
    2772             : std::string
    2773         795 : Manager::addAccount(const std::map<std::string, std::string>& details, const std::string& accountId)
    2774             : {
    2775             :     /** @todo Deal with both the accountMap_ and the Configuration */
    2776         795 :     auto newAccountID = accountId.empty() ? getNewAccountId() : accountId;
    2777             : 
    2778             :     // Get the type
    2779         795 :     std::string_view accountType;
    2780         795 :     auto typeIt = details.find(Conf::CONFIG_ACCOUNT_TYPE);
    2781         795 :     if (typeIt != details.end())
    2782         795 :         accountType = typeIt->second;
    2783             :     else
    2784           0 :         accountType = AccountFactory::DEFAULT_ACCOUNT_TYPE;
    2785             : 
    2786        3180 :     JAMI_DEBUG("Adding account {:s} with type {}", newAccountID, accountType);
    2787             : 
    2788         795 :     auto newAccount = accountFactory.createAccount(accountType, newAccountID);
    2789         795 :     if (!newAccount) {
    2790           0 :         JAMI_ERROR("Unknown {:s} param when calling addAccount(): {:s}", Conf::CONFIG_ACCOUNT_TYPE, accountType);
    2791           0 :         return "";
    2792             :     }
    2793             : 
    2794         795 :     newAccount->setAccountDetails(details);
    2795         795 :     saveConfig(newAccount);
    2796         795 :     newAccount->doRegister();
    2797             : 
    2798         795 :     preferences.addAccount(newAccountID);
    2799         795 :     if (accountType != ACCOUNT_TYPE_SIP)
    2800         771 :         markAccountPending(newAccountID);
    2801             : 
    2802         795 :     emitSignal<libjami::ConfigurationSignal::AccountsChanged>();
    2803             : 
    2804         795 :     return newAccountID;
    2805         795 : }
    2806             : 
    2807             : void
    2808         771 : Manager::markAccountPending(const std::string& accountId)
    2809             : {
    2810         771 :     if (preferences.addPendingAccountId(accountId))
    2811         771 :         saveConfig();
    2812         771 : }
    2813             : 
    2814             : void
    2815         789 : Manager::markAccountReady(const std::string& accountId)
    2816             : {
    2817         789 :     if (preferences.removePendingAccountId(accountId))
    2818         769 :         saveConfig();
    2819         789 : }
    2820             : 
    2821             : void
    2822         795 : Manager::removeAccount(const std::string& accountID, bool flush)
    2823             : {
    2824             :     // Get it down and dying
    2825         795 :     if (const auto& remAccount = getAccount(accountID)) {
    2826         795 :         if (auto acc = std::dynamic_pointer_cast<JamiAccount>(remAccount)) {
    2827         771 :             acc->hangupCalls();
    2828         795 :         }
    2829         795 :         remAccount->doUnregister(true);
    2830         795 :         if (flush)
    2831         795 :             remAccount->flush();
    2832         795 :         accountFactory.removeAccount(*remAccount);
    2833         795 :     }
    2834             : 
    2835         795 :     preferences.removeAccount(accountID);
    2836         795 :     preferences.removePendingAccountId(accountID);
    2837             : 
    2838         795 :     saveConfig();
    2839             : 
    2840         795 :     emitSignal<libjami::ConfigurationSignal::AccountsChanged>();
    2841         795 : }
    2842             : 
    2843             : void
    2844           3 : Manager::removeAccounts()
    2845             : {
    2846           3 :     for (const auto& acc : getAccountList())
    2847           3 :         removeAccount(acc);
    2848           3 : }
    2849             : 
    2850             : std::vector<std::string_view>
    2851        3675 : Manager::loadAccountOrder() const
    2852             : {
    2853        3675 :     return split_string(preferences.getAccountOrder(), '/');
    2854             : }
    2855             : 
    2856             : int
    2857          35 : Manager::loadAccountMap(const YAML::Node& node)
    2858             : {
    2859          35 :     int errorCount = 0;
    2860             :     try {
    2861             :         // build preferences
    2862          35 :         preferences.unserialize(node);
    2863          29 :         voipPreferences.unserialize(node);
    2864          29 :         audioPreference.unserialize(node);
    2865             : #ifdef ENABLE_VIDEO
    2866          29 :         videoPreferences.unserialize(node);
    2867             : #endif
    2868             : #ifdef ENABLE_PLUGIN
    2869          29 :         pluginPreferences.unserialize(node);
    2870             : #endif
    2871           6 :     } catch (const YAML::Exception& e) {
    2872          24 :         JAMI_ERROR("[config] Preferences unserialize YAML exception: {}", e.what());
    2873           6 :         ++errorCount;
    2874           6 :     } catch (const std::exception& e) {
    2875           0 :         JAMI_ERROR("[config] Preferences unserialize exception: {}", e.what());
    2876           0 :         ++errorCount;
    2877           0 :     } catch (...) {
    2878           0 :         JAMI_ERROR("[config] Preferences unserialize unknown exception");
    2879           0 :         ++errorCount;
    2880           0 :     }
    2881             : 
    2882             :     // load saved preferences for IP2IP account from configuration file
    2883          35 :     const auto& accountList = node["accounts"];
    2884             : 
    2885          35 :     for (auto& a : accountList) {
    2886           0 :         pimpl_->loadAccount(a, errorCount);
    2887          35 :     }
    2888             : 
    2889          35 :     const auto& accountBaseDir = fileutils::get_data_dir();
    2890          35 :     auto dirs = dhtnet::fileutils::readDirectory(accountBaseDir);
    2891             : 
    2892          35 :     std::condition_variable cv;
    2893          35 :     std::mutex lock;
    2894          35 :     size_t remaining {0};
    2895          35 :     std::unique_lock l(lock);
    2896          70 :     for (const auto& dir : dirs) {
    2897          35 :         if (accountFactory.hasAccount<JamiAccount>(dir)) {
    2898           0 :             continue;
    2899             :         }
    2900             : 
    2901          35 :         if (preferences.isAccountPending(dir)) {
    2902           0 :             JAMI_INFO("[account:%s] Removing pending account from disk", dir.c_str());
    2903           0 :             removeAccount(dir, true);
    2904           0 :             pimpl_->cleanupAccountStorage(dir);
    2905           0 :             continue;
    2906             :         }
    2907             : 
    2908          35 :         remaining++;
    2909         105 :         dht::ThreadPool::computation().run(
    2910          70 :             [this, dir, &cv, &remaining, &lock, configFile = accountBaseDir / dir / "config.yml"] {
    2911          35 :                 if (std::filesystem::is_regular_file(configFile)) {
    2912             :                     try {
    2913           0 :                         auto configNode = YAML::LoadFile(configFile.string());
    2914           0 :                         if (auto a = accountFactory.createAccount(JamiAccount::ACCOUNT_TYPE, dir)) {
    2915           0 :                             auto config = a->buildConfig();
    2916           0 :                             config->unserialize(configNode);
    2917           0 :                             a->setConfig(std::move(config));
    2918           0 :                         }
    2919           0 :                     } catch (const std::exception& e) {
    2920           0 :                         JAMI_ERROR("[account:{}] Unable to import account: {}", dir, e.what());
    2921           0 :                     }
    2922             :                 }
    2923          35 :                 std::lock_guard l(lock);
    2924          35 :                 remaining--;
    2925          35 :                 cv.notify_one();
    2926          35 :             });
    2927             :     }
    2928         105 :     cv.wait(l, [&remaining] { return remaining == 0; });
    2929             : 
    2930             : #ifdef ENABLE_PLUGIN
    2931          35 :     if (pluginPreferences.getPluginsEnabled()) {
    2932           8 :         jami::Manager::instance().getJamiPluginManager().loadPlugins();
    2933             :     }
    2934             : #endif
    2935             : 
    2936          35 :     return errorCount;
    2937          35 : }
    2938             : 
    2939             : std::vector<std::string>
    2940           0 : Manager::getCallList() const
    2941             : {
    2942           0 :     std::vector<std::string> results;
    2943           0 :     for (const auto& call : callFactory.getAllCalls()) {
    2944           0 :         if (!call->isSubcall())
    2945           0 :             results.push_back(call->getCallId());
    2946           0 :     }
    2947           0 :     return results;
    2948           0 : }
    2949             : 
    2950             : void
    2951          32 : Manager::registerAccounts()
    2952             : {
    2953          32 :     for (auto& a : getAllAccounts()) {
    2954           0 :         if (a->isUsable())
    2955           0 :             a->doRegister();
    2956          32 :     }
    2957          32 : }
    2958             : 
    2959             : void
    2960         212 : Manager::sendRegister(const std::string& accountID, bool enable)
    2961             : {
    2962         212 :     const auto acc = getAccount(accountID);
    2963         212 :     if (!acc)
    2964           0 :         return;
    2965             : 
    2966         212 :     acc->setEnabled(enable);
    2967         212 :     saveConfig(acc);
    2968             : 
    2969         212 :     if (acc->isEnabled()) {
    2970          43 :         acc->doRegister();
    2971             :     } else
    2972         169 :         acc->doUnregister();
    2973         212 : }
    2974             : 
    2975             : uint64_t
    2976           0 : Manager::sendTextMessage(const std::string& accountID,
    2977             :                          const std::string& to,
    2978             :                          const std::map<std::string, std::string>& payloads,
    2979             :                          bool fromPlugin,
    2980             :                          bool onlyConnected)
    2981             : {
    2982           0 :     if (const auto acc = getAccount(accountID)) {
    2983             :         try {
    2984             : #ifdef ENABLE_PLUGIN // modifies send message
    2985           0 :             auto& pluginChatManager = getJamiPluginManager().getChatServicesManager();
    2986           0 :             if (pluginChatManager.hasHandlers()) {
    2987           0 :                 auto cm = std::make_shared<JamiMessage>(accountID, to, false, payloads, fromPlugin);
    2988           0 :                 pluginChatManager.publishMessage(cm);
    2989           0 :                 return acc->sendTextMessage(cm->peerId, "", cm->data, 0, onlyConnected);
    2990           0 :             } else
    2991             : #endif // ENABLE_PLUGIN
    2992           0 :                 return acc->sendTextMessage(to, "", payloads, 0, onlyConnected);
    2993           0 :         } catch (const std::exception& e) {
    2994           0 :             JAMI_ERROR("[account:{}] Exception during text message sending: {}", accountID, e.what());
    2995           0 :         }
    2996           0 :     }
    2997           0 :     return 0;
    2998             : }
    2999             : 
    3000             : int
    3001           0 : Manager::getMessageStatus(uint64_t) const
    3002             : {
    3003           0 :     JAMI_ERROR("Deprecated method. Please use status from message");
    3004           0 :     return 0;
    3005             : }
    3006             : 
    3007             : int
    3008           0 : Manager::getMessageStatus(const std::string&, uint64_t) const
    3009             : {
    3010           0 :     JAMI_ERROR("Deprecated method. Please use status from message");
    3011           0 :     return 0;
    3012             : }
    3013             : 
    3014             : void
    3015           0 : Manager::setAccountActive(const std::string& accountID, bool active, bool shutdownConnections)
    3016             : {
    3017           0 :     const auto acc = getAccount(accountID);
    3018           0 :     if (!acc || acc->isActive() == active)
    3019           0 :         return;
    3020           0 :     acc->setActive(active);
    3021           0 :     if (acc->isEnabled()) {
    3022           0 :         if (active) {
    3023           0 :             acc->doRegister();
    3024             :         } else {
    3025           0 :             acc->doUnregister(shutdownConnections);
    3026             :         }
    3027             :     }
    3028           0 :     emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(accountID, acc->getVolatileAccountDetails());
    3029           0 : }
    3030             : 
    3031             : void
    3032           0 : Manager::loadAccountAndConversation(const std::string& accountId, bool loadAll, const std::string& convId)
    3033             : {
    3034           0 :     auto account = getAccount(accountId);
    3035           0 :     if (!account && !autoLoad) {
    3036             :         /*
    3037             :          With the LIBJAMI_FLAG_NO_AUTOLOAD flag active, accounts are not
    3038             :          automatically created during manager initialization, nor are
    3039             :          their configurations set or backed up. This is because account
    3040             :          creation triggers the initialization of the certStore. There why
    3041             :          account creation now occurs here in response to a received notification.
    3042             :          */
    3043           0 :         const auto& accountBaseDir = fileutils::get_data_dir();
    3044           0 :         auto configFile = accountBaseDir / accountId / "config.yml";
    3045             :         try {
    3046           0 :             if ((account = accountFactory.createAccount(JamiAccount::ACCOUNT_TYPE, accountId))) {
    3047           0 :                 account->enableAutoLoadConversations(false);
    3048           0 :                 auto configNode = YAML::LoadFile(configFile.string());
    3049           0 :                 auto config = account->buildConfig();
    3050           0 :                 config->unserialize(configNode);
    3051           0 :                 account->setConfig(std::move(config));
    3052           0 :             }
    3053           0 :         } catch (const std::runtime_error& e) {
    3054           0 :             JAMI_WARNING("[account:{}] Failed to load account: {}", accountId, e.what());
    3055           0 :             return;
    3056           0 :         }
    3057           0 :     }
    3058             : 
    3059           0 :     if (!account) {
    3060           0 :         JAMI_WARNING("[account:{}] Unable to load account", accountId);
    3061           0 :         return;
    3062             :     }
    3063             : 
    3064           0 :     if (auto jamiAcc = std::dynamic_pointer_cast<JamiAccount>(account)) {
    3065           0 :         jamiAcc->setActive(true);
    3066           0 :         jamiAcc->reloadContacts();
    3067           0 :         if (jamiAcc->isUsable())
    3068           0 :             jamiAcc->doRegister();
    3069           0 :         if (auto* convModule = jamiAcc->convModule()) {
    3070           0 :             convModule->reloadRequests();
    3071           0 :             if (loadAll) {
    3072           0 :                 convModule->loadConversations();
    3073           0 :             } else if (!convId.empty()) {
    3074           0 :                 jamiAcc->loadConversation(convId);
    3075             :             }
    3076             :         }
    3077           0 :     }
    3078           0 : }
    3079             : 
    3080             : std::shared_ptr<AudioLayer>
    3081         454 : Manager::getAudioDriver()
    3082             : {
    3083         454 :     return pimpl_->audiodriver_;
    3084             : }
    3085             : 
    3086             : std::shared_ptr<Call>
    3087         112 : Manager::newOutgoingCall(std::string_view toUrl,
    3088             :                          const std::string& accountId,
    3089             :                          const std::vector<libjami::MediaMap>& mediaList)
    3090             : {
    3091         112 :     auto account = getAccount(accountId);
    3092         112 :     if (not account) {
    3093           0 :         JAMI_WARNING("[account:{}] No account matches ID", accountId);
    3094           0 :         return {};
    3095             :     }
    3096             : 
    3097         112 :     if (not account->isUsable()) {
    3098           0 :         JAMI_WARNING("[account:{}] Account is unusable", accountId);
    3099           0 :         return {};
    3100             :     }
    3101             : 
    3102         112 :     return account->newOutgoingCall(toUrl, mediaList);
    3103         112 : }
    3104             : 
    3105             : #ifdef ENABLE_VIDEO
    3106             : std::shared_ptr<video::SinkClient>
    3107         236 : Manager::createSinkClient(const std::string& id, bool mixer)
    3108             : {
    3109         236 :     std::lock_guard lk(pimpl_->sinksMutex_);
    3110         236 :     auto& sinkRef = pimpl_->sinkMap_[id];
    3111         236 :     if (auto sink = sinkRef.lock())
    3112         236 :         return sink;
    3113         217 :     auto sink = std::make_shared<video::SinkClient>(id, mixer);
    3114         217 :     sinkRef = sink;
    3115         217 :     return sink;
    3116         236 : }
    3117             : 
    3118             : void
    3119         354 : Manager::createSinkClients(const std::string& callId,
    3120             :                            const ConfInfo& infos,
    3121             :                            const std::vector<std::shared_ptr<video::VideoFrameActiveWriter>>& videoStreams,
    3122             :                            std::map<std::string, std::shared_ptr<video::SinkClient>>& sinksMap,
    3123             :                            const std::string& accountId)
    3124             : {
    3125         354 :     auto account = accountId.empty() ? nullptr : getAccount<JamiAccount>(accountId);
    3126             : 
    3127         354 :     std::set<std::string> sinkIdsList {};
    3128         354 :     std::vector<std::pair<std::shared_ptr<video::SinkClient>, std::pair<int, int>>> newSinks;
    3129             : 
    3130             :     // create video sinks
    3131         354 :     std::unique_lock lk(pimpl_->sinksMutex_);
    3132        1238 :     for (const auto& participant : infos) {
    3133         884 :         std::string sinkId = participant.sinkId;
    3134         884 :         if (sinkId.empty()) {
    3135         162 :             sinkId = callId;
    3136         162 :             sinkId += string_remove_suffix(participant.uri, '@') + participant.device;
    3137             :         }
    3138         884 :         if (participant.w && participant.h && !participant.videoMuted) {
    3139          11 :             auto& currentSinkW = pimpl_->sinkMap_[sinkId];
    3140          22 :             if (account && string_remove_suffix(participant.uri, '@') == account->getUsername()
    3141          22 :                 && participant.device == account->currentDeviceId()) {
    3142             :                 // This is a local sink that must already exist
    3143          11 :                 continue;
    3144             :             }
    3145           0 :             if (auto currentSink = currentSinkW.lock()) {
    3146             :                 // If sink exists, update it
    3147           0 :                 currentSink->setCrop(participant.x, participant.y, participant.w, participant.h);
    3148           0 :                 sinkIdsList.emplace(sinkId);
    3149           0 :                 continue;
    3150           0 :             }
    3151           0 :             auto newSink = std::make_shared<video::SinkClient>(sinkId, false);
    3152           0 :             currentSinkW = newSink;
    3153           0 :             newSink->setCrop(participant.x, participant.y, participant.w, participant.h);
    3154           0 :             newSinks.emplace_back(newSink, std::make_pair(participant.w, participant.h));
    3155           0 :             sinksMap.emplace(sinkId, std::move(newSink));
    3156           0 :             sinkIdsList.emplace(sinkId);
    3157           0 :         } else {
    3158         873 :             sinkIdsList.erase(sinkId);
    3159             :         }
    3160         884 :     }
    3161         354 :     lk.unlock();
    3162             : 
    3163             :     // remove unused video sinks
    3164         354 :     for (auto it = sinksMap.begin(); it != sinksMap.end();) {
    3165           0 :         if (sinkIdsList.find(it->first) == sinkIdsList.end()) {
    3166           0 :             for (auto& videoStream : videoStreams)
    3167           0 :                 videoStream->detach(it->second.get());
    3168           0 :             it->second->stop();
    3169           0 :             it = sinksMap.erase(it);
    3170             :         } else {
    3171           0 :             it++;
    3172             :         }
    3173             :     }
    3174             : 
    3175             :     // create new video sinks
    3176         354 :     for (const auto& [sink, size] : newSinks) {
    3177           0 :         sink->start();
    3178           0 :         sink->setFrameSize(size.first, size.second);
    3179           0 :         for (auto& videoStream : videoStreams)
    3180           0 :             videoStream->attach(sink.get());
    3181             :     }
    3182         354 : }
    3183             : 
    3184             : std::shared_ptr<video::SinkClient>
    3185           2 : Manager::getSinkClient(const std::string& id)
    3186             : {
    3187           2 :     std::lock_guard lk(pimpl_->sinksMutex_);
    3188           2 :     const auto& iter = pimpl_->sinkMap_.find(id);
    3189           2 :     if (iter != std::end(pimpl_->sinkMap_))
    3190           0 :         if (auto sink = iter->second.lock())
    3191           0 :             return sink;
    3192           2 :     return nullptr;
    3193           2 : }
    3194             : #endif // ENABLE_VIDEO
    3195             : 
    3196             : RingBufferPool&
    3197       44041 : Manager::getRingBufferPool()
    3198             : {
    3199       44041 :     return *pimpl_->ringbufferpool_;
    3200             : }
    3201             : 
    3202             : bool
    3203           0 : Manager::hasAccount(const std::string& accountID)
    3204             : {
    3205           0 :     return accountFactory.hasAccount(accountID);
    3206             : }
    3207             : 
    3208             : const std::shared_ptr<dhtnet::IceTransportFactory>&
    3209         920 : Manager::getIceTransportFactory()
    3210             : {
    3211         920 :     return pimpl_->ice_tf_;
    3212             : }
    3213             : 
    3214             : VideoManager*
    3215        4098 : Manager::getVideoManager() const
    3216             : {
    3217        4098 :     return pimpl_->videoManager_.get();
    3218             : }
    3219             : 
    3220             : std::vector<libjami::Message>
    3221           0 : Manager::getLastMessages(const std::string& accountID, const uint64_t& base_timestamp)
    3222             : {
    3223           0 :     if (const auto acc = getAccount(accountID))
    3224           0 :         return acc->getLastMessages(base_timestamp);
    3225           0 :     return {};
    3226             : }
    3227             : 
    3228             : SIPVoIPLink&
    3229        3451 : Manager::sipVoIPLink() const
    3230             : {
    3231        3451 :     return *pimpl_->sipLink_;
    3232             : }
    3233             : 
    3234             : #ifdef ENABLE_PLUGIN
    3235             : JamiPluginManager&
    3236        3694 : Manager::getJamiPluginManager() const
    3237             : {
    3238        3694 :     return *pimpl_->jami_plugin_manager;
    3239             : }
    3240             : #endif
    3241             : 
    3242             : std::shared_ptr<dhtnet::ChannelSocket>
    3243        2144 : Manager::gitSocket(std::string_view accountId, std::string_view deviceId, std::string_view conversationId)
    3244             : {
    3245        2144 :     if (const auto acc = getAccount<JamiAccount>(accountId))
    3246        2144 :         if (auto* convModule = acc->convModule(true))
    3247        2144 :             return convModule->gitSocket(deviceId, conversationId);
    3248           0 :     return nullptr;
    3249             : }
    3250             : 
    3251             : std::map<std::string, std::string>
    3252           0 : Manager::getNearbyPeers(const std::string& accountID)
    3253             : {
    3254           0 :     if (const auto acc = getAccount<JamiAccount>(accountID))
    3255           0 :         return acc->getNearbyPeers();
    3256           0 :     return {};
    3257             : }
    3258             : 
    3259             : void
    3260           0 : Manager::setDefaultModerator(const std::string& accountID, const std::string& peerURI, bool state)
    3261             : {
    3262           0 :     auto acc = getAccount(accountID);
    3263           0 :     if (!acc) {
    3264           0 :         JAMI_ERROR("[account:{}] Failed to change default moderator: account not found", accountID);
    3265           0 :         return;
    3266             :     }
    3267             : 
    3268           0 :     if (state)
    3269           0 :         acc->addDefaultModerator(peerURI);
    3270             :     else
    3271           0 :         acc->removeDefaultModerator(peerURI);
    3272           0 :     saveConfig(acc);
    3273           0 : }
    3274             : 
    3275             : std::vector<std::string>
    3276           0 : Manager::getDefaultModerators(const std::string& accountID)
    3277             : {
    3278           0 :     auto acc = getAccount(accountID);
    3279           0 :     if (!acc) {
    3280           0 :         JAMI_ERROR("[account:{}] Failed to get default moderators: account not found", accountID);
    3281           0 :         return {};
    3282             :     }
    3283             : 
    3284           0 :     auto set = acc->getDefaultModerators();
    3285           0 :     return std::vector<std::string>(set.begin(), set.end());
    3286           0 : }
    3287             : 
    3288             : void
    3289           0 : Manager::enableLocalModerators(const std::string& accountID, bool isModEnabled)
    3290             : {
    3291           0 :     if (auto acc = getAccount(accountID))
    3292           0 :         acc->editConfig([&](AccountConfig& config) { config.localModeratorsEnabled = isModEnabled; });
    3293           0 : }
    3294             : 
    3295             : bool
    3296           0 : Manager::isLocalModeratorsEnabled(const std::string& accountID)
    3297             : {
    3298           0 :     auto acc = getAccount(accountID);
    3299           0 :     if (!acc) {
    3300           0 :         JAMI_ERROR("[account:{}] Failed to get local moderators: account not found", accountID);
    3301           0 :         return true; // Default value
    3302             :     }
    3303           0 :     return acc->isLocalModeratorsEnabled();
    3304           0 : }
    3305             : 
    3306             : void
    3307           0 : Manager::setAllModerators(const std::string& accountID, bool allModerators)
    3308             : {
    3309           0 :     if (auto acc = getAccount(accountID))
    3310           0 :         acc->editConfig([&](AccountConfig& config) { config.allModeratorsEnabled = allModerators; });
    3311           0 : }
    3312             : 
    3313             : bool
    3314           0 : Manager::isAllModerators(const std::string& accountID)
    3315             : {
    3316           0 :     auto acc = getAccount(accountID);
    3317           0 :     if (!acc) {
    3318           0 :         JAMI_ERROR("[account:{}] Failed to get all moderators: account not found", accountID);
    3319           0 :         return true; // Default value
    3320             :     }
    3321           0 :     return acc->isAllModerators();
    3322           0 : }
    3323             : 
    3324             : void
    3325        2143 : Manager::insertGitTransport(git_smart_subtransport* tr, std::unique_ptr<P2PSubTransport>&& sub)
    3326             : {
    3327        2143 :     std::lock_guard lk(pimpl_->gitTransportsMtx_);
    3328        2144 :     pimpl_->gitTransports_[tr] = std::move(sub);
    3329        2144 : }
    3330             : 
    3331             : void
    3332        2144 : Manager::eraseGitTransport(git_smart_subtransport* tr)
    3333             : {
    3334        2144 :     std::lock_guard lk(pimpl_->gitTransportsMtx_);
    3335        2144 :     pimpl_->gitTransports_.erase(tr);
    3336        2144 : }
    3337             : 
    3338             : dhtnet::tls::CertificateStore&
    3339        9028 : Manager::certStore(const std::string& accountId) const
    3340             : {
    3341        9028 :     if (const auto& account = getAccount<JamiAccount>(accountId)) {
    3342       18060 :         return account->certStore();
    3343        9030 :     }
    3344           0 :     throw std::runtime_error("No account found");
    3345             : }
    3346             : 
    3347             : } // namespace jami

Generated by: LCOV version 1.14