LCOV - code coverage report
Current view: top level - foo/src - manager.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 906 1742 52.0 %
Date: 2026-02-28 10:41:24 Functions: 189 465 40.6 %

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

Generated by: LCOV version 1.14