~aleteoryx/muditaos

3343f028092103a98dc97d5615dc176df5818bfb — Adam Dobrowolski 3 years ago e7444ff
[MOS-266] Call StateMachine implementation

With some tests, documentation and so on
59 files changed, 1961 insertions(+), 682 deletions(-)

M .gitmodules
M module-db/Interface/CalllogRecord.cpp
M module-db/Interface/CalllogRecord.hpp
M module-os/RTOSWrapper/include/thread.hpp
M module-services/service-cellular/CellularRequestHandler.cpp
M module-services/service-cellular/CellularServiceAPI.cpp
M module-services/service-cellular/CellularUrcHandler.cpp
M module-services/service-cellular/ServiceCellular.cpp
M module-services/service-cellular/call/CMakeLists.txt
D module-services/service-cellular/call/CallDB.cpp
D module-services/service-cellular/call/CallDB.hpp
D module-services/service-cellular/call/CallGUI.hpp
A module-services/service-cellular/call/CallMachine.hpp
D module-services/service-cellular/call/CallMulticast.hpp
D module-services/service-cellular/call/CallRingGuard.cpp
D module-services/service-cellular/call/CallRingGuard.hpp
M module-services/service-cellular/call/CellularCall.cpp
D module-services/service-cellular/call/README.md
R module-services/service-cellular/call/{ => api}/CallAudio.cpp
R module-services/service-cellular/call/{ => api}/CallAudio.hpp
A module-services/service-cellular/call/api/CallDB.cpp
A module-services/service-cellular/call/api/CallDB.hpp
R module-services/service-cellular/call/{ => api}/CallGUI.cpp
A module-services/service-cellular/call/api/CallGUI.hpp
R module-services/service-cellular/call/{ => api}/CallMulticast.cpp
A module-services/service-cellular/call/api/CallMulticast.hpp
A module-services/service-cellular/call/api/CallTimer.cpp
A module-services/service-cellular/call/api/CallTimer.hpp
A module-services/service-cellular/call/api/ModemCallApi.cpp
A module-services/service-cellular/call/api/ModemCallApi.hpp
A module-services/service-cellular/call/doc/README.md
A module-services/service-cellular/call/doc/call.svg
A module-services/service-cellular/call/doc/uml/CMakeLists.txt
A module-services/service-cellular/call/doc/uml/call_flow.puml
A module-services/service-cellular/call/doc/uml/uml_printer.cpp
A module-services/service-cellular/call/include/call/CallEvents.hpp
M module-services/service-cellular/call/include/call/CellularCall.hpp
A module-services/service-cellular/call/tests/CMakeLists.txt
A module-services/service-cellular/call/tests/test-CallMachine.cpp
M module-services/service-cellular/connection-manager/ConnectionManager.cpp
M module-services/service-cellular/connection-manager/ConnectionManagerCellularCommands.cpp
M module-services/service-cellular/service-cellular/CellularMessage.hpp
M module-services/service-cellular/service-cellular/CellularServiceAPI.hpp
M module-services/service-cellular/service-cellular/ServiceCellular.hpp
M module-services/service-cellular/service-cellular/connection-manager/ConnectionManager.hpp
M module-services/service-cellular/service-cellular/connection-manager/ConnectionManagerCellularCommands.hpp
M module-services/service-cellular/service-cellular/connection-manager/ConnectionManagerCellularCommandsInterface.hpp
M module-services/service-db/DBServiceAPI.cpp
M module-services/service-evtmgr/battery/BatteryController.cpp
M module-sys/SystemManager/include/SystemManager/CpuSentinel.hpp
M module-utils/log/Logger.cpp
M module-utils/unicode/utf8/CMakeLists.txt
M products/PurePhone/services/db/ServiceDB.cpp
M third-party/CMakeLists.txt
A third-party/fakeit/CMakeLists.txt
A third-party/fakeit/FakeIt
M third-party/sml-utils/CMakeLists.txt
M third-party/sml-utils/include/sml-utils/Logger.hpp
A third-party/sml-utils/include/sml-utils/PlantUML.hpp
M .gitmodules => .gitmodules +3 -0
@@ 113,3 113,6 @@
	path = third-party/freeRTOS-kernel/freeRTOS-kernel
	url = ../FreeRTOS-Kernel.git
	branch = mudita
[submodule "third-party/fakeit/FakeIt"]
	path = third-party/fakeit/FakeIt
	url = https://github.com/eranpeer/FakeIt.git

M module-db/Interface/CalllogRecord.cpp => module-db/Interface/CalllogRecord.cpp +19 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "CalllogRecord.hpp"


@@ 29,6 29,24 @@ CalllogRecord::CalllogRecord(const CallType type, const utils::PhoneNumber::View
    : presentation(PresentationType::PR_UNKNOWN), date(std::time(nullptr)), type(type), phoneNumber(number)
{}

CalllogRecord::CalllogRecord(const CalllogRecord &rhs) : Record(rhs)
{
    operator=(rhs);
}

CalllogRecord &CalllogRecord::operator=(const CalllogRecord &rhs)
{
    ID           = rhs.ID;
    presentation = rhs.presentation;
    date         = rhs.date;
    duration     = rhs.duration;
    type         = rhs.type;
    name         = rhs.name;
    phoneNumber  = rhs.phoneNumber;
    isRead       = rhs.isRead;
    return *this;
}

std::ostream &operator<<(std::ostream &out, const CalllogRecord &rec)
{
    out << " <id> " << rec.ID << " <number> " << rec.phoneNumber.getEntered() << " <e164number> "

M module-db/Interface/CalllogRecord.hpp => module-db/Interface/CalllogRecord.hpp +3 -1
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 29,8 29,10 @@ struct CalllogRecord : public Record
    [[nodiscard]] std::string str() const;

    CalllogRecord() = default;
    CalllogRecord(const CalllogRecord &rhs);
    CalllogRecord(const CallType type, const utils::PhoneNumber::View &number);
    CalllogRecord(const CalllogTableRow &tableRow);
    CalllogRecord &operator=(const CalllogRecord &);
};

enum class CalllogRecordField

M module-os/RTOSWrapper/include/thread.hpp => module-os/RTOSWrapper/include/thread.hpp +1 -1
@@ 53,7 53,7 @@
#ifndef CPP_FREERTOS_NO_CPP_STRINGS
#include <string>
#endif
#include "FreeRTOS.h"
#include <FreeRTOS.h>
#include "task.h"
#include "mutex.hpp"
#include "semaphore.hpp"

M module-services/service-cellular/CellularRequestHandler.cpp => module-services/service-cellular/CellularRequestHandler.cpp +1 -4
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "service-cellular/CellularRequestHandler.hpp"


@@ 68,12 68,9 @@ void CellularRequestHandler::handle(cellular::CallRequest &request, at::Result &
        request.setHandled(false);
        return;
    }
    // activate call state timer
    cellular.callStateTimer.start();
    // Propagate "Ringing" notification into system
    cellular.bus.sendMulticast(std::make_shared<CellularRingingMessage>(request.getNumber()),
                               sys::BusChannel::ServiceCellularNotifications);
    AudioServiceAPI::RoutingStart(&cellular);
    request.setHandled(true);
}


M module-services/service-cellular/CellularServiceAPI.cpp => module-services/service-cellular/CellularServiceAPI.cpp +2 -2
@@ 47,9 47,9 @@ bool CellularServiceAPI::HangupCall(sys::Service *serv)
    return true;
}

bool CellularServiceAPI::DismissCall(sys::Service *serv, bool addNotificationToDB)
bool CellularServiceAPI::DismissCall(sys::Service *serv)
{
    auto msg = std::make_shared<CellularDismissCallMessage>(addNotificationToDB);
    auto msg = std::make_shared<CellularDismissCallMessage>();
    serv->bus.sendMulticast(std::move(msg), sys::BusChannel::ServiceCellularNotifications);
    return true;
}

M module-services/service-cellular/CellularUrcHandler.cpp => module-services/service-cellular/CellularUrcHandler.cpp +1 -1
@@ 212,7 212,7 @@ void CellularUrcHandler::Handle(UrcResponse &urc)

    for (auto &t : typesToHandle) {
        if (t == urc.getURCResponseType()) {
            LOG_TRACE("call aborted");
            LOG_INFO("call (aborted|missed) type: %s", magic_enum::enum_name(t).data());
            response = std::make_unique<CellularCallAbortedNotification>();
            urc.setHandled(true);
        }

M module-services/service-cellular/ServiceCellular.cpp => module-services/service-cellular/ServiceCellular.cpp +37 -116
@@ 164,28 164,6 @@ ServiceCellular::ServiceCellular()
        priv->csqHandler->handleTimerTick();
    });

    callDurationTimer = sys::TimerFactory::createPeriodicTimer(
        this, "callDurationTimer", std::chrono::milliseconds{1000}, [this](sys::Timer &) {
            ongoingCall.handleCallDurationTimer();
        });
    ongoingCall.setStartCallAction([=](const CalllogRecord &rec) {
        auto call = DBServiceAPI::CalllogAdd(this, rec);
        if (call.ID == DB_ID_NONE) {
            LOG_ERROR("CalllogAdd failed");
        }
        return call;
    });

    ongoingCall.setEndCallAction([=](const CalllogRecord &rec) {
        if (DBServiceAPI::CalllogUpdate(this, rec) && rec.type == CallType::CT_MISSED) {
            DBServiceAPI::GetQuery(this,
                                   db::Interface::Name::Notifications,
                                   std::make_unique<db::query::notifications::Increment>(
                                       NotificationsRecord::Key::Calls, rec.phoneNumber));
        }
        return true;
    });

    notificationCallback = [this](std::string &data) {
        LOG_DEBUG("Notifications callback called with %u data bytes", static_cast<unsigned int>(data.size()));



@@ 222,7 200,7 @@ void ServiceCellular::SleepTimerHandler()
                                          ? currentTime - lastCommunicationTimestamp
                                          : std::numeric_limits<TickType_t>::max() - lastCommunicationTimestamp + currentTime;

    if (!ongoingCall.isValid() && priv->state->get() >= State::ST::URCReady &&
    if (!ongoingCall->active() && priv->state->get() >= State::ST::URCReady &&
        timeOfInactivity >= constants::maxTimeWithoutCommunication.count()) {
        cmux->enterSleepMode();
        cpuSentinel->ReleaseMinimumFrequency();


@@ 279,7 257,16 @@ sys::ReturnCodes ServiceCellular::InitHandler()
    };

    cpuSentinel = std::make_shared<sys::CpuSentinel>(serviceName, this);
    ongoingCall.setCpuSentinel(cpuSentinel);

    ongoingCall =
        std::make_unique<call::Call>(this,
                                     cmux.get(),
                                     sys::TimerFactory::createPeriodicTimer(
                                         this,
                                         "callDurationTimer",
                                         std::chrono::milliseconds{1000},
                                         [this](sys::Timer &) { ongoingCall->handle(call::event::OngoingTimer{}); }),
                                     cpuSentinel);

    auto sentinelRegistrationMsg = std::make_shared<sys::SentinelRegistrationMessage>(cpuSentinel);
    bus.sendUnicast(sentinelRegistrationMsg, ::service::name::system_manager);


@@ 322,9 309,10 @@ void ServiceCellular::registerMessageHandlers()
        if (priv->simCard->isSimCardSelected()) {
            connectionManager->onPhoneModeChange(mode);
        }
        ongoingCall.setMode(mode);
        if (ongoingCall != nullptr) {
            ongoingCall->handle(call::event::ModeChange{mode});
        }
    });
    ongoingCall.setMode(phoneModeObserver->getCurrentPhoneMode());
    phoneModeObserver->subscribe([&](sys::phone_modes::Tethering tethering) {
        if (tethering == sys::phone_modes::Tethering::On) {
            priv->tetheringHandler->enable();


@@ 559,9 547,6 @@ void ServiceCellular::registerMessageHandlers()
    connect(typeid(CellularCallActiveNotification),
            [&](sys::Message *request) -> sys::MessagePointer { return handleCallActiveNotification(request); });

    connect(typeid(CellularCallAbortedNotification),
            [&](sys::Message *request) -> sys::MessagePointer { return handleCallAbortedNotification(request); });

    connect(typeid(CellularPowerUpProcedureCompleteNotification), [&](sys::Message *request) -> sys::MessagePointer {
        return handlePowerUpProcedureCompleteNotification(request);
    });


@@ 609,17 594,15 @@ void ServiceCellular::registerMessageHandlers()

    connect(typeid(cellular::CallAudioEventRequest), [&](sys::Message *request) -> sys::MessagePointer {
        auto message = static_cast<cellular::CallAudioEventRequest *>(request);
        ongoingCall.handleCallAudioEventRequest(message->eventType);
        ongoingCall->handle(call::event::AudioRequest{message->eventType});
        return sys::MessageNone{};
    });

    connect(typeid(cellular::CallActiveNotification), [&](sys::Message *request) -> sys::MessagePointer {
        callDurationTimer.start();
        return sys::MessageNone{};
    });

    connect(typeid(cellular::CallEndedNotification), [&](sys::Message *request) -> sys::MessagePointer {
        callDurationTimer.stop();
    /// aborted notification can come from:
    /// 1. URC
    /// 2. from here from handle cellular hangup
    connect(typeid(CellularCallAbortedNotification), [&](sys::Message * /*request*/) -> sys::MessagePointer {
        ongoingCall->handle(call::event::Ended{});
        return sys::MessageNone{};
    });



@@ 1786,22 1769,7 @@ auto ServiceCellular::handleCellularAnswerIncomingCallMessage(CellularMessage *m
    -> std::shared_ptr<CellularResponseMessage>
{
    LOG_INFO("%s", __PRETTY_FUNCTION__);
    if (ongoingCall.getType() != CallType::CT_INCOMING) {
        return std::make_shared<CellularResponseMessage>(true);
    }

    auto channel = cmux->get(CellularMux::Channel::Commands);
    auto ret     = false;
    if (channel) {
        auto response = channel->cmd(at::AT::ATA);
        if (response) {
            // Propagate "CallActive" notification into system
            bus.sendMulticast(std::make_shared<CellularCallActiveNotification>(),
                              sys::BusChannel::ServiceCellularNotifications);
            AudioServiceAPI::RoutingStart(this);
            ret = true;
        }
    }
    auto ret = ongoingCall->handle(call::event::Answer{});
    return std::make_shared<CellularResponseMessage>(ret);
}



@@ 1868,37 1836,17 @@ auto ServiceCellular::handleCellularCallRequestMessage(CellularCallRequestMessag
void ServiceCellular::handleCellularHangupCallMessage(CellularHangupCallMessage *msg)
{
    LOG_INFO("%s", __PRETTY_FUNCTION__);
    auto channel = cmux->get(CellularMux::Channel::Commands);
    if (channel) {
        if (channel->cmd(at::AT::ATH)) {
            callStateTimer.stop();
            callEndedRecentlyTimer.start();
            if (!ongoingCall.endCall(CellularCall::Forced::True)) {
                LOG_ERROR("Failed to end ongoing call");
            }
            bus.sendMulticast(std::make_shared<CellularResponseMessage>(true, msg->type),
                              sys::BusChannel::ServiceCellularNotifications);
        }
        else {
            LOG_ERROR("Call not aborted");
            bus.sendMulticast(std::make_shared<CellularResponseMessage>(false, msg->type),
                              sys::BusChannel::ServiceCellularNotifications);
        }
    if (!ongoingCall->handle(call::event::Reject{})) {
        LOG_ERROR("Failed to end ongoing call");
    }
    bus.sendMulticast(std::make_shared<CellularResponseMessage>(false, msg->type),
                      sys::BusChannel::ServiceCellularNotifications);
    callStateTimer.stop();
    callEndedRecentlyTimer.start();
}

void ServiceCellular::handleCellularDismissCallMessage(sys::Message *msg)
{
    LOG_INFO("%s", __PRETTY_FUNCTION__);

    auto message = static_cast<CellularDismissCallMessage *>(msg);

    hangUpCall();
    if (message->addNotificationRequired()) {
        handleCallAbortedNotification(msg);
    }
}

auto ServiceCellular::handleDBQueryResponseMessage(db::QueryResponse *msg) -> std::shared_ptr<sys::ResponseMessage>


@@ 1932,8 1880,7 @@ auto ServiceCellular::handleCellularListCallsMessage(CellularMessage *msg) -> st
        });

        if (it != std::end(data)) {
            auto notification = std::make_shared<CellularCallActiveNotification>();
            bus.sendMulticast(std::move(notification), sys::BusChannel::ServiceCellularNotifications);
            ongoingCall->handle(call::event::Answer{});
            callStateTimer.stop();
            return std::make_shared<CellularResponseMessage>(true);
        }


@@ 1955,7 1902,8 @@ auto ServiceCellular::handleDBNotificationMessage(db::NotificationMessage *msg) 
auto ServiceCellular::handleCellularRingingMessage(CellularRingingMessage *msg) -> std::shared_ptr<sys::ResponseMessage>
{
    LOG_INFO("%s", __PRETTY_FUNCTION__);
    return std::make_shared<CellularResponseMessage>(ongoingCall.startOutgoing(msg->number));
    return std::make_shared<CellularResponseMessage>(
        ongoingCall->handle(call::event::StartCall{CallType::CT_OUTGOING, msg->number}));
}

auto ServiceCellular::handleCellularGetIMSIMessage(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>


@@ 2121,7 2069,7 @@ auto ServiceCellular::handleStateRequestMessage(sys::Message *msg) -> std::share

auto ServiceCellular::handleCallActiveNotification(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>
{
    auto ret = std::make_shared<CellularResponseMessage>(ongoingCall.setActive());
    auto ret = std::make_shared<CellularResponseMessage>(true);
    NetworkSettings networkSettings(*this);
    auto currentNAT = networkSettings.getCurrentNAT();
    if (currentNAT) {


@@ 2141,15 2089,7 @@ auto ServiceCellular::handleCallActiveNotification(sys::Message *msg) -> std::sh
    }
    return ret;
}
auto ServiceCellular::handleCallAbortedNotification(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>
{
    callStateTimer.stop();
    const auto ret = ongoingCall.endCall();
    if (not ret) {
        LOG_ERROR("failed to end call");
    }
    return std::make_shared<CellularResponseMessage>(ret);
}

auto ServiceCellular::handlePowerUpProcedureCompleteNotification(sys::Message *msg)
    -> std::shared_ptr<sys::ResponseMessage>
{


@@ 2217,31 2157,22 @@ auto ServiceCellular::handleCellularSetFlightModeMessage(sys::Message *msg) -> s
auto ServiceCellular::handleCellularRingNotification(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>
{
    LOG_INFO("%s", __PRETTY_FUNCTION__);

    if (phoneModeObserver->isTetheringOn() || connectionManager->forceDismissCalls()) {
    if (phoneModeObserver->isTetheringOn()) {
        return std::make_shared<CellularResponseMessage>(hangUpCall());
    }
    ongoingCall.handleRING();

    ongoingCall->handle(call::event::RING{});
    return std::make_shared<CellularResponseMessage>(true);
}

auto ServiceCellular::handleCellularCallerIdNotification(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>
{
    if (connectionManager->forceDismissCalls()) {
        return std::make_shared<CellularResponseMessage>(hangUpCall());
    }

    auto message = static_cast<CellularCallerIdNotification *>(msg);
    if (phoneModeObserver->isTetheringOn()) {
        tetheringCalllog.push_back(CalllogRecord{CallType::CT_MISSED, message->getNubmer()});
        return std::make_shared<CellularResponseMessage>(hangUpCallBusy());
    }

    if (not ongoingCall.handleCLIP(message->getNubmer())) {
        CellularServiceAPI::DismissCall(
            this, phoneModeObserver->getCurrentPhoneMode() == sys::phone_modes::PhoneMode::DoNotDisturb);
    }
    ongoingCall->handle(call::event::CLIP{message->getNubmer()});

    return std::make_shared<CellularResponseMessage>(true);
}


@@ 2261,15 2192,11 @@ auto ServiceCellular::handleCellularSetConnectionFrequencyMessage(sys::Message *

auto ServiceCellular::hangUpCall() -> bool
{
    auto channel = cmux->get(CellularMux::Channel::Commands);
    if (channel != nullptr) {
        if (channel->cmd(at::factory(at::AT::ATH))) {
            ongoingCall.endCall();
            return true;
        }
    auto ret = ongoingCall->handle(call::event::Ended{});
    if (!ret) {
        LOG_ERROR("Failed to hang up call");
    }
    LOG_ERROR("Failed to hang up call");
    return false;
    return ret;
}

auto ServiceCellular::hangUpCallBusy() -> bool


@@ 2357,12 2284,6 @@ auto ServiceCellular::logTetheringCalls() -> void
    }
}

bool ServiceCellular::areCallsFromFavouritesEnabled()
{
    return utils::getNumericValue<bool>(
        settings->getValue(settings::Offline::callsFromFavorites, settings::SettingsScope::Global));
}

TaskHandle_t ServiceCellular::getTaskHandle()
{
    return xTaskGetCurrentTaskHandle();

M module-services/service-cellular/call/CMakeLists.txt => module-services/service-cellular/call/CMakeLists.txt +24 -10
@@ 1,26 1,40 @@
project(service-cellular-call)
message( "${PROJECT_NAME}  ${CMAKE_CURRENT_LIST_DIR}" )

add_library(${PROJECT_NAME} STATIC 
add_library(${PROJECT_NAME} STATIC
    CellularCall.cpp
    CallAudio.cpp
    CallGUI.cpp
    CallDB.cpp
    CallMulticast.cpp
    CallRingGuard.cpp
    api/CallAudio.cpp
    api/CallGUI.cpp
    api/CallDB.cpp
    api/CallMulticast.cpp
    api/CallTimer.cpp
    api/ModemCallApi.cpp
)
set_source_files_properties(
        CellularCall.cpp
        PROPERTIES COMPILE_FLAGS
        "-Wno-error=unused-but-set-variable"
)

target_include_directories(${PROJECT_NAME}
    PRIVATE
    PUBLIC
        ./
        ./include/
)

target_link_libraries(${PROJECT_NAME}
    PRIVATE
        module-sys
        sml::utils::logger
    PUBLIC
        libphonenumber::libphonenumber
    )

# uncomment to add tests:
# if (${ENABLE_TESTS})
#     add_subdirectory(tests)
# endif ()
if (${PROJECT_TARGET} STREQUAL "TARGET_Linux")
    add_subdirectory(doc/uml/)
endif()

if (${ENABLE_TESTS})
    add_subdirectory(tests)
endif ()

D module-services/service-cellular/call/CallDB.cpp => module-services/service-cellular/call/CallDB.cpp +0 -18
@@ 1,18 0,0 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "CallDB.hpp"
#include "service-appmgr/data/CallActionsParams.hpp"
#include <queries/notifications/QueryNotificationsIncrement.hpp>
#include <service-appmgr/Controller.hpp>

CallDB::CallDB(sys::Service &s) : owner(s)
{}

void CallDB::incrementNotAnsweredCallsNotification(const utils::PhoneNumber::View &number)
{
    DBServiceAPI::GetQuery(
        &owner,
        db::Interface::Name::Notifications,
        std::make_unique<db::query::notifications::Increment>(NotificationsRecord::Key::Calls, number));
}

D module-services/service-cellular/call/CallDB.hpp => module-services/service-cellular/call/CallDB.hpp +0 -21
@@ 1,21 0,0 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <PhoneNumber.hpp>

namespace sys
{
    class Service;
}

class CallDB
{
    sys::Service &owner;

  public:
    explicit CallDB(sys::Service &);

    void incrementNotAnsweredCallsNotification(const utils::PhoneNumber::View &number);
};

D module-services/service-cellular/call/CallGUI.hpp => module-services/service-cellular/call/CallGUI.hpp +0 -28
@@ 1,28 0,0 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <PhoneNumber.hpp>
#include <Tables/CalllogTable.hpp>

namespace sys
{
    class Service;
}

class CallGUI
{
    struct CallMeta;
    sys::Service &owner;

  public:
    explicit CallGUI(sys::Service &);

    void notifyRING();
    void notifyCLIP(const utils::PhoneNumber::View &number);
    void notifyCallStarted(utils::PhoneNumber phoneNumber, const CallType type);
    void notifyCallEnded();
    void notifyCallActive();
    void notifyCallDurationUpdate(const time_t &duration);
};

A module-services/service-cellular/call/CallMachine.hpp => module-services/service-cellular/call/CallMachine.hpp +337 -0
@@ 0,0 1,337 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <boost/sml.hpp>
#include <sml-utils/Logger.hpp>
#include "call/CellularCall.hpp"
#include "log/log.hpp"
#include "magic_enum.hpp"
#include <cxxabi.h>

namespace call
{
    void setNumber(CallData &call, const utils::PhoneNumber::View &number)
    {
        call.record.presentation =
            number.getFormatted().empty() ? PresentationType::PR_UNKNOWN : PresentationType::PR_ALLOWED;
        call.record.phoneNumber = number;
    }

    const auto setIncomingCall = [](Dependencies &di, CallData &call, const utils::PhoneNumber::View &view) {
        call.record.type   = CallType::CT_INCOMING;
        call.record.isRead = false;
        setNumber(call, view);
    };

    const auto handleClipCommon = [](Dependencies &di, const call::event::CLIP &clip, CallData &call) {
        const utils::PhoneNumber::View &nr = clip.number;
        setIncomingCall(di, call, nr);
        call.record.date = std::time(nullptr);

        di.gui->notifyCLIP(nr);
        di.multicast->notifyIdentifiedCall(nr);
        di.db->startCall(call.record);
        di.sentinel->HoldMinimumFrequency(bsp::CpuFrequencyMHz::Level_6);
    };

    struct RING
    {
        bool operator()(CallData &call)
        {
            return call.mode == sys::phone_modes::PhoneMode::Connected;
        }
    } constexpr RING;

    struct HandleInit
    {
        void operator()(Dependencies &di, CallData &call)
        {
            call      = CallData{};
            call.mode = di.modem->getMode();
            di.sentinel->ReleaseMinimumFrequency();
            di.timer->stop();
        }
    } constexpr HandleInit;

    struct HandleRing
    {
        void operator()(Dependencies &di, CallData &call)
        {
            auto nr = utils::PhoneNumber::View();
            setIncomingCall(di, call, nr);
            call.record.date = std::time(nullptr);

            di.gui->notifyRING();
            di.multicast->notifyIncommingCall();
            di.db->startCall(call.record);
            di.sentinel->HoldMinimumFrequency(bsp::CpuFrequencyMHz::Level_6);
            di.audio->play();
        }
    } constexpr HandleRing;

    struct ClipConnected
    {
        bool operator()(CallData &call)
        {
            return call.mode == sys::phone_modes::PhoneMode::Connected;
        }
    } constexpr ClipConnected;

    struct HandleClipWithoutRing
    {
        void operator()(Dependencies &di, const call::event::CLIP &clip, CallData &call)
        {
            handleClipCommon(di, clip, call);
            di.audio->play();
        };
    } constexpr HandleClipWithoutRing;

    struct HandleClipConnected
    {
        /// does everything that clip would do + plays ring
        void operator()(Dependencies &di, const call::event::CLIP &clip, CallData &call)
        {
            handleClipCommon(di, clip, call);
        };
    } constexpr HandleClipConnected;

    struct ClipDND_OK
    {
        bool operator()(const call::event::CLIP &clip, Dependencies &di, CallData &call)
        {
            return call.mode == sys::phone_modes::PhoneMode::DoNotDisturb &&
                   di.modem->areCallsFromFavouritesEnabled() && di.db->isNumberInFavourites(clip.number);
        }
    } constexpr ClipDND_OK;

    struct HandleClipDND
    {
        void operator()(const call::event::CLIP &clip, Dependencies &di, CallData &call)
        {
            handleClipCommon(di, clip, call);
            di.audio->play();
        }
    } constexpr HandleClipDND;

    struct ClipDND_NOK
    {
        bool operator()(Dependencies &di, CallData &call)
        {
            return call.mode == sys::phone_modes::PhoneMode::DoNotDisturb &&
                   not di.db->isNumberInFavourites(call.record.phoneNumber);
        }
    } constexpr ClipDND_NOK;

    struct HandleDND_Reject
    {
        void operator()(Dependencies &di, const call::event::CLIP &clip, CallData &call)
        {
            setNumber(call, clip.number);
            di.db->incrementNotAnsweredCallsNotification(call.record.phoneNumber);
            di.db->endCall(call.record);
            di.modem->rejectCall();
        }
    } constexpr HandleDND_Reject;

    /// when we have ongoing call - handle incomming CLIP information
    /// this happens when we answer call before first CLIP is received
    struct HandleAddID
    {
        void operator()(Dependencies &di, const call::event::CLIP &clip, CallData &call)
        {
            const utils::PhoneNumber::View &number = clip.number;
            setNumber(call, number);
            di.gui->notifyCLIP(number);
            di.multicast->notifyIdentifiedCall(number);
        }
    } constexpr HandleAddID;

    struct HandleAnswerCall
    {
        void operator()(Dependencies &di, CallData &call)
        {
            if (not di.modem->answerIncommingCall()) {
                throw std::runtime_error("modem failure!");
            }
            di.audio->routingStart();
            di.multicast->notifyCallActive();
            di.gui->notifyCallActive();
            di.timer->start();
        }
    } constexpr HandleAnswerCall;

    struct CallIncoming
    {
        bool operator()(CallData &call)
        {
            return call.record.type == CallType::CT_INCOMING;
        }
    } constexpr CallIncoming;

    struct HandleCallTimer
    {
        void operator()(Dependencies &di, CallData &call)
        {
            di.gui->notifyCallDurationUpdate(di.timer->duration());
        }
    } constexpr HandleCallTimer;

    struct HandleStartCall
    {
        void operator()(Dependencies &di, const call::event::StartCall &start, CallData &call)
        {
            call.record.type        = start.type;
            call.record.isRead      = true;
            call.record.phoneNumber = start.number;
            call.record.date        = std::time(nullptr);

            di.db->startCall(call.record);
            di.gui->notifyCallStarted(call.record.phoneNumber, call.record.type);
            di.sentinel->HoldMinimumFrequency(bsp::CpuFrequencyMHz::Level_6);
        }
    } constexpr HandleStartCall;

    struct HandleStartedCall
    {
        void operator()(Dependencies &di, CallData &call)
        {
            di.audio->routingStart();
            di.timer->start();
        }
    } constexpr HandleStartedCall;

    struct HandleRejectCall
    {
        void operator()(Dependencies &di, CallData &call)
        {
            di.audio->stop();
            call.record.type   = CallType::CT_REJECTED;
            call.record.isRead = false;
            di.db->endCall(call.record);
            di.gui->notifyCallEnded();
            di.modem->hangupCall();
        }
    } constexpr HandleRejectCall;

    struct HandleMissedCall
    {
        void operator()(Dependencies &di, CallData &call)
        {
            di.audio->stop();
            call.record.type   = CallType::CT_MISSED;
            call.record.isRead = false;
            di.db->endCall(call.record);
            di.gui->notifyCallEnded();
            di.modem->hangupCall();
        };
    } constexpr HandleMissedCall;

    struct HandleEndCall
    {
        void operator()(Dependencies &di, CallData &call)
        {
            di.audio->stop();
            call.record.duration = di.timer->duration();
            di.db->endCall(call.record);
            di.gui->notifyCallEnded();
            di.timer->stop();
            di.modem->hangupCall();
        }
    } constexpr HandleEndCall;

    struct HandleAudioRequest
    {
        void operator()(Dependencies &di, const call::event::AudioRequest &request)
        {
            cellular::CallAudioEventRequest::EventType event = request.event;
            switch (event) {
            case cellular::CallAudioEventRequest::EventType::Mute:
                di.audio->muteCall();
                break;
            case cellular::CallAudioEventRequest::EventType::Unmute:
                di.audio->unmuteCall();
                break;
            case cellular::CallAudioEventRequest::EventType::LoudspeakerOn:
                di.audio->setLaudspeakerOn();
                break;
            case cellular::CallAudioEventRequest::EventType::LoudspeakerOff:
                di.audio->setLaudspeakerOff();
                break;
            }
        }
    } constexpr HandleAudioRequest;

    // show exception in log and notify the world about ended call
    struct ExceptionHandler
    {
        void operator()(const std::runtime_error &err, Dependencies &di)
        {
            di.multicast->notifyCallAborted();
            di.gui->notifyCallEnded();
            LOG_FATAL("EXCEPTION %s", err.what());
        }
    } constexpr ExceptionHandler;

    struct SM
    {
        auto operator()() const
        {
            namespace evt = call::event;
            using namespace boost::sml;

            return make_transition_table(
                *"Idle"_s + on_entry<_> / HandleInit,

                "Idle"_s + boost::sml::event<evt::RING>[RING] / HandleRing                     = "Ringing"_s,
                "Idle"_s + boost::sml::event<evt::CLIP>[ClipConnected] / HandleClipWithoutRing = "HaveId"_s,
                "Idle"_s + boost::sml::event<evt::CLIP>[ClipDND_OK] / HandleClipDND            = "HaveId"_s,
                "Idle"_s + boost::sml::event<evt::CLIP>[ClipDND_NOK] / HandleDND_Reject        = "Idle"_s,
                // outgoing call: Pure is Ringing (called from: handleCellularRingingMessage)
                "Idle"_s + boost::sml::event<evt::StartCall> / HandleStartCall = "Starting"_s,

                "Starting"_s + boost::sml::event<evt::Ended> / HandleEndCall      = "Ongoing"_s,
                "Starting"_s + boost::sml::event<evt::Reject> / HandleEndCall     = "Ongoing"_s,
                "Starting"_s + boost::sml::event<evt::Answer> / HandleStartedCall = "Ongoing"_s,

                "Ringing"_s + boost::sml::event<evt::Answer> / HandleAnswerCall                 = "Ongoing"_s,
                "Ringing"_s + boost::sml::event<evt::Reject> / HandleRejectCall                 = "Idle"_s,
                "Ringing"_s + boost::sml::event<evt::Ended> / HandleRejectCall                  = "Idle"_s,
                "Ringing"_s + boost::sml::event<evt::CLIP>[ClipConnected] / HandleClipConnected = "HaveId"_s,
                "Ringing"_s + boost::sml::event<evt::CLIP>[ClipDND_OK] / HandleClipDND          = "HaveId"_s,
                "Ringing"_s + boost::sml::event<evt::CLIP>[ClipDND_NOK] / HandleDND_Reject      = "Idle"_s,

                "HaveId"_s + boost::sml::event<evt::Answer>[CallIncoming] / HandleAnswerCall = "Ongoing"_s,
                "HaveId"_s + boost::sml::event<evt::Ended> / HandleMissedCall                = "Idle"_s,
                "HaveId"_s + boost::sml::event<evt::Reject> / HandleRejectCall               = "Idle"_s,

                "Ongoing"_s + boost::sml::event<evt::CLIP> / HandleAddID                = "Ongoing"_s,
                "Ongoing"_s + boost::sml::event<evt::OngoingTimer> / HandleCallTimer    = "Ongoing"_s,
                "Ongoing"_s + boost::sml::event<evt::AudioRequest> / HandleAudioRequest = "Ongoing"_s,
                // end call and reject when call is ongoing is same
                "Ongoing"_s + boost::sml::event<evt::Ended> / HandleEndCall  = "Idle"_s,
                "Ongoing"_s + boost::sml::event<evt::Reject> / HandleEndCall = "Idle"_s,

                *("exceptions handling"_s) + exception<std::runtime_error> / ExceptionHandler = "Idle"_s,
                "exceptions handling"_s + exception<std::runtime_error> / ExceptionHandler    = "Idle"_s);
        }
    };

    struct StateMachine
    {
        CallData call{};
        Dependencies di{};
        Logger logger;

        explicit StateMachine(Dependencies di) : di(std::move(di))
        {}

        bool active() const
        {
            using namespace boost::sml;
            return not machine.is("Idle"_s);
        }

        boost::sml::sm<SM, boost::sml::logger<Logger>> machine{logger, di, call};
    };

} // namespace call

D module-services/service-cellular/call/CallMulticast.hpp => module-services/service-cellular/call/CallMulticast.hpp +0 -22
@@ 1,22 0,0 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <PhoneNumber.hpp>

namespace sys
{
    class Service;
}

class CallMulticast
{
    sys::Service &owner;

  public:
    explicit CallMulticast(sys::Service &owner) : owner(owner)
    {}
    void notifyIncommingCall();
    void notifyIdentifiedCall(const utils::PhoneNumber::View &number);
};

D module-services/service-cellular/call/CallRingGuard.cpp => module-services/service-cellular/call/CallRingGuard.cpp +0 -21
@@ 1,21 0,0 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "CallRingGuard.hpp"
#include "call/CellularCall.hpp"

bool callRingGuard(CellularCall::Call &call)
{
    return call.mode == sys::phone_modes::PhoneMode::Connected;
}

bool callClipGuard(CellularCall::Call &call)
{
    return call.mode == sys::phone_modes::PhoneMode::DoNotDisturb && call.operations.areCallsFromFavouritesEnabled() &&
           call.operations.isNumberInFavourites();
}

bool callDNDGuard(CellularCall::Call &call)
{
    return call.mode == sys::phone_modes::PhoneMode::DoNotDisturb;
}

D module-services/service-cellular/call/CallRingGuard.hpp => module-services/service-cellular/call/CallRingGuard.hpp +0 -34
@@ 1,34 0,0 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#pragma once

namespace CellularCall
{
    class Call;
}

/// Guard to check if we should ring on cellular RING URC
/// This flow is when:
/// - We are in normal mode
/// - somebody calls
/// Without this check we could never ring on private numbers
bool callRingGuard(CellularCall::Call &call);

/// Guard to check if we should ring on cellular CLIP URC
/// This flow is when:
/// - We are in do not disturb mode
/// - somebody calls
/// - we have this somebody phone number via CLIP URC
/// It wont be ever called for private numbers
bool callClipGuard(CellularCall::Call &call);

/// Guard to check if we should place missed call notification
/// This flow is when:
/// - We are in do not disturb mode
/// - somebody calls
/// - we have this somebody phone number via CLIP/RING URC
/// It wont be ever called for private numbers
bool callDNDGuard(CellularCall::Call &call);

M module-services/service-cellular/call/CellularCall.cpp => module-services/service-cellular/call/CellularCall.cpp +74 -169
@@ 2,11 2,9 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "call/CellularCall.hpp"
#include "service-cellular/call/CallRingGuard.hpp"
#include "service-cellular/ServiceCellular.hpp"
#include "service-db/agents/settings/SystemSettings.hpp"

#include <queries/notifications/QueryNotificationsIncrement.hpp>
#include <CalllogRecord.hpp>
#include <PhoneNumber.hpp>
#include <Utils.hpp>


@@ 14,217 12,124 @@

#include <cinttypes>
#include <ctime>
#include <memory>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "CallMachine.hpp"

namespace CellularCall
namespace call
{
    Call::Call(ServiceCellular &owner) : owner(owner), audio(owner), multicast(owner), gui(owner), db(owner)
    {
        utils::PhoneNumber::View number = utils::PhoneNumber::View();
        const CallType type             = CallType::CT_NONE;
        const time_t date               = 0;
        const time_t duration           = 0;

        clear();

        this->call.phoneNumber = number;
        this->call.date        = date;
        this->call.duration    = duration;
        this->call.type        = type;
        this->call.name        = number.getEntered(); // temporary set number as name
    }

    bool Call::handleRING()
    {
        if (callRingGuard(*this)) {
            startCall(utils::PhoneNumber::View(), CallType::CT_INCOMING);
            audio.play();

            if (!wasRinging) {
                wasRinging = true;
                gui.notifyRING();
                multicast.notifyIncommingCall();
            }
            return true;
        }
        return false;
    }
    Call::Call()
    {}

    bool Call::handleCLIP(const utils::PhoneNumber::View &number)
    Call::Call(ServiceCellular *owner,
               CellularMux *cmux,
               sys::TimerHandle timer,
               std::shared_ptr<sys::CpuSentinel> sentinel)
    {
        setNumber(number);
        if (callClipGuard(*this)) {
            startCall(number, CallType::CT_INCOMING);
            audio.play();
        }
        machine = std::make_unique<StateMachine>(Dependencies{std::make_unique<Audio>(owner),
                                                              std::make_unique<CallMulticast>(owner),
                                                              std::make_unique<CallGUI>(owner),
                                                              std::make_unique<CallDB>(owner),
                                                              std::make_unique<CallTimer>(std::move(timer)),
                                                              std::make_unique<cellular::Api>(owner, cmux),
                                                              std::move(sentinel)});
    }

        if (callClipGuard(*this) || callRingGuard(*this)) {
            if (!isNumberDisplayed) {
                isNumberDisplayed = true;
                gui.notifyCLIP(number);
                multicast.notifyIdentifiedCall(number);
            }
            return true;
        }
    Call::~Call()
    {}

        if (callDNDGuard(*this)) {
            db.incrementNotAnsweredCallsNotification(number);
    Call &Call::operator=(Call &&other) noexcept
    {
        if (not other.machine) {
            LOG_ERROR("operator= on wrong call");
            return *this;
        }

        return false;
        machine = std::make_unique<StateMachine>(std::move(other.machine->di));
        return *this;
    }

    bool Call::startOutgoing(const utils::PhoneNumber::View &number)
    bool Call::handle(const call::event::RING &ring)
    {
        return startCall(number, CallType::CT_OUTGOING);
        if (machine == nullptr) {
            return false;
        };
        return machine->machine.process_event(ring);
    }

    bool Call::startCall(const utils::PhoneNumber::View &number, const CallType type)
    bool Call::handle(const call::event::CLIP &clip)
    {
        LOG_INFO("starting call");

        if (isValid()) {
            LOG_ERROR("call already established");
            return false;
        }

        if (cpuSentinel) {
            cpuSentinel->HoldMinimumFrequency(bsp::CpuFrequencyMHz::Level_6);
        }

        clear();
        CalllogRecord callRec{type, number};
        call = startCallAction ? startCallAction(callRec) : CalllogRecord();
        if (!call.isValid()) {
            LOG_ERROR("failed to obtain a call log record");
            clear();
            if (cpuSentinel) {
                cpuSentinel->ReleaseMinimumFrequency();
            }
            LOG_INFO("failed to start call");
        if (machine == nullptr) {
            return false;
        }

        gui.notifyCallStarted(number, type);
        LOG_INFO("call started");
        return true;
        };
        return machine->machine.process_event(clip);
    }

    bool Call::setActive()
    bool Call::handle(const call::event::StartCall &call)
    {
        if (isValid()) {
            startActiveTime = utils::time::getCurrentTimestamp();
            isActiveCall    = true;
            gui.notifyCallActive();
            LOG_INFO("set call active");
            return true;
        }
        LOG_ERROR("no valid call to activate");
        return false;
        if (machine == nullptr) {
            return false;
        };
        return machine->machine.process_event(call);
    }

    bool Call::endCall(Forced forced)
    bool Call::handle(const call::event::Ended &ended)
    {
        audio.stop();
        LOG_INFO("ending call");
        if (!isValid()) {
            LOG_ERROR("no call to end");
        if (machine == nullptr) {
            return false;
        }

        if (cpuSentinel) {
            cpuSentinel->ReleaseMinimumFrequency();
        }

        if (isActiveCall) {
            auto endTime  = utils::time::getCurrentTimestamp();
            call.duration = (endTime - startActiveTime).get();
        }
        else {
            auto callType = call.type;
            switch (callType) {
            case CallType::CT_INCOMING: {
                if (forced == Forced::True) {
                    setType(CallType::CT_REJECTED);
                }
                else {
                    setType(CallType::CT_MISSED);
                    markUnread();
                }
            } break;

            case CallType::CT_OUTGOING: {
                // do nothing
            } break;

            default:
                LOG_ERROR("Not a valid call type %u", static_cast<int>(callType));
                return false;
            }
        }

        if (!(endCallAction && endCallAction(call))) {
            LOG_ERROR("CalllogUpdate failed, id %" PRIu32, call.ID);
            return false;
        }

        // Calllog entry was updated, ongoingCall can be cleared
        clear();
        gui.notifyCallEnded();
        LOG_INFO("call ended");
        return true;
        };
        return machine->machine.process_event(ended);
    }

    void Call::setNumber(const utils::PhoneNumber::View &number)
    bool Call::handle(const call::event::Reject &reject)
    {
        call.presentation = number.getFormatted().empty() ? PresentationType::PR_UNKNOWN : PresentationType::PR_ALLOWED;
        call.phoneNumber  = number;
        if (machine == nullptr) {
            return false;
        };
        return machine->machine.process_event(reject);
    }

    void Call::setCpuSentinel(std::shared_ptr<sys::CpuSentinel> sentinel)
    bool Call::handle(const call::event::AudioRequest &request)
    {
        cpuSentinel = std::move(sentinel);
        if (machine == nullptr) {
            return false;
        };
        return machine->machine.process_event(request);
    }

    void Call::handleCallAudioEventRequest(cellular::CallAudioEventRequest::EventType event)
    bool Call::handle(const call::event::ModeChange &change)
    {
        switch (event) {
        case cellular::CallAudioEventRequest::EventType::Mute:
            audio.muteCall();
            break;
        case cellular::CallAudioEventRequest::EventType::Unmute:
            audio.unmuteCall();
            break;
        case cellular::CallAudioEventRequest::EventType::LoudspeakerOn:
            audio.setLaudspeakerOn();
            break;
        case cellular::CallAudioEventRequest::EventType::LoudspeakerOff:
            audio.setLaudspeakerOff();
            break;
        }
        if (machine == nullptr) {
            return false;
        };
        machine->call.mode = change.mode;
        return true;
    }
    void Call::handleCallDurationTimer()

    bool Call::handle(const call::event::Answer &answer)
    {
        if (not isActiveCall) {
            return;
        }
        auto now         = utils::time::getCurrentTimestamp();
        callDurationTime = (now - startActiveTime).getSeconds();
        gui.notifyCallDurationUpdate(callDurationTime);
        if (machine == nullptr) {
            return false;
        };
        return machine->machine.process_event(answer);
    }

    bool Call::Operations::areCallsFromFavouritesEnabled()
    bool Call::handle(const call::event::OngoingTimer &tim)
    {
        return call.owner.areCallsFromFavouritesEnabled();
        if (machine == nullptr) {
            return false;
        };
        return machine->machine.process_event(tim);
    }

    bool Call::Operations::isNumberInFavourites()
    bool Call::active() const
    {
        return DBServiceAPI::IsContactInFavourites(&call.owner, call.call.phoneNumber);
        return machine->active();
    }

} // namespace CellularCall

D module-services/service-cellular/call/README.md => module-services/service-cellular/call/README.md +0 -10
@@ 1,10 0,0 @@
Encapsulated cellular call processing
=====================================

This library's goal is to encapsulate the whole call flow/flows we can have.
It's end goal is to provide: actions and guards to write call state machine to asure full control over call processing.

# library organisation

Public api headers should be placed in `include/call/` catalog - these are exported in cmake to include paths for related libraries
All other headers should **not** be placed there. These are private internals of the library

R module-services/service-cellular/call/CallAudio.cpp => module-services/service-cellular/call/api/CallAudio.cpp +26 -19
@@ 11,54 11,61 @@
#include <Timers/TimerFactory.hpp>
#include <gsl/util>

struct CallRingAudio::CallMeta
struct Audio::CallMeta
{
    sys::Async<AudioStartPlaybackRequest, AudioStartPlaybackResponse> async;
};

CallRingAudio::CallRingAudio(sys::Service &s) : owner(s), meta(new CallRingAudio::CallMeta)
Audio::Audio(sys::Service *s) : owner(s), meta(new Audio::CallMeta)
{}

CallRingAudio::~CallRingAudio()
Audio::~Audio()
{
    delete meta;
}

void CallRingAudio::play()
void Audio::play()
{
    started         = true;
    const auto file = AudioServiceAPI::GetSound(&owner, audio::PlaybackType::CallRingtone);
    meta->async     = owner.async_call<AudioStartPlaybackRequest, AudioStartPlaybackResponse>(
        service::name::audio, file, audio::PlaybackType::CallRingtone);
    if (not started) {
        started         = true;
        const auto file = AudioServiceAPI::GetSound(owner, audio::PlaybackType::CallRingtone);
        meta->async     = owner->async_call<AudioStartPlaybackRequest, AudioStartPlaybackResponse>(
            service::name::audio, file, audio::PlaybackType::CallRingtone);
    }
}

void CallRingAudio::stop()
void Audio::stop()
{
    auto _ = gsl::finally([this] { AudioServiceAPI::StopAll(&owner); });
    auto _ = gsl::finally([this] { AudioServiceAPI::StopAll(owner); });

    if (not started) {
        return;
    }
    started = false;
    owner.sync(meta->async);
    owner->sync(meta->async);
}

void Audio::muteCall()
{
    AudioServiceAPI::SendEvent(owner, audio::EventType::CallMute);
}

void CallRingAudio::muteCall()
void Audio::unmuteCall()
{
    AudioServiceAPI::SendEvent(&owner, audio::EventType::CallMute);
    AudioServiceAPI::SendEvent(owner, audio::EventType::CallUnmute);
}

void CallRingAudio::unmuteCall()
void Audio::setLaudspeakerOn()
{
    AudioServiceAPI::SendEvent(&owner, audio::EventType::CallUnmute);
    AudioServiceAPI::SendEvent(owner, audio::EventType::CallLoudspeakerOn);
}

void CallRingAudio::setLaudspeakerOn()
void Audio::setLaudspeakerOff()
{
    AudioServiceAPI::SendEvent(&owner, audio::EventType::CallLoudspeakerOn);
    AudioServiceAPI::SendEvent(owner, audio::EventType::CallLoudspeakerOff);
}

void CallRingAudio::setLaudspeakerOff()
void Audio::routingStart()
{
    AudioServiceAPI::SendEvent(&owner, audio::EventType::CallLoudspeakerOff);
    AudioServiceAPI::RoutingStart(owner);
}

R module-services/service-cellular/call/CallAudio.hpp => module-services/service-cellular/call/api/CallAudio.hpp +21 -4
@@ 8,16 8,32 @@ namespace sys
    class Service;
}

class CallRingAudio
namespace call::api
{
    class Audio
    {
      public:
        virtual void play()              = 0;
        virtual void stop()              = 0;
        virtual void muteCall()          = 0;
        virtual void unmuteCall()        = 0;
        virtual void setLaudspeakerOn()  = 0;
        virtual void setLaudspeakerOff() = 0;
        virtual void routingStart()      = 0;
        virtual ~Audio()                 = default;
    };
} // namespace call::api

class Audio : public call::api::Audio
{
    struct CallMeta;
    sys::Service &owner;
    sys::Service *owner;
    CallMeta *meta = nullptr;
    bool started   = false;

  public:
    explicit CallRingAudio(sys::Service &);
    ~CallRingAudio();
    explicit Audio(sys::Service *);
    ~Audio();

    void play();
    void stop();


@@ 25,4 41,5 @@ class CallRingAudio
    void unmuteCall();
    void setLaudspeakerOn();
    void setLaudspeakerOff();
    void routingStart();
};

A module-services/service-cellular/call/api/CallDB.cpp => module-services/service-cellular/call/api/CallDB.cpp +52 -0
@@ 0,0 1,52 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "CallDB.hpp"
#include "service-appmgr/data/CallActionsParams.hpp"
#include <queries/notifications/QueryNotificationsIncrement.hpp>
#include <service-appmgr/Controller.hpp>
#include "call/CellularCall.hpp"

CallDB::CallDB(sys::Service *s) : owner(s)
{}

void CallDB::incrementNotAnsweredCallsNotification(const utils::PhoneNumber::View &number)
{
    DBServiceAPI::GetQuery(
        owner,
        db::Interface::Name::Notifications,
        std::make_unique<db::query::notifications::Increment>(NotificationsRecord::Key::Calls, number));
}

void CallDB::startCall(CalllogRecord &rec)
{
    if (rec.ID == DB_ID_NONE) {
        auto call = DBServiceAPI::CalllogAdd(owner, rec);
        rec       = call;
    }
    else {
        auto ret = DBServiceAPI::CalllogUpdate(owner, rec);
        if (not ret) {
            throw std::runtime_error("CalllogUpdate failed");
        }
    }
    if (rec.ID == DB_ID_NONE) {
        throw std::runtime_error("CalllogAdd failed");
    }
}

void CallDB::endCall(const CalllogRecord &rec)
{
    if (DBServiceAPI::CalllogUpdate(owner, rec) && rec.type == CallType::CT_MISSED) {
        DBServiceAPI::GetQuery(
            owner,
            db::Interface::Name::Notifications,
            std::make_unique<db::query::notifications::Increment>(NotificationsRecord::Key::Calls, rec.phoneNumber));
        return;
    }
}

bool CallDB::isNumberInFavourites(const utils::PhoneNumber::View &number)
{
    return DBServiceAPI::IsContactInFavourites(owner, number);
}

A module-services/service-cellular/call/api/CallDB.hpp => module-services/service-cellular/call/api/CallDB.hpp +44 -0
@@ 0,0 1,44 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "CalllogRecord.hpp"
#include <PhoneNumber.hpp>

namespace sys
{
    class Service;
}
namespace CellularCall
{
    class Call;
}

namespace call::api
{
    class DB
    {
      public:
        virtual void incrementNotAnsweredCallsNotification(const utils::PhoneNumber::View &number) = 0;
        // overrides record data
        virtual void startCall(CalllogRecord &rec)                                = 0;
        virtual void endCall(const CalllogRecord &rec)                            = 0;
        virtual bool isNumberInFavourites(const utils::PhoneNumber::View &number) = 0;
        virtual ~DB()                                                             = default;
    };

} // namespace call::api

class CallDB : public call::api::DB
{
    sys::Service *owner;

  public:
    explicit CallDB(sys::Service *);

    void incrementNotAnsweredCallsNotification(const utils::PhoneNumber::View &number) override;
    void startCall(CalllogRecord &rec) override;
    void endCall(const CalllogRecord &rec) override;
    bool isNumberInFavourites(const utils::PhoneNumber::View &number) override;
};

R module-services/service-cellular/call/CallGUI.cpp => module-services/service-cellular/call/api/CallGUI.cpp +12 -13
@@ 6,42 6,41 @@
#include "service-appmgr/data/CallActionsParams.hpp"
#include <service-appmgr/Controller.hpp>

CallGUI::CallGUI(sys::Service &s) : owner(s)
CallGUI::CallGUI(sys::Service *s) : owner(s)
{}

void CallGUI::notifyRING()
{
    app::manager::Controller::sendAction(
        &owner, app::manager::actions::HandleIncomingCall, std::make_unique<app::manager::actions::CallParams>());
        owner, app::manager::actions::HandleIncomingCall, std::make_unique<app::manager::actions::CallParams>());
}

void CallGUI::notifyCLIP(const utils::PhoneNumber::View &number)
{
    app::manager::Controller::sendAction(
        &owner, app::manager::actions::HandleCallerId, std::make_unique<app::manager::actions::CallParams>(number));
        owner, app::manager::actions::HandleCallerId, std::make_unique<app::manager::actions::CallParams>(number));
}

void CallGUI::notifyCallStarted(utils::PhoneNumber phoneNumber, const CallType type)
void CallGUI::notifyCallStarted(const utils::PhoneNumber &number, const CallType &type)
{
    owner.bus.sendMulticast(
        std::make_shared<cellular::CallStartedNotification>(phoneNumber, type == CallType::CT_INCOMING),
        sys::BusChannel::ServiceCellularNotifications);
    owner->bus.sendMulticast(std::make_shared<cellular::CallStartedNotification>(number, type == CallType::CT_INCOMING),
                             sys::BusChannel::ServiceCellularNotifications);
}

void CallGUI::notifyCallEnded()
{
    owner.bus.sendMulticast(std::make_shared<cellular::CallEndedNotification>(),
                            sys::BusChannel::ServiceCellularNotifications);
    owner->bus.sendMulticast(std::make_shared<cellular::CallEndedNotification>(),
                             sys::BusChannel::ServiceCellularNotifications);
}

void CallGUI::notifyCallActive()
{
    owner.bus.sendMulticast(std::make_shared<cellular::CallActiveNotification>(),
                            sys::BusChannel::ServiceCellularNotifications);
    owner->bus.sendMulticast(std::make_shared<cellular::CallActiveNotification>(),
                             sys::BusChannel::ServiceCellularNotifications);
}

void CallGUI::notifyCallDurationUpdate(const time_t &duration)
{
    owner.bus.sendMulticast(std::make_shared<cellular::CallDurationNotification>(duration),
                            sys::BusChannel::ServiceCellularNotifications);
    owner->bus.sendMulticast(std::make_shared<cellular::CallDurationNotification>(duration),
                             sys::BusChannel::ServiceCellularNotifications);
}

A module-services/service-cellular/call/api/CallGUI.hpp => module-services/service-cellular/call/api/CallGUI.hpp +43 -0
@@ 0,0 1,43 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <PhoneNumber.hpp>
#include <Tables/CalllogTable.hpp>

namespace sys
{
    class Service;
}

namespace call::api
{
    class GUI
    {
      public:
        virtual void notifyRING()                                                              = 0;
        virtual void notifyCLIP(const utils::PhoneNumber::View &number)                        = 0;
        virtual void notifyCallStarted(const utils::PhoneNumber &number, const CallType &type) = 0;
        virtual void notifyCallEnded()                                                         = 0;
        virtual void notifyCallActive()                                                        = 0;
        virtual void notifyCallDurationUpdate(const time_t &duration)                          = 0;
        virtual ~GUI()                                                                         = default;
    };
}; // namespace call::api

class CallGUI : public call::api::GUI
{
    struct CallMeta;
    sys::Service *owner;

  public:
    explicit CallGUI(sys::Service *);

    void notifyRING() override;
    void notifyCLIP(const utils::PhoneNumber::View &number) override;
    void notifyCallStarted(const utils::PhoneNumber &number, const CallType &type) override;
    void notifyCallEnded() override;
    void notifyCallActive() override;
    void notifyCallDurationUpdate(const time_t &duration) override;
};

R module-services/service-cellular/call/CallMulticast.cpp => module-services/service-cellular/call/api/CallMulticast.cpp +17 -4
@@ 7,12 7,25 @@

void CallMulticast::notifyIncommingCall()
{
    owner.bus.sendMulticast(std::make_shared<CellularIncominCallMessage>(),
                            sys::BusChannel::ServiceCellularNotifications);
    owner->bus.sendMulticast(std::make_shared<CellularIncominCallMessage>(),
                             sys::BusChannel::ServiceCellularNotifications);
}

void CallMulticast::notifyIdentifiedCall(const utils::PhoneNumber::View &number)
{
    owner.bus.sendMulticast(std::make_shared<CellularCallerIdMessage>(number),
                            sys::BusChannel::ServiceCellularNotifications);
    owner->bus.sendMulticast(std::make_shared<CellularCallerIdMessage>(number),
                             sys::BusChannel::ServiceCellularNotifications);
}

void CallMulticast::notifyCallActive()
{

    owner->bus.sendMulticast(std::make_shared<CellularCallActiveNotification>(),
                             sys::BusChannel::ServiceCellularNotifications);
}

void CallMulticast::notifyCallAborted()
{
    owner->bus.sendMulticast(std::make_shared<CellularCallAbortedNotification>(),
                             sys::BusChannel::ServiceCellularNotifications);
}

A module-services/service-cellular/call/api/CallMulticast.hpp => module-services/service-cellular/call/api/CallMulticast.hpp +37 -0
@@ 0,0 1,37 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <PhoneNumber.hpp>

namespace sys
{
    class Service;
}

namespace call::api
{
    class Multicast
    {
      public:
        virtual void notifyIncommingCall()                                        = 0;
        virtual void notifyIdentifiedCall(const utils::PhoneNumber::View &number) = 0;
        virtual void notifyCallActive()                                           = 0;
        virtual void notifyCallAborted()                                          = 0;
        virtual ~Multicast()                                                      = default;
    };
} // namespace call::api

class CallMulticast : public call::api::Multicast
{
    sys::Service *owner;

  public:
    explicit CallMulticast(sys::Service *owner) : owner(owner)
    {}
    void notifyIncommingCall() override;
    void notifyIdentifiedCall(const utils::PhoneNumber::View &number) override;
    void notifyCallActive() override;
    void notifyCallAborted() override;
};

A module-services/service-cellular/call/api/CallTimer.cpp => module-services/service-cellular/call/api/CallTimer.cpp +24 -0
@@ 0,0 1,24 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "CallTimer.hpp"
#include <Timers/TimerFactory.hpp>

CallTimer::CallTimer(sys::TimerHandle handle) : handle(std::move(handle))
{}

void CallTimer::start()
{
    startActiveTime = std::time(nullptr);
    handle.start();
}

void CallTimer::stop()
{
    handle.stop();
}

time_t CallTimer::duration()
{
    return std::time(nullptr) - startActiveTime;
}

A module-services/service-cellular/call/api/CallTimer.hpp => module-services/service-cellular/call/api/CallTimer.hpp +31 -0
@@ 0,0 1,31 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <memory>
#include <Timers/TimerHandle.hpp>

namespace call::api
{
    class Timer
    {
      public:
        virtual void start()      = 0;
        virtual void stop()       = 0;
        virtual time_t duration() = 0;
        virtual ~Timer()          = default;
    };
}; // namespace call::api

class CallTimer : public call::api::Timer
{
    sys::TimerHandle handle;
    std::time_t startActiveTime{};

  public:
    explicit CallTimer(sys::TimerHandle handle);
    void start() override;
    void stop() override;
    time_t duration() override;
};

A module-services/service-cellular/call/api/ModemCallApi.cpp => module-services/service-cellular/call/api/ModemCallApi.cpp +61 -0
@@ 0,0 1,61 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "ModemCallApi.hpp"
#include <modem/mux/CellularMux.h>
#include <stdexcept>
#include <service-cellular/ServiceCellular.hpp>
#include <service-db/agents/settings/SystemSettings.hpp>

namespace cellular
{
    Api::Api(ServiceCellular *cellular, CellularMux *cmux) : cellular(cellular), cmux(cmux)
    {}

    bool Api::answerIncommingCall()
    {
        if (cmux == nullptr) {
            throw std::runtime_error("call api not initialized");
        }
        auto channel = cmux->get(CellularMux::Channel::Commands);
        if (channel != nullptr) {
            auto response = channel->cmd(at::AT::ATA);
            if (response) {
                return true;
            }
        }
        return false;
    }

    bool Api::hangupCall()
    {
        if (cmux == nullptr) {
            throw std::runtime_error("call api not initialized");
        }
        auto channel = cmux->get(CellularMux::Channel::Commands);
        return channel != nullptr && channel->cmd(at::AT::ATH);
    }

    bool Api::rejectCall()
    {
        return hangupCall();
    }

    bool Api::areCallsFromFavouritesEnabled()
    {
        if (cellular == nullptr) {
            throw std::runtime_error("call api not initialized");
        }

        return utils::getNumericValue<bool>(
            cellular->settings->getValue(settings::Offline::callsFromFavorites, settings::SettingsScope::Global));
    }

    sys::phone_modes::PhoneMode Api::getMode()
    {
        if (cellular == nullptr) {
            throw std::runtime_error("call api not initialized");
        }
        return cellular->phoneModeObserver->getCurrentPhoneMode();
    }
} // namespace cellular

A module-services/service-cellular/call/api/ModemCallApi.hpp => module-services/service-cellular/call/api/ModemCallApi.hpp +42 -0
@@ 0,0 1,42 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "PhoneModes/PhoneMode.hpp"
class CellularMux;
class ServiceCellular;

namespace call::api
{
    class Api
    {
      public:
        virtual bool answerIncommingCall()            = 0;
        virtual bool hangupCall()                     = 0;
        virtual bool rejectCall()                     = 0;
        virtual bool areCallsFromFavouritesEnabled()  = 0;
        virtual sys::phone_modes::PhoneMode getMode() = 0;
        virtual ~Api()                                = default;
    };
} // namespace call::api

namespace cellular
{
    class Api : public call::api::Api
    {
      private:
        ServiceCellular *cellular = nullptr;
        CellularMux *cmux         = nullptr;

      public:
        Api() = default;
        Api(ServiceCellular *cellular, CellularMux *);

        bool answerIncommingCall() override;
        bool hangupCall() override;
        bool rejectCall() override;
        bool areCallsFromFavouritesEnabled() override;
        sys::phone_modes::PhoneMode getMode() override;
    };
} // namespace cellular

A module-services/service-cellular/call/doc/README.md => module-services/service-cellular/call/doc/README.md +55 -0
@@ 0,0 1,55 @@
Call documentation
==================

MuditaOS call library that is able to perform:
- ingoring and outgoing calls
- perform call filtering
    - on RING (notification that there is incomming call)
    - on CLIP (notification on identified call)
    - based on contacts
    - based on phone mode

The call logic is one of oldest functionalities in the OS, which was being changed mulitiple times in the past.
Therefore it has multiple visible (mis)conceptions. Curent aproach we want to keep is:
- Call handling logic should be in CellularCall.hpp it's an interface for ServiceCellular to use to perform calls
    - Especially all actions changing call - i.e. hanging, acting upon CLIP/RING, or setting up audio
    - It's responsible to multicast specyfic call states to parties interested (i.e. call answered)
- Call UI should be in application-call - and only UI, there should be no more state handling then required to show UI
    - all actions that changes call - shoud be passed to CellularCall

**NOTE:** Historically lots of state keeping was done in application call, while quite a bit of logic was glued in ServiceCellular - this is wrong way to go.


# library organisation

This library's goal is to encapsulate the whole call flow/flows we can have.
It's end goal is to provide: actions and guards to write call state machine to asure full control over call processing.

Public api headers should be placed in `include/call/` catalog - these are exported in cmake to include paths for related libraries
All other headers should **not** be placed there. These are private internals of the library

# Call - how to write logic

CellularCall.hpp is written such as:
- CellularCall is state machine processor
- It has Call StateMachine inside and drives it
- API it requires is in `api` catalog
    - any api has to first define virtual interface
    - then define platform implementation

State machine is written using [boost::stm](https://boost-ext.github.io/sml/index.html)

**NOTE** API catalog should and can be further split into api header and platform implementation.

# Call state machine

Call state machine can be generated with:
```
ninja call_uml
call_uml > call.puml
```

Latest generated UML:
![](call.svg)

**NOTE*:** The UML is generated automatically, for better readability please change lambdas to functions

A module-services/service-cellular/call/doc/call.svg => module-services/service-cellular/call/doc/call.svg +61 -0
@@ 0,0 1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentScriptType="application/ecmascript" contentStyleType="text/css" height="673px" preserveAspectRatio="none" style="width:1725px;height:673px;background:#FFFFFF;" version="1.1" viewBox="0 0 1725 673" width="1725px" zoomAndPan="magnify"><defs><filter height="300%" id="fagroh412bl9h" width="300%" x="-1" y="-1"><feGaussianBlur result="blurOut" stdDeviation="2.0"/><feColorMatrix in="blurOut" result="blurOut2" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .4 0"/><feOffset dx="4.0" dy="4.0" in="blurOut2" result="blurOut3"/><feBlend in="SourceGraphic" in2="blurOut3" mode="normal"/></filter></defs><g><ellipse cx="689.42" cy="16" fill="#000000" filter="url(#fagroh412bl9h)" rx="10" ry="10" style="stroke:none;stroke-width:1.0;"/><g id="Idle"><rect fill="#FEFECE" filter="url(#fagroh412bl9h)" height="71.7561" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="108" x="635.42" y="87"/><line style="stroke:#A80036;stroke-width:1.5;" x1="635.42" x2="743.42" y1="116.0679" y2="116.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="26" x="676.42" y="106.9659">Idle</text><text fill="#000000" font-family="sans-serif" font-size="12" lengthAdjust="spacing" textLength="55" x="640.42" y="133.896">on_entry /</text><text fill="#000000" font-family="sans-serif" font-size="12" lengthAdjust="spacing" textLength="85" x="643.42" y="150.2401">call::handle_init</text></g><g id="Ringing"><rect fill="#FEFECE" filter="url(#fagroh412bl9h)" height="50" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="73" x="207.92" y="276.5"/><line style="stroke:#A80036;stroke-width:1.5;" x1="207.92" x2="280.92" y1="305.5679" y2="305.5679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="53" x="217.92" y="296.4659">Ringing</text></g><g id="HaveId"><rect fill="#FEFECE" filter="url(#fagroh412bl9h)" height="50" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="67" x="655.92" y="444"/><line style="stroke:#A80036;stroke-width:1.5;" x1="655.92" x2="722.92" y1="473.0679" y2="473.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="47" x="665.92" y="463.9659">HaveId</text></g><g id="Starting"><rect fill="#FEFECE" filter="url(#fagroh412bl9h)" height="50" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="74" x="1216.42" y="444"/><line style="stroke:#A80036;stroke-width:1.5;" x1="1216.42" x2="1290.42" y1="473.0679" y2="473.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="54" x="1226.42" y="463.9659">Starting</text></g><g id="Ongoing"><rect fill="#FEFECE" filter="url(#fagroh412bl9h)" height="50" rx="12.5" ry="12.5" style="stroke:#A80036;stroke-width:1.5;" width="79" x="1143.92" y="609"/><line style="stroke:#A80036;stroke-width:1.5;" x1="1143.92" x2="1222.92" y1="638.0679" y2="638.0679"/><text fill="#000000" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="59" x="1153.92" y="628.9659">Ongoing</text></g><!--MD5=[38f1f8b7f214ff6bc6d4b705f682283d]
link *start to Idle--><path d="M689.42,26.32 C689.42,38.72 689.42,61.43 689.42,81.62 " fill="none" id="*start-to-Idle" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="689.42,86.69,693.4457,77.7015,689.4343,81.69,685.4457,77.6786,689.42,86.69" style="stroke:#A80036;stroke-width:1.0;"/><!--MD5=[4a14721c1eaf3d017c18f39545a6a6ef]
link Idle to Ringing--><path d="M635.22,126.97 C510.58,134.34 212.03,155.36 182.42,189 C166.28,207.35 172.55,221.64 182.42,244 C187.26,254.96 195.39,264.71 204.2,272.89 " fill="none" id="Idle-to-Ringing" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="208.01,276.3,203.9599,267.3224,204.2801,272.9702,198.6323,273.2903,208.01,276.3" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="96" x="184.42" y="203.897">call::event::RING</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="72" x="197.92" y="221.603">[call::RING] /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="95" x="186.42" y="239.309">call::HandleRing</text><!--MD5=[b445053e7a1d5b3c2afc54b1d6f0c485]
link Ringing to Idle--><path d="M262.94,276.26 C270.37,266.35 278.94,254.72 286.42,244 C303.15,220.04 298.04,205.09 322.42,189 C372.05,156.25 539.56,137.03 630.12,128.78 " fill="none" id="Ringing-to-Idle" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="635.18,128.33,625.8614,125.142,630.1996,128.7724,626.5692,133.1106,635.18,128.33" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="109" x="332.92" y="212.897">call::event::Reject /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="125" x="326.42" y="230.603">call::HandleRejectCall</text><!--MD5=[b445053e7a1d5b3c2afc54b1d6f0c485]
link Ringing to Idle--><path d="M281.16,298.6 C326.33,294.62 403.48,282.27 456.42,244 C480.1,226.89 469.25,206.8 492.42,189 C532.63,158.12 588.19,141.54 630.06,132.88 " fill="none" id="Ringing-to-Idle-1" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="635.1,131.86,625.4879,129.7135,630.1982,132.8461,627.0656,137.5564,635.1,131.86" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="111" x="501.92" y="212.897">call::event::Ended /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="125" x="496.42" y="230.603">call::HandleRejectCall</text><!--MD5=[b445053e7a1d5b3c2afc54b1d6f0c485]
link Ringing to Idle--><path d="M207.88,296.57 C150.88,289.62 45,273.04 22.42,244 C7.42,224.7 6,207.1 22.42,189 C62.87,144.43 473.7,129.44 629.74,125.34 " fill="none" id="Ringing-to-Idle-2" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="635.18,125.19,626.0799,121.4233,630.1817,125.3189,626.2861,129.4207,635.18,125.19" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="92" x="47.92" y="203.897">call::event::CLIP</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="125" x="32.92" y="221.603">[call::ClipDND_NOK] /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="138" x="26.42" y="239.309">call::HandleDND_Reject</text><!--MD5=[d9a236f899448a21faa88e2b2c7523ea]
link Idle to HaveId--><path d="M736.63,159.18 C768.99,186.44 809.23,227.35 827.42,274 C854.18,342.59 776.89,409.65 727.39,444.11 " fill="none" id="Idle-to-HaveId" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="722.99,447.14,732.6725,445.3374,727.1102,444.3074,728.1403,438.7451,722.99,447.14" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="92" x="871.92" y="288.897">call::event::CLIP</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="128" x="855.42" y="306.603">[call::ClipConnected] /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="166" x="836.42" y="324.309">call::HandleClipWithoutRing</text><!--MD5=[d9a236f899448a21faa88e2b2c7523ea]
link Idle to HaveId--><path d="M743.83,134.16 C853.13,157.1 1083.05,219.65 1011.42,329 C947.61,426.42 799.41,455.86 728.54,464.52 " fill="none" id="Idle-to-HaveId-1" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="723.16,465.15,732.5662,468.0695,728.1257,464.565,731.6302,460.1244,723.16,465.15" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="92" x="1039.92" y="288.897">call::event::CLIP</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="115" x="1029.92" y="306.603">[call::ClipDND_OK] /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="118" x="1028.42" y="324.309">call::HandleClipDND</text><!--MD5=[b42b0813981780530edef3df7548b0ab]
link HaveId to Idle--><path d="M666.4,443.93 C643.12,420.43 605.39,384.58 568.42,359 C544.56,342.49 525.51,353.79 510.42,329 C497.72,308.12 495.46,293.33 510.42,274 C543.03,231.89 584.69,277.08 626.42,244 C651.94,223.77 668.28,190.35 677.91,163.91 " fill="none" id="HaveId-to-Idle" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="679.63,159.05,672.8525,166.196,677.9592,163.7626,680.3926,168.8693,679.63,159.05" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="111" x="522.92" y="297.897">call::event::Ended /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="131" x="514.42" y="315.603">call::HandleMissedCall</text><!--MD5=[b42b0813981780530edef3df7548b0ab]
link HaveId to Idle--><path d="M689.42,443.6 C689.42,385.08 689.42,236.26 689.42,164.25 " fill="none" id="HaveId-to-Idle-1" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="689.42,159.2,685.42,168.2,689.42,164.2,693.42,168.2,689.42,159.2" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="109" x="699.92" y="297.897">call::event::Reject /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="125" x="693.42" y="315.603">call::HandleRejectCall</text><!--MD5=[4f7116d012c72546cb0d8b5f1cb34cc3]
link Idle to Idle--><path d="M743.74,110.63 C762.89,110.43 778.42,114.56 778.42,123 C778.42,130.65 765.66,134.76 749.02,135.31 " fill="none" id="Idle-to-Idle" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="743.74,135.37,752.7851,139.2669,748.7397,135.3129,752.6937,131.2674,743.74,135.37" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="92" x="808.92" y="110.397">call::event::CLIP</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="125" x="793.92" y="128.103">[call::ClipDND_NOK] /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="138" x="787.42" y="145.809">call::HandleDND_Reject</text><!--MD5=[5038e0717032de67f19838e91765af9d]
link Idle to Starting--><path d="M743.72,125.88 C840.16,132.19 1041.83,159.22 1155.42,274 C1202.99,322.07 1231.99,397.91 1245.1,439.08 " fill="none" id="Idle-to-Starting" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="1246.62,443.93,1247.7368,434.1447,1245.1205,439.1601,1240.105,436.5439,1246.62,443.93" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="123" x="1195.42" y="297.897">call::event::StartCall /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="118" x="1199.42" y="315.603">call::HandleStartCall</text><!--MD5=[a8a6e8911ee4b822779a5c15ea0fe9d5]
link Starting to Ongoing--><path d="M1290.55,489.72 C1303.05,498.42 1315.56,509.91 1322.42,524 C1333.13,545.97 1336.23,558.83 1322.42,579 C1301.4,609.72 1260.13,622.97 1228,628.68 " fill="none" id="Starting-to-Ongoing" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="1222.93,629.53,1232.4686,631.9825,1227.8608,628.7011,1231.1423,624.0933,1222.93,629.53" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="111" x="1334.42" y="547.897">call::event::Ended /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="112" x="1335.42" y="565.603">call::HandleEndCall</text><!--MD5=[a8a6e8911ee4b822779a5c15ea0fe9d5]
link Starting to Ongoing--><path d="M1216.08,473.25 C1157.2,479.22 1047.05,494.18 1023.42,524 C1008.24,543.16 1009.17,559.14 1023.42,579 C1049.34,615.09 1100.64,627.38 1138.19,631.39 " fill="none" id="Starting-to-Ongoing-1" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="1143.63,631.93,1135.0616,627.0738,1138.6537,631.4438,1134.2837,635.0359,1143.63,631.93" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="109" x="1027.42" y="547.897">call::event::Reject /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="112" x="1027.42" y="565.603">call::HandleEndCall</text><!--MD5=[a8a6e8911ee4b822779a5c15ea0fe9d5]
link Starting to Ongoing--><path d="M1216.16,486.4 C1200.94,495.19 1184.94,507.63 1176.42,524 C1163.57,548.71 1167.77,580.95 1173.69,603.94 " fill="none" id="Starting-to-Ongoing-2" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="1175.05,609,1176.5727,599.2696,1173.7501,604.1719,1168.8478,601.3494,1175.05,609" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="117" x="1186.92" y="547.897">call::event::Answer /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="133" x="1180.42" y="565.603">call::HandleStartedCall</text><!--MD5=[2cebd8ac44a4363bc14d79ffcc983203]
link Ringing to Ongoing--><path d="M229.14,326.58 C215.86,350.87 201.48,388.36 220.42,414 C368.8,614.81 513.34,526.44 757.42,579 C894.7,608.56 1060.6,623.86 1138.68,629.87 " fill="none" id="Ringing-to-Ongoing" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="1143.69,630.26,1135.0253,625.5777,1138.7049,629.8743,1134.4083,633.5539,1143.69,630.26" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="117" x="305.92" y="465.397">call::event::Answer /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="133" x="299.42" y="483.103">call::HandleAnswerCall</text><!--MD5=[1eb793f0abbdfe879580084477c36e66]
link Ringing to HaveId--><path d="M235.23,326.6 C227.43,351.64 220.48,390.38 241.42,414 C268.42,444.44 546.62,461.04 650.55,466.21 " fill="none" id="Ringing-to-HaveId" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="655.66,466.46,646.8647,462.0281,650.6659,466.2174,646.4765,470.0187,655.66,466.46" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="92" x="275.42" y="373.897">call::event::CLIP</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="128" x="258.92" y="391.603">[call::ClipConnected] /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="155" x="245.42" y="409.309">call::HandleClipConnected</text><!--MD5=[1eb793f0abbdfe879580084477c36e66]
link Ringing to HaveId--><path d="M280.94,308.31 C315.88,315.38 368.58,330.14 405.42,359 C428.43,377.01 417.36,397.44 441.42,414 C505.29,457.96 598.53,467 650.58,468.36 " fill="none" id="Ringing-to-HaveId-1" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="655.8,468.47,646.8938,464.2654,650.8013,468.3557,646.7109,472.2633,655.8,468.47" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="92" x="456.92" y="373.897">call::event::CLIP</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="115" x="446.92" y="391.603">[call::ClipDND_OK] /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="118" x="445.42" y="409.309">call::HandleClipDND</text><!--MD5=[5f70ee8c8facd2edf16dd4e5478bfbc0]
link HaveId to Ongoing--><path d="M719.92,494.04 C753.56,519.29 810.29,558.16 865.42,579 C958.54,614.19 1075.66,626.55 1138.56,630.81 " fill="none" id="HaveId-to-Ongoing" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="1143.73,631.15,1135.0125,626.5668,1138.7408,630.8212,1134.4864,634.5495,1143.73,631.15" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="109" x="879.92" y="538.897">call::event::Answer</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="119" x="876.42" y="556.603">[call::CallIncoming] /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="133" x="869.42" y="574.309">call::HandleAnswerCall</text><!--MD5=[97d569762578c974cd2dfaf5367bfc1a]
link Ongoing to Ongoing--><path d="M1223.28,628.98 C1241.77,628.43 1257.92,630.1 1257.92,634 C1257.92,637.53 1244.66,639.24 1228.41,639.12 " fill="none" id="Ongoing-to-Ongoing" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="1223.28,639.02,1232.2101,643.1737,1228.2793,639.1057,1232.3472,635.1749,1223.28,639.02" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="100" x="1267.92" y="630.397">call::event::CLIP /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="105" x="1266.92" y="648.103">call::HandleAddID</text><!--MD5=[97d569762578c974cd2dfaf5367bfc1a]
link Ongoing to Ongoing--><path d="M1223.24,626.86 C1282.93,621.02 1377.92,623.4 1377.92,634 C1377.92,644.29 1288.41,646.83 1228.57,641.63 " fill="none" id="Ongoing-to-Ongoing-1" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="1223.24,641.14,1231.8359,645.9473,1228.219,641.5978,1232.5685,637.9809,1223.24,641.14" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="160" x="1383.92" y="630.397">call::event::OngoingTimer /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="123" x="1403.92" y="648.103">call::HandleCallTimer</text><!--MD5=[97d569762578c974cd2dfaf5367bfc1a]
link Ongoing to Ongoing--><path d="M1223.2,625.93 C1322.77,613.99 1549.92,616.68 1549.92,634 C1549.92,651.03 1330.25,653.91 1228.26,642.65 " fill="none" id="Ongoing-to-Ongoing-2" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="1223.2,642.07,1231.6844,647.0716,1228.1673,642.641,1232.5979,639.1239,1223.2,642.07" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="157" x="1555.92" y="630.397">call::event::AudioRequest /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="152" x="1559.92" y="648.103">call::HandleAudioRequest</text><!--MD5=[08eaf5efa1f60c65a3812368f3ac4307]
link Ongoing to Idle--><path d="M1222.96,631.14 C1291.38,627 1427.33,614.42 1456.42,579 C1549.86,465.28 1437.6,371.59 1327.42,274 C1240.97,197.42 890.4,147.96 748.99,130.78 " fill="none" id="Ongoing-to-Idle" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="743.64,130.13,752.0959,135.1796,748.604,130.7291,753.0544,127.2372,743.64,130.13" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="111" x="1466.42" y="382.897">call::event::Ended /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="112" x="1467.42" y="400.603">call::HandleEndCall</text><!--MD5=[08eaf5efa1f60c65a3812368f3ac4307]
link Ongoing to Idle--><path d="M1222.92,630.44 C1297.14,624.96 1453.88,609.99 1497.42,579 C1537.16,550.72 1638.88,437.36 1588.42,359 C1489.57,205.48 1390.16,238.56 1214.42,189 C1050.08,142.65 847.54,129.34 748.96,125.53 " fill="none" id="Ongoing-to-Idle-1" style="stroke:#A80036;stroke-width:1.0;"/><polygon fill="#A80036" points="743.77,125.33,752.6153,129.6613,748.7666,125.5156,752.9123,121.6668,743.77,125.33" style="stroke:#A80036;stroke-width:1.0;"/><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="109" x="1605.42" y="382.897">call::event::Reject /</text><text fill="#000000" font-family="sans-serif" font-size="13" lengthAdjust="spacing" textLength="112" x="1605.42" y="400.603">call::HandleEndCall</text><!--MD5=[f81c42a19a41ff84caa28b19edd7dd9c]
@startuml

[*] - -> Idle
Idle : on_entry /\n call::handle_init
Idle - -> Ringing : call::event::RING\n [call::RING] /\n call::HandleRing
Idle - -> HaveId : call::event::CLIP\n [call::ClipConnected] /\n call::HandleClipWithoutRing
Idle - -> HaveId : call::event::CLIP\n [call::ClipDND_OK] /\n call::HandleClipDND
Idle - -> Idle : call::event::CLIP\n [call::ClipDND_NOK] /\n call::HandleDND_Reject
Idle - -> Starting : call::event::StartCall /\n call::HandleStartCall
Starting - -> Ongoing : call::event::Ended /\n call::HandleEndCall
Starting - -> Ongoing : call::event::Reject /\n call::HandleEndCall
Starting - -> Ongoing : call::event::Answer /\n call::HandleStartedCall
Ringing - -> Ongoing : call::event::Answer /\n call::HandleAnswerCall
Ringing - -> Idle : call::event::Reject /\n call::HandleRejectCall
Ringing - -> Idle : call::event::Ended /\n call::HandleRejectCall
Ringing - -> HaveId : call::event::CLIP\n [call::ClipConnected] /\n call::HandleClipConnected
Ringing - -> HaveId : call::event::CLIP\n [call::ClipDND_OK] /\n call::HandleClipDND
Ringing - -> Idle : call::event::CLIP\n [call::ClipDND_NOK] /\n call::HandleDND_Reject
HaveId - -> Ongoing : call::event::Answer\n [call::CallIncoming] /\n call::HandleAnswerCall
HaveId - -> Idle : call::event::Ended /\n call::HandleMissedCall
HaveId - -> Idle : call::event::Reject /\n call::HandleRejectCall
Ongoing - -> Ongoing : call::event::CLIP /\n call::HandleAddID
Ongoing - -> Ongoing : call::event::OngoingTimer /\n call::HandleCallTimer
Ongoing - -> Ongoing : call::event::AudioRequest /\n call::HandleAudioRequest
Ongoing - -> Idle : call::event::Ended /\n call::HandleEndCall
Ongoing - -> Idle : call::event::Reject /\n call::HandleEndCall

@enduml

PlantUML version 1.2021.9(Sun Jul 25 12:13:56 CEST 2021)
(GPL source distribution)
Java Runtime: OpenJDK Runtime Environment
JVM: OpenJDK 64-Bit Server VM
Default Encoding: UTF-8
Language: en
Country: US
--></g></svg>
\ No newline at end of file

A module-services/service-cellular/call/doc/uml/CMakeLists.txt => module-services/service-cellular/call/doc/uml/CMakeLists.txt +10 -0
@@ 0,0 1,10 @@
project(call_uml)
add_executable(${PROJECT_NAME} uml_printer.cpp)
target_link_libraries(${PROJECT_NAME} PRIVATE
        log
        module-cellular
        service-cellular-call
        sml::sml
        sml::utils::logger
        test::fakeit
)

A module-services/service-cellular/call/doc/uml/call_flow.puml => module-services/service-cellular/call/doc/uml/call_flow.puml +0 -0
A module-services/service-cellular/call/doc/uml/uml_printer.cpp => module-services/service-cellular/call/doc/uml/uml_printer.cpp +11 -0
@@ 0,0 1,11 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "call/CallEvents.hpp"
#include <CallMachine.hpp>
#include <sml-utils/PlantUML.hpp>

int main()
{
    dump<call::SM>();
}

A module-services/service-cellular/call/include/call/CallEvents.hpp => module-services/service-cellular/call/include/call/CallEvents.hpp +44 -0
@@ 0,0 1,44 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <PhoneNumber.hpp>
#include <PhoneModes/PhoneMode.hpp>
#include <service-cellular/CellularMessage.hpp>

namespace call::event
{
    struct Answer
    {};
    struct StartCall
    {
        CallType type;
        const utils::PhoneNumber::View number;
    };
    struct RING
    {};
    struct CLIP
    {
        const utils::PhoneNumber::View number;
    };
    struct AudioRequest
    {
        cellular::CallAudioEventRequest::EventType event;
    };

    struct ModeChange
    {
        sys::phone_modes::PhoneMode mode;
    };

    struct OngoingTimer
    {};

    struct Ended
    {};

    struct Reject
    {};

} // namespace call::event

M module-services/service-cellular/call/include/call/CellularCall.hpp => module-services/service-cellular/call/include/call/CellularCall.hpp +47 -109
@@ 3,11 3,12 @@

#pragma once

#include "call/CallAudio.hpp"
#include "call/CallGUI.hpp"
#include "call/CallDB.hpp"
#include "call/CallMulticast.hpp"
#include "PhoneModes/PhoneMode.hpp"
#include "api/CallAudio.hpp"
#include "api/CallGUI.hpp"
#include "api/CallDB.hpp"
#include "api/CallTimer.hpp"
#include "api/CallMulticast.hpp"
#include "api/ModemCallApi.hpp"
#include <service-cellular/CellularMessage.hpp>
#include <Interface/CalllogRecord.hpp>
#include <SystemManager/CpuSentinel.hpp>


@@ 23,120 24,57 @@
#include <iosfwd>
#include <string>
#include <sys/types.h>
#include "CallEvents.hpp"

class ServiceCellular;

namespace CellularCall
namespace call
{
    enum class Forced : bool
    struct StateMachine;

    struct CallData
    {
        False,
        True
        CalllogRecord record             = {};
        sys::phone_modes::PhoneMode mode = sys::phone_modes::PhoneMode::Connected;
    };

    class Call
    struct Dependencies
    {
        CalllogRecord call;
        bool isActiveCall      = false;
        bool wasRinging        = false;
        bool isNumberDisplayed = false;
        std::function<CalllogRecord(const CalllogRecord &rec)> startCallAction;
        std::function<bool(const CalllogRecord &rec)> endCallAction;
        utils::time::Timestamp startActiveTime;
        time_t callDurationTime = 0;

        std::shared_ptr<sys::CpuSentinel> cpuSentinel;

        void setType(const CallType type)
        {
            call.type = type;
        }

        void markUnread()
        {
            call.isRead = false;
        }

        void clear()
        {
            call              = CalllogRecord();
            isActiveCall      = false;
            wasRinging        = false;
            isNumberDisplayed = false;
            startActiveTime.set_time(0);
            callDurationTime = 0;
        }

        bool startCall(const utils::PhoneNumber::View &number, const CallType type);
        std::shared_ptr<call::api::Audio> audio;
        std::shared_ptr<call::api::Multicast> multicast;
        std::shared_ptr<call::api::GUI> gui;
        std::shared_ptr<call::api::DB> db;
        std::shared_ptr<call::api::Timer> timer;
        std::shared_ptr<call::api::Api> modem;
        std::shared_ptr<sys::CpuSentinel> sentinel;
    };

        ServiceCellular &owner;
        CallRingAudio audio;
        CallMulticast multicast;
        CallGUI gui;
        CallDB db;
    class Call
    {
      private:
        std::unique_ptr<StateMachine> machine;

      public:
        void setMode(sys::phone_modes::PhoneMode mode)
        {
            this->mode = mode;
        }

        mutable sys::phone_modes::PhoneMode mode;
        explicit Call(ServiceCellular &owner);

        void setStartCallAction(const std::function<CalllogRecord(const CalllogRecord &rec)> callAction)
        {
            startCallAction = callAction;
        }

        void ongoingCallShowUI();
        void setEndCallAction(const std::function<bool(const CalllogRecord &rec)> callAction)
        {
            endCallAction = callAction;
        }

        bool setActive();
        void setNumber(const utils::PhoneNumber::View &number);
        bool ringAudioOnClip();

        bool startOutgoing(const utils::PhoneNumber::View &number);
        bool handleRING();
        bool handleCLIP(const utils::PhoneNumber::View &number);
        bool endCall(Forced forced = Forced::False);

        void handleCallAudioEventRequest(cellular::CallAudioEventRequest::EventType event);

        bool isValid() const
        {
            return call.ID != DB_ID_NONE;
        }

        bool isActive() const
        {
            return isActiveCall;
        }

        [[nodiscard]] CallType getType() const noexcept
        {
            return call.type;
        }

        void setCpuSentinel(std::shared_ptr<sys::CpuSentinel> sentinel);

        void handleCallDurationTimer();

        struct Operations
        {
            bool areCallsFromFavouritesEnabled();
            bool isNumberInFavourites();

          private:
            friend Call;
            Call &call;
            explicit Operations(Call &owner) : call(owner)
            {}
        };
        Operations operations = Operations(*this);
        friend Operations;
        Call();
        Call(ServiceCellular *owner,
             CellularMux *cmux,
             sys::TimerHandle timer,
             std::shared_ptr<sys::CpuSentinel> sentinel);
        ~Call();
        Call &operator=(Call &&other) noexcept;

        bool handle(const call::event::Answer &);
        bool handle(const call::event::RING &);
        bool handle(const call::event::StartCall &call);
        bool handle(const call::event::CLIP &);
        bool handle(const call::event::AudioRequest &);
        bool handle(const call::event::ModeChange &);
        bool handle(const call::event::OngoingTimer &);
        bool handle(const call::event::Ended &);
        bool handle(const call::event::Reject &);

        /// return if call procesing is in any other state than `Idle`
        /// if so - we have ongoing call in handling
        bool active() const;
    };
} // namespace CellularCall

A module-services/service-cellular/call/tests/CMakeLists.txt => module-services/service-cellular/call/tests/CMakeLists.txt +19 -0
@@ 0,0 1,19 @@
add_catch2_executable(
        NAME
        call-machine
        SRCS
        test-CallMachine.cpp

        LIBS
        module-cellular
        service-cellular-call
        sml::sml
        sml::utils::logger
        test::fakeit
)

set_source_files_properties(
        test-CallMachine.cpp
        PROPERTIES COMPILE_FLAGS
        "-Wno-error=unused-but-set-variable"
)

A module-services/service-cellular/call/tests/test-CallMachine.cpp => module-services/service-cellular/call/tests/test-CallMachine.cpp +386 -0
@@ 0,0 1,386 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "call/CallEvents.hpp"
#include <catch2/catch.hpp>
#include <CallMachine.hpp>
#include <catch/fakeit.hpp>

namespace mocks
{
    class Sentinel : public sys::CpuSentinel
    {
      public:
        Sentinel() : sys::CpuSentinel{"test", nullptr, nullptr}
        {}
        void HoldMinimumFrequency(bsp::CpuFrequencyMHz frequencyToHold) override
        {}
        void ReleaseMinimumFrequency() override
        {}
    };

    auto audio()
    {
        fakeit::Mock<call::api::Audio> audio;
        fakeit::When(Method(audio, play)).AlwaysReturn();
        fakeit::When(Method(audio, stop)).AlwaysReturn();
        fakeit::When(Method(audio, muteCall)).AlwaysReturn();
        fakeit::When(Method(audio, unmuteCall)).AlwaysReturn();
        fakeit::When(Method(audio, setLaudspeakerOn)).AlwaysReturn();
        fakeit::When(Method(audio, setLaudspeakerOff)).AlwaysReturn();
        fakeit::When(Method(audio, routingStart)).AlwaysReturn();
        return audio;
    }

    auto gui()
    {
        fakeit::Mock<call::api::GUI> gui;
        fakeit::When(Method(gui, notifyRING)).AlwaysReturn();
        fakeit::When(Method(gui, notifyCLIP)).AlwaysReturn();
        fakeit::When(Method(gui, notifyCallStarted)).AlwaysReturn();
        fakeit::When(Method(gui, notifyCallEnded)).AlwaysReturn();
        fakeit::When(Method(gui, notifyCallActive)).AlwaysReturn();
        fakeit::When(Method(gui, notifyCallDurationUpdate)).AlwaysReturn();
        return gui;
    }

    auto multicast()
    {
        fakeit::Mock<call::api::Multicast> multicast;
        fakeit::When(Method(multicast, notifyIncommingCall)).AlwaysReturn();
        fakeit::When(Method(multicast, notifyIdentifiedCall)).AlwaysReturn();
        fakeit::When(Method(multicast, notifyCallActive)).AlwaysReturn();
        fakeit::When(Method(multicast, notifyCallAborted)).AlwaysReturn();
        return multicast;
    };

    auto db()
    {
        fakeit::Mock<call::api::DB> db;
        fakeit::When(Method(db, incrementNotAnsweredCallsNotification)).AlwaysReturn();
        fakeit::When(Method(db, startCall)).AlwaysReturn();
        fakeit::When(Method(db, endCall)).AlwaysReturn();
        fakeit::When(Method(db, isNumberInFavourites)).AlwaysReturn(true);
        return db;
    }

    auto timer()
    {
        fakeit::Mock<call::api::Timer> timer;
        fakeit::When(Method(timer, start)).AlwaysReturn();
        fakeit::When(Method(timer, stop)).AlwaysReturn();
        fakeit::When(Method(timer, duration)).AlwaysReturn(0);
        return timer;
    }

    auto api(bool dnd = false)
    {
        fakeit::Mock<call::api::Api> api;
        fakeit::When(Method(api, answerIncommingCall)).AlwaysReturn(true);
        fakeit::When(Method(api, hangupCall)).AlwaysReturn(true);
        fakeit::When(Method(api, rejectCall)).AlwaysReturn(true);
        fakeit::When(Method(api, areCallsFromFavouritesEnabled)).AlwaysReturn(true);
        fakeit::When(Method(api, getMode))
            .AlwaysReturn(dnd ? sys::phone_modes::PhoneMode::DoNotDisturb : sys::phone_modes::PhoneMode::Connected);
        return api;
    }

    template <typename T> auto mock_to_shared(T *val)
    {
        auto t = std::shared_ptr<T>(val, [](...) {});
        return t;
    }

    struct DIWrapper
    {
        decltype(mocks::audio()) audio         = mocks::audio();
        decltype(mocks::multicast()) multicast = mocks::multicast();
        decltype(mocks::gui()) gui             = mocks::gui();
        decltype(mocks::db()) db               = mocks::db();
        decltype(mocks::timer()) timer         = mocks::timer();
        decltype(mocks::api()) api;
        call::Dependencies di{};
        std::shared_ptr<mocks::Sentinel> sentinel = std::make_shared<mocks::Sentinel>();

        DIWrapper(bool dnd = false) : api(mocks::api(dnd))
        {
            auto audio_p     = mock_to_shared<call::api::Audio>(&audio.get());
            auto multicast_p = mock_to_shared<call::api::Multicast>(&multicast.get());
            auto gui_p       = mock_to_shared<call::api::GUI>(&gui.get());
            auto db_p        = mock_to_shared<call::api::DB>(&db.get());
            auto timer_p     = mock_to_shared<call::api::Timer>(&timer.get());
            auto api_p       = mock_to_shared<call::api::Api>(&api.get());
            di               = call::Dependencies{std::move(audio_p),
                                    std::move(multicast_p),
                                    std::move(gui_p),
                                    std::move(db_p),
                                    std::move(timer_p),
                                    std::move(api_p),
                                    sentinel};
        }

        auto get()
        {
            return di;
        }
    };
} // namespace mocks

// ----------------------------------
// INCOMMING CALL TESTS - mode normal
// ----------------------------------

TEST_CASE("base positive call flow, answered, end from caller")
{
    auto number  = utils::PhoneNumber("+48700800900");
    auto di      = mocks::DIWrapper();
    auto machine = std::make_unique<call::StateMachine>(di.get());

    // 0 rings
    fakeit::Verify(Method(di.audio, play)).Exactly(0);
    // 1st ring
    REQUIRE(machine->machine.process_event(call::event::RING{}));
    fakeit::Verify(Method(di.audio, play)).Exactly(1);
    fakeit::Verify(Method(di.gui, notifyRING)).Exactly(1);
    // 2nd ring - ignored
    REQUIRE(not machine->machine.process_event(call::event::RING{}));
    fakeit::Verify(Method(di.audio, play)).Exactly(1);
    fakeit::Verify(Method(di.gui, notifyRING)).Exactly(1);

    REQUIRE(machine->machine.process_event(call::event::CLIP{number.getView()}));
    REQUIRE(machine->call.record.phoneNumber == number.getView());
    fakeit::Verify(Method(di.gui, notifyCLIP)).Exactly(1);

    // answer the call from the pure
    REQUIRE(machine->machine.process_event(call::event::Answer{}));
    fakeit::Verify(Method(di.audio, routingStart)).Exactly(1);
    fakeit::Verify(Method(di.gui, notifyCallActive)).Exactly(1);

    REQUIRE(machine->machine.process_event(call::event::OngoingTimer{}));
    REQUIRE(machine->machine.process_event(call::event::Ended{}));
}

TEST_CASE("CLIP before ring, answered, end from caller")
{
    auto number  = utils::PhoneNumber("+48700800900");
    auto di      = mocks::DIWrapper();
    auto machine = std::make_unique<call::StateMachine>(di.get());

    REQUIRE(machine->machine.process_event(call::event::CLIP{number.getView()}));
    REQUIRE(machine->call.record.phoneNumber == number.getView());
    fakeit::Verify(Method(di.audio, play)).Exactly(1);
    fakeit::Verify(Method(di.gui, notifyCLIP)).Exactly(1);

    REQUIRE(machine->machine.process_event(call::event::Answer{}));
    fakeit::Verify(Method(di.audio, routingStart)).Exactly(1);
    fakeit::Verify(Method(di.gui, notifyCallActive)).Exactly(1);

    REQUIRE(machine->machine.process_event(call::event::OngoingTimer{}));

    CalllogRecord record_when_called;
    fakeit::When(Method(di.db, endCall)).AlwaysDo([&record_when_called](const CalllogRecord &rec) {
        record_when_called = rec;
    });

    REQUIRE(machine->machine.process_event(call::event::Ended{}));

    REQUIRE(record_when_called.type == CallType::CT_INCOMING);
    REQUIRE(record_when_called.phoneNumber == number.getView());
}

/// no clip is when: caller id is unknown
TEST_CASE("no CLIP at all, anonymus call answered, end from caller")
{
    auto di      = mocks::DIWrapper();
    auto machine = std::make_unique<call::StateMachine>(di.get());

    fakeit::Verify(Method(di.audio, play)).Exactly(0);

    REQUIRE(machine->machine.process_event(call::event::RING{}));
    fakeit::Verify(Method(di.audio, play)).Exactly(1);
    fakeit::Verify(Method(di.gui, notifyRING)).Exactly(1);

    REQUIRE(machine->machine.process_event(call::event::Answer{}));
    fakeit::Verify(Method(di.audio, routingStart)).Exactly(1);
    fakeit::Verify(Method(di.gui, notifyCallActive)).Exactly(1);

    REQUIRE(machine->machine.process_event(call::event::OngoingTimer{}));

    // we cant use fakeit matching - as it has const references, not copies of object
    // see: https://github.com/eranpeer/FakeIt/issues/31
    // therefore we store value to be checked and then check it

    CalllogRecord record_when_called;
    fakeit::When(Method(di.db, endCall)).AlwaysDo([&record_when_called](const CalllogRecord &rec) {
        record_when_called = rec;
    });

    REQUIRE(machine->machine.process_event(call::event::Ended{}));

    // no CLIP: there was no phone number set
    // - therefore we call db save with empty phone nr
    // - call type set is answered
    REQUIRE(record_when_called.type == CallType::CT_INCOMING);
    REQUIRE(record_when_called.phoneNumber == utils::PhoneNumber().getView());
}

TEST_CASE("reject on PURE after RING")
{
    auto di      = mocks::DIWrapper();
    auto machine = std::make_unique<call::StateMachine>(di.get());

    REQUIRE(machine->machine.process_event(call::event::RING{}));
    fakeit::Verify(Method(di.audio, play)).Exactly(1);

    CalllogRecord record_when_called;
    fakeit::When(Method(di.db, endCall)).AlwaysDo([&record_when_called](const CalllogRecord &rec) {
        record_when_called = rec;
    });

    REQUIRE(machine->machine.process_event(call::event::Reject{}));

    fakeit::Verify(Method(di.audio, stop)).Exactly(1);
    // UI was notified to stop "calling" display
    fakeit::Verify(Method(di.gui, notifyCallEnded)).Exactly(1);

    REQUIRE(record_when_called.type == CallType::CT_REJECTED);
}

TEST_CASE("reject on PURE after CLIP")
{
    auto number  = utils::PhoneNumber("+48700800900");
    auto di      = mocks::DIWrapper();
    auto machine = std::make_unique<call::StateMachine>(di.get());

    REQUIRE(machine->machine.process_event(call::event::RING{}));
    REQUIRE(machine->machine.process_event(call::event::CLIP{number.getView()}));
    fakeit::Verify(Method(di.audio, play)).Exactly(1);

    CalllogRecord record_when_called;
    fakeit::When(Method(di.db, endCall)).AlwaysDo([&record_when_called](const CalllogRecord &rec) {
        record_when_called = rec;
    });

    REQUIRE(machine->machine.process_event(call::event::Reject{}));

    fakeit::Verify(Method(di.audio, stop)).Exactly(1);
    REQUIRE(record_when_called.type == CallType::CT_REJECTED);
}

TEST_CASE("call missed on PURE after CLIP")
{
    auto number  = utils::PhoneNumber("+48700800900");
    auto di      = mocks::DIWrapper();
    auto machine = std::make_unique<call::StateMachine>(di.get());

    REQUIRE(machine->machine.process_event(call::event::RING{}));
    REQUIRE(machine->machine.process_event(call::event::CLIP{number.getView()}));
    CalllogRecord record_when_called;
    fakeit::When(Method(di.db, endCall)).AlwaysDo([&record_when_called](const CalllogRecord &rec) {
        record_when_called = rec;
    });
    REQUIRE(machine->machine.process_event(call::event::Ended{}));
    REQUIRE(record_when_called.type == CallType::CT_MISSED);
}

// ---------------------------------
// INCOMMING CALL TESTS - mode dnd
// ---------------------------------

TEST_CASE("call incoming - dnd - not favourite")
{
    auto number  = utils::PhoneNumber("+48700800900");
    auto di      = mocks::DIWrapper(true);
    auto machine = std::make_unique<call::StateMachine>(di.get());

    fakeit::When(Method(di.db, isNumberInFavourites)).AlwaysReturn(false);

    REQUIRE(not machine->machine.process_event(call::event::RING{}));
    REQUIRE(machine->machine.process_event(call::event::CLIP{number.getView()}));
    fakeit::Verify(Method(di.audio, play)).Exactly(0);
    fakeit::Verify(Method(di.api, rejectCall)).Exactly(1);
}

TEST_CASE("call incoming - dnd - in favourite")
{
    auto number  = utils::PhoneNumber("+48700800900");
    auto di      = mocks::DIWrapper(true);
    auto machine = std::make_unique<call::StateMachine>(di.get());

    fakeit::When(Method(di.db, isNumberInFavourites)).AlwaysReturn(true);

    REQUIRE(not machine->machine.process_event(call::event::RING{}));
    REQUIRE(machine->machine.process_event(call::event::CLIP{number.getView()}));
    fakeit::Verify(Method(di.audio, play)).Exactly(1);
    fakeit::Verify(Method(di.api, rejectCall)).Exactly(0);
    using namespace boost::sml;
    REQUIRE(machine->machine.is("HaveId"_s));
}

TEST_CASE("call incomming - proper date")
{
    auto di      = mocks::DIWrapper();
    auto machine = std::make_unique<call::StateMachine>(di.get());

    REQUIRE(machine->machine.process_event(call::event::RING{}));

    auto t     = std::time(nullptr);
    auto date  = *std::localtime(&t);
    auto saved = *std::localtime(&machine->call.record.date);

    REQUIRE(date.tm_year == saved.tm_year);
    REQUIRE(date.tm_mday == saved.tm_mday);
    REQUIRE(date.tm_hour == saved.tm_hour);
}

// ---------------------------------
// OUTGOING CALL TESTS - mode normal
// ---------------------------------

TEST_CASE("call outgoing - answered")
{
    auto number  = utils::PhoneNumber("+48700800900");
    auto di      = mocks::DIWrapper();
    auto machine = std::make_unique<call::StateMachine>(di.get());

    REQUIRE(machine->machine.process_event(call::event::StartCall{CallType::CT_OUTGOING, number.getView()}));
    REQUIRE(machine->machine.process_event(call::event::Answer{}));
    fakeit::Verify(Method(di.audio, play)).Exactly(0);
    REQUIRE(machine->machine.process_event(call::event::Ended{}));
    fakeit::Verify(Method(di.audio, stop)).Exactly(1);
}

TEST_CASE("call outgoing - ended, before answered")
{
    auto number  = utils::PhoneNumber("+48700800900");
    auto di      = mocks::DIWrapper();
    auto machine = std::make_unique<call::StateMachine>(di.get());

    REQUIRE(machine->machine.process_event(call::event::StartCall{CallType::CT_OUTGOING, number.getView()}));
    fakeit::Verify(Method(di.audio, play)).Exactly(0);
    REQUIRE(machine->machine.process_event(call::event::Ended{}));
    fakeit::Verify(Method(di.audio, stop)).Exactly(1);
}

// ---------------------------------
// TEST EXCEPTION ABORTION HANDLER
// ---------------------------------

TEST_CASE("test call exception abort")
{
    auto number  = utils::PhoneNumber("+48700800900");
    auto di      = mocks::DIWrapper();
    auto machine = std::make_unique<call::StateMachine>(di.get());

    REQUIRE(machine->machine.process_event(call::event::StartCall{CallType::CT_OUTGOING, number.getView()}));
    REQUIRE(machine->machine.process_event(call::event::Answer{}));

    fakeit::When(Method(di.audio, stop)).AlwaysDo([] {
        throw std::runtime_error("lol");
        return true;
    });

    REQUIRE(machine->machine.process_event(call::event::Ended{}));

    fakeit::Verify(Method(di.multicast, notifyCallAborted)).Exactly(1);
}

M module-services/service-cellular/connection-manager/ConnectionManager.cpp => module-services/service-cellular/connection-manager/ConnectionManager.cpp +1 -7
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <module-services/service-cellular/service-cellular/connection-manager/ConnectionManager.hpp>


@@ 10,10 10,8 @@ auto ConnectionManager::onPhoneModeChange(sys::phone_modes::PhoneMode mode) -> b
    cellular->holdMinimumCpuFrequency();

    if (mode == sys::phone_modes::PhoneMode::Offline) {
        forceDismissCallsFlag = true;
        return handleModeChangeToCommonOffline();
    }
    forceDismissCallsFlag = false;
    return handleModeChangeToConnected();
}



@@ 129,10 127,6 @@ auto ConnectionManager::handleModeChangeToConnected() -> bool
    return true;
}

auto ConnectionManager::forceDismissCalls() -> bool
{
    return forceDismissCallsFlag;
}
void ConnectionManager::retryOnFail()
{
    if (failRetries < maxFailRetries) {

M module-services/service-cellular/connection-manager/ConnectionManagerCellularCommands.cpp => module-services/service-cellular/connection-manager/ConnectionManagerCellularCommands.cpp +3 -16
@@ 64,23 64,10 @@ auto ConnectionManagerCellularCommands::clearNetworkIndicator() -> bool
    return true;
}

auto ConnectionManagerCellularCommands::hangUpOngoingCall() -> bool
void ConnectionManagerCellularCommands::hangUpOngoingCall()
{
    if (cellular.ongoingCall.isActive()) {
        auto channel = cellular.cmux->get(CellularMux::Channel::Commands);
        if (channel) {
            if (channel->cmd(at::factory(at::AT::ATH))) {
                cellular.callStateTimer.stop();
                if (!cellular.ongoingCall.endCall(CellularCall::Forced::True)) {
                    LOG_ERROR("Failed to end ongoing call");
                    return false;
                }
                auto msg = std::make_shared<CellularCallAbortedNotification>();
                cellular.bus.sendMulticast(msg, sys::BusChannel::ServiceCellularNotifications);
            }
        }
    }
    return true;
    CellularHangupCallMessage msg;
    cellular.handleCellularHangupCallMessage(&msg);
}

auto ConnectionManagerCellularCommands::isConnectionTimerActive() -> bool

M module-services/service-cellular/service-cellular/CellularMessage.hpp => module-services/service-cellular/service-cellular/CellularMessage.hpp +1 -10
@@ 613,17 613,8 @@ class CellularHangupCallMessage : public CellularMessage
class CellularDismissCallMessage : public CellularMessage
{
  public:
    CellularDismissCallMessage(bool addNotificationToDB)
        : CellularMessage(Type::DismissCall), addNotificationToDB{addNotificationToDB}
    CellularDismissCallMessage() : CellularMessage(Type::DismissCall)
    {}

    auto addNotificationRequired() const noexcept -> bool
    {
        return addNotificationToDB;
    }

  private:
    const bool addNotificationToDB;
};

class CellularListCallsMessage : public CellularMessage

M module-services/service-cellular/service-cellular/CellularServiceAPI.hpp => module-services/service-cellular/service-cellular/CellularServiceAPI.hpp +1 -1
@@ 28,7 28,7 @@ namespace CellularServiceAPI

    bool AnswerIncomingCall(sys::Service *serv);
    bool HangupCall(sys::Service *serv);
    bool DismissCall(sys::Service *serv, bool addNotificationToDB);
    bool DismissCall(sys::Service *serv);
    /*
     * @brief Its calls sercive-cellular for selected SIM IMSI number.
     * @param serv pointer to caller service.

M module-services/service-cellular/service-cellular/ServiceCellular.hpp => module-services/service-cellular/service-cellular/ServiceCellular.hpp +4 -3
@@ 110,7 110,6 @@ class ServiceCellular : public sys::Service
    sys::TimerHandle ussdTimer;
    sys::TimerHandle simTimer;
    sys::TimerHandle csqTimer;
    sys::TimerHandle callDurationTimer;

    // used to enter modem sleep mode
    sys::TimerHandle sleepTimer;


@@ 136,7 135,7 @@ class ServiceCellular : public sys::Service

    std::vector<std::string> messageParts;

    CellularCall::Call ongoingCall = CellularCall::Call(*this);
    std::unique_ptr<call::Call> ongoingCall;
    std::vector<CalllogRecord> tetheringCalllog;

    ussd::State ussdState = ussd::State::none;


@@ 256,6 255,7 @@ class ServiceCellular : public sys::Service
    friend class packet_data::PDPContext;
    friend class packet_data::PacketData;
    friend class ConnectionManagerCellularCommands;
    friend class cellular::Api;

    void volteChanged(const std::string &value);
    void apnListChanged(const std::string &value);


@@ 266,8 266,10 @@ class ServiceCellular : public sys::Service
    void handleCellularHangupCallMessage(CellularHangupCallMessage *msg);
    void handleCellularDismissCallMessage(sys::Message *msg);
    auto handleDBQueryResponseMessage(db::QueryResponse *msg) -> std::shared_ptr<sys::ResponseMessage>;
    /// when we start call from Pure -> to the calee, then we poll for the moment that the calee answered the call
    auto handleCellularListCallsMessage(CellularMessage *msg) -> std::shared_ptr<sys::ResponseMessage>;
    auto handleDBNotificationMessage(db::NotificationMessage *msg) -> std::shared_ptr<sys::ResponseMessage>;
    /// handle mudita phone -> world ringing (not AT RING!)
    auto handleCellularRingingMessage(CellularRingingMessage *msg) -> std::shared_ptr<sys::ResponseMessage>;
    auto handleCellularCallerIdMessage(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>;
    auto handleCellularGetIMSIMessage(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>;


@@ 288,7 290,6 @@ class ServiceCellular : public sys::Service
    auto handleStateRequestMessage(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>;

    auto handleCallActiveNotification(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>;
    auto handleCallAbortedNotification(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>;
    auto handlePowerUpProcedureCompleteNotification(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>;
    auto handlePowerDownDeregisteringNotification(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>;
    auto handlePowerDownDeregisteredNotification(sys::Message *msg) -> std::shared_ptr<sys::ResponseMessage>;

M module-services/service-cellular/service-cellular/connection-manager/ConnectionManager.hpp => module-services/service-cellular/service-cellular/connection-manager/ConnectionManager.hpp +1 -11
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 51,11 51,6 @@ class ConnectionManager
     */
    void onTimerTick();

    /// Should we always dismiss incoming calls?
    /// @return true or false depending on state of forceDismissCallsMode flag
    /// @see forceDismissCallsMode
    auto forceDismissCalls() -> bool;

  private:
    bool isFlightMode;
    std::chrono::minutes connectionInterval{0};


@@ 65,11 60,6 @@ class ConnectionManager
    bool onlinePeriod = false;
    int failRetries   = 0;

    /// Flag determining if we should always dismiss incoming calls - even when
    /// we are in offline mode (messages only) and we connect to network to poll
    /// for new messages
    bool forceDismissCallsFlag = false;

    /**
     * @brief Checks if flightMode and connection interval are set as Messages only mode
     * @return true when Messages only is configured, false when not

M module-services/service-cellular/service-cellular/connection-manager/ConnectionManagerCellularCommands.hpp => module-services/service-cellular/service-cellular/connection-manager/ConnectionManagerCellularCommands.hpp +2 -2
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 16,7 16,7 @@ class ConnectionManagerCellularCommands : public ConnectionManagerCellularComman
    auto connectToNetwork() -> bool final;
    auto isConnectedToNetwork() -> std::optional<bool> final;
    auto clearNetworkIndicator() -> bool final;
    auto hangUpOngoingCall() -> bool final;
    auto hangUpOngoingCall() -> void final;
    auto isConnectionTimerActive() -> bool final;
    void startConnectionTimer() final;
    void stopConnectionTimer() final;

M module-services/service-cellular/service-cellular/connection-manager/ConnectionManagerCellularCommandsInterface.hpp => module-services/service-cellular/service-cellular/connection-manager/ConnectionManagerCellularCommandsInterface.hpp +2 -3
@@ 1,4 1,4 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 34,9 34,8 @@ class ConnectionManagerCellularCommandsInterface
    virtual auto clearNetworkIndicator() -> bool = 0;
    /**
     * @brief Checks if there is ongoing call and terminates it
     * @return true on success, false on fail
     */
    virtual auto hangUpOngoingCall() -> bool = 0;
    virtual void hangUpOngoingCall() = 0;
    /**
     * @brief Checks if connection Timer is active
     * @return true when timer is active, false when not

M module-services/service-db/DBServiceAPI.cpp => module-services/service-db/DBServiceAPI.cpp +2 -8
@@ 244,10 244,7 @@ auto DBServiceAPI::CalllogRemove(sys::Service *serv, uint32_t id) -> bool
        LOG_ERROR("DB response error, return code: %s", c_str(ret.first));
        return false;
    }
    if ((ret.first == sys::ReturnCodes::Success) && (calllogResponse->retCode != 0)) {
        return true;
    }
    return false;
    return ((ret.first == sys::ReturnCodes::Success) && (calllogResponse->retCode != 0));
}

auto DBServiceAPI::CalllogUpdate(sys::Service *serv, const CalllogRecord &rec) -> bool


@@ 260,10 257,7 @@ auto DBServiceAPI::CalllogUpdate(sys::Service *serv, const CalllogRecord &rec) -
        LOG_ERROR("DB response error, return code: %s", c_str(ret.first));
        return false;
    }
    if ((ret.first == sys::ReturnCodes::Success) && (calllogResponse->retCode != 0)) {
        return true;
    }
    return false;
    return ((ret.first == sys::ReturnCodes::Success) && (calllogResponse->retCode != 0));
}

auto DBServiceAPI::DBBackup(sys::Service *serv, std::string backupPath) -> bool

M module-services/service-evtmgr/battery/BatteryController.cpp => module-services/service-evtmgr/battery/BatteryController.cpp +5 -4
@@ 113,10 113,11 @@ void sevm::battery::BatteryController::poll()
}
void sevm::battery::BatteryController::printCurrentState()
{
    LOG_INFO("Charger state:%s", magic_enum::enum_name(Store::Battery::get().state).data());
    LOG_INFO("Battery SOC:%d", Store::Battery::get().level);
    LOG_INFO("Battery voltage:%" PRIu32 "mV", charger->getBatteryVoltage());
    LOG_INFO("Battery state:%s", magic_enum::enum_name(Store::Battery::get().levelState).data());
    LOG_INFO("Charger state:%s Battery SOC %d voltage: %" PRIu32 "mV state: %s",
             magic_enum::enum_name(Store::Battery::get().state).data(),
             Store::Battery::get().level,
             charger->getBatteryVoltage(),
             magic_enum::enum_name(Store::Battery::get().levelState).data());
}
void sevm::battery::BatteryController::update()
{

M module-sys/SystemManager/include/SystemManager/CpuSentinel.hpp => module-sys/SystemManager/include/SystemManager/CpuSentinel.hpp +3 -2
@@ 26,8 26,8 @@ namespace sys
                             std::function<void(bsp::CpuFrequencyMHz)> callback = nullptr);

        [[nodiscard]] auto GetName() const noexcept -> std::string;
        void HoldMinimumFrequency(bsp::CpuFrequencyMHz frequencyToHold);
        void ReleaseMinimumFrequency();
        virtual void HoldMinimumFrequency(bsp::CpuFrequencyMHz frequencyToHold);
        virtual void ReleaseMinimumFrequency();

        [[nodiscard]] auto GetFrequency() const noexcept -> bsp::CpuFrequencyMHz;



@@ 36,6 36,7 @@ namespace sys
        void ReadRegistrationData(bsp::CpuFrequencyMHz frequencyHz);
        TaskHandle_t getTask();
        std::string getReason();
        virtual ~CpuSentinel() = default;

      protected:
        const std::string name;

M module-utils/log/Logger.cpp => module-utils/log/Logger.cpp +1 -1
@@ 26,8 26,8 @@ namespace Log
    {
        filtered = {
            {"ApplicationManager", logger_level::LOGINFO},
#if (!LOG_SENSITIVE_DATA_ENABLED)
            {"CellularMux", logger_level::LOGINFO},
#if (!LOG_SENSITIVE_DATA_ENABLED)
            {"ServiceCellular", logger_level::LOGINFO},
#endif
            {"ServiceAntenna", logger_level::LOGERROR},

M module-utils/unicode/utf8/CMakeLists.txt => module-utils/unicode/utf8/CMakeLists.txt +1 -1
@@ 10,7 10,7 @@ target_include_directories(utf8
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
)

target_link_libraries(utf8 PRIVATE log-api)
target_link_libraries(utf8 PRIVATE log-api log)

if (${ENABLE_TESTS})
    add_subdirectory(test)

M products/PurePhone/services/db/ServiceDB.cpp => products/PurePhone/services/db/ServiceDB.cpp +1 -0
@@ 172,6 172,7 @@ sys::MessagePointer ServiceDB::DataReceivedHandler(sys::DataMessage *msgl, sys::
        auto record           = std::make_unique<std::vector<CalllogRecord>>();
        msg->record.ID        = DB_ID_NONE;
        auto ret              = calllogRecordInterface->Add(msg->record);
        LOG_INFO("HERE! ADD! SUCCESS? %s", ret ? "SUCCESS" : "FAIL");
        if (ret) {
            // return the newly added record
            msg->record = calllogRecordInterface->GetByID(calllogRecordInterface->GetLastID());

M third-party/CMakeLists.txt => third-party/CMakeLists.txt +1 -0
@@ 4,6 4,7 @@
add_subdirectory(base64)
add_subdirectory(date)
add_subdirectory(reedgefs)
add_subdirectory(fakeit)
add_subdirectory(fatfs)
add_subdirectory(gsl)
add_subdirectory(hash-library)

A third-party/fakeit/CMakeLists.txt => third-party/fakeit/CMakeLists.txt +4 -0
@@ 0,0 1,4 @@
add_library(fakeit INTERFACE) 
add_library(test::fakeit ALIAS fakeit)

target_include_directories(fakeit INTERFACE FakeIt/single_header/) 

A third-party/fakeit/FakeIt => third-party/fakeit/FakeIt +1 -0
@@ 0,0 1,1 @@
Subproject commit 98979d2784d270514c3d4cde18d4370d1381d866

M third-party/sml-utils/CMakeLists.txt => third-party/sml-utils/CMakeLists.txt +1 -1
@@ 1,4 1,4 @@
add_library(sml-logger INTERFACE) 
add_library(sml::utils::logger ALIAS sml-logger)

target_include_directories(sml-logger INTERFACE include/) 
target_include_directories(sml-logger INTERFACE include/)

M third-party/sml-utils/include/sml-utils/Logger.hpp => third-party/sml-utils/include/sml-utils/Logger.hpp +4 -4
@@ 11,12 11,12 @@ struct Logger
    template <class SM, class TEvent> void log_process_event(const TEvent &)
    {
        LOG_DEBUG(
            "[%s][process_event] %s\n", boost::sml::aux::get_type_name<SM>(), boost::sml::aux::get_type_name<TEvent>());
            "[%s][process_event] %s", boost::sml::aux::get_type_name<SM>(), boost::sml::aux::get_type_name<TEvent>());
    }

    template <class SM, class TGuard, class TEvent> void log_guard(const TGuard &, const TEvent &, bool result)
    {
        LOG_DEBUG("[%s][guard] %s %s %s\n",
        LOG_DEBUG("[%s][guard] %s %s %s",
                  boost::sml::aux::get_type_name<SM>(),
                  boost::sml::aux::get_type_name<TGuard>(),
                  boost::sml::aux::get_type_name<TEvent>(),


@@ 25,7 25,7 @@ struct Logger

    template <class SM, class TAction, class TEvent> void log_action(const TAction &, const TEvent &)
    {
        LOG_DEBUG("[%s][action] %s %s\n",
        LOG_DEBUG("[%s][action] %s %s",
                  boost::sml::aux::get_type_name<SM>(),
                  boost::sml::aux::get_type_name<TAction>(),
                  boost::sml::aux::get_type_name<TEvent>());


@@ 34,6 34,6 @@ struct Logger
    template <class SM, class TSrcState, class TDstState>
    void log_state_change(const TSrcState &src, const TDstState &dst)
    {
        LOG_DEBUG("[%s][transition] %s -> %s\n", boost::sml::aux::get_type_name<SM>(), src.c_str(), dst.c_str());
        LOG_DEBUG("[%s][transition] %s -> %s", boost::sml::aux::get_type_name<SM>(), src.c_str(), dst.c_str());
    }
};

A third-party/sml-utils/include/sml-utils/PlantUML.hpp => third-party/sml-utils/include/sml-utils/PlantUML.hpp +377 -0
@@ 0,0 1,377 @@
// Adapted example from:
//
// Copyright (c) 2016-2020 Kris Jusiak (kris at jusiak dot net)
//
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)
//

#include <algorithm>
#include <boost/sml.hpp>
#include <iostream>
#include <sstream>
#include <string>
#include <typeinfo>
#include <utility>
#include <vector>

namespace sml = boost::sml;

inline void do_indent(unsigned int indent) { std::cout << std::string(indent, ' '); }

// use this to track initialization for orthogonal states
bool state_initialized = false;  // NOLINT(misc-definitions-in-headers)
// use this to track which submachines have already been dumped so they don't get dumped twice
std::vector<std::string> completed_submachines;  // NOLINT(misc-definitions-in-headers)

/** allows for checking if the type is sml::front::seq_
 * This type is used by sml when there are lists of actions.
 */
template <class... Ts>
struct is_seq_ : sml::aux::false_type {};  // NOLINT(readability-identifier-naming)
template <class... Ts>
struct is_seq_<sml::front::seq_<Ts...>> : sml::aux::true_type {};  // NOLINT(readability-identifier-naming)

/** allows for checking if the type is sml::front::not_
 * This type is used by sml inside of guards, when the guard value is negated with !
 *
 * The partial specialization matches if the type passed in is sml::front::not_, causing the struct to
 * inherit from sml::aux::true_type, which gives it a member called "value" that is set to true.
 * If the type passed doesn't match sml::front::not_, it'll match the generic is_not_ which inherits
 * from sml::aux::false_type, giving it a member called "value" that is set to false.
 */
template <class... Ts>
struct is_not_ : sml::aux::false_type {};  // NOLINT(readability-identifier-naming)
template <class... Ts>
struct is_not_<sml::front::not_<Ts...>> : sml::aux::true_type {};  // NOLINT(readability-identifier-naming)

/** provides access to the template parameter type of an sml::front::not_<T>
 */
template <class T>
struct strip_not_ {
  using type = T;
};  // NOLINT(readability-identifier-naming)
template <class T>
struct strip_not_<sml::front::not_<T>> {
  using type = T;
};  // NOLINT(readability-identifier-naming)

/** allows for checking if the type is sml::front::and_
 * This type is used by sml inside of guards when two guard functions are combined with &&
 */
template <class... Ts>
struct is_and_ : sml::aux::false_type {};  // NOLINT(readability-identifier-naming)
template <class... Ts>
struct is_and_<sml::front::and_<Ts...>> : sml::aux::true_type {};  // NOLINT(readability-identifier-naming)

/** allows for checking if the type is sml::front::or_
 * This type is used by sml inside of guards when two guard functions are combined with ||
 */
template <class... Ts>
struct is_or_ : sml::aux::false_type {};  // NOLINT(readability-identifier-naming)
template <class... Ts>
struct is_or_<sml::front::or_<Ts...>> : sml::aux::true_type {};  // NOLINT(readability-identifier-naming)

/** uses std::tuple_element and std::tuple to access the Nth type in a parameter pack
 */
template <int N, class... Ts>
using NthTypeOf = typename std::tuple_element<N, std::tuple<Ts...>>::type;

/** gets the size of a parameter pack
 * this isn't really necessary, sizeof...(Ts) can be used directly instead
 */
template <class... Ts>
struct count {                                     // NOLINT(readability-identifier-naming)
  static const std::size_t value = sizeof...(Ts);  // NOLINT(readability-identifier-naming)
};

/** allows for checking if the type is sml::aux::zero_wrapper
 * sml puts this around types inside of guards and event sequences
 */
template <class T>
struct is_zero_wrapper : sml::aux::false_type {};  // NOLINT(readability-identifier-naming)
template <class T>
struct is_zero_wrapper<sml::aux::zero_wrapper<T>> : sml::aux::true_type {};  // NOLINT(readability-identifier-naming)

/** if T is a zero wrapper, ::type will be the inner type. if not, it will be T.
 */
template <class T>
struct strip_zero_wrapper {
  using type = T;
};  // NOLINT(readability-identifier-naming)
template <class T>
struct strip_zero_wrapper<sml::aux::zero_wrapper<T>> {
  using type = T;
};  // NOLINT(readability-identifier-naming)

/** accesses the type of a state-machine, sml::back::sm
 */
template <class T>
struct submachine_type {
  using type = T;
};  // NOLINT(readability-identifier-naming)
template <class T>
struct submachine_type<sml::back::sm<T>> {
  using type = typename T::sm;
};  // NOLINT(readability-identifier-naming)

/** print the types inside a sml::front::seq_
 * These types came from a list of actions.
 */
template <class... Ts>
struct print_seq_types {  // NOLINT(readability-identifier-naming)
  template <int I>
  static void func() {
    constexpr auto param_pack_empty = (sizeof...(Ts) == I);
    if constexpr (!param_pack_empty) {  // NOLINT(readability-braces-around-statements,bugprone-suspicious-semicolon)
      using current_type = NthTypeOf<I, Ts...>;
      if constexpr (is_seq_<typename current_type::type>::value) {  // NOLINT(readability-braces-around-statements)
        // handle nested seq_ types, these happen when there are 3 or more actions
        print_seq_types<typename current_type::type>::template func<0>();
      } else {  // NOLINT(readability-misleading-indentation)
        // print this param directly
        std::cout << sml::aux::string<typename strip_zero_wrapper<current_type>::type>{}.c_str();
      }
      if constexpr (I + 1 < sizeof...(Ts)) {  // NOLINT(readability-braces-around-statements,bugprone-suspicious-semicolon)
        std::cout << ",\\n ";
      }
      print_seq_types<Ts...>::template func<I + 1>();
    }
  }
};
template <class... Ts>
struct print_seq_types<sml::front::seq_<Ts...>> {  // NOLINT(readability-identifier-naming)
  template <int I>
  static void func() {
    print_seq_types<Ts...>::template func<0>();
  }
};

/** print the types inside a guard
 * These can be a functor, an sml::front::not_, an sml::front::and_, or an sml::front::or_ which makes
 * this one more complicated. They also involve the zero_wrapper.
 * The various partial specializations handle all of the possible types.
 */
template <class... Ts>
struct print_guard {  // NOLINT(readability-identifier-naming)
  template <int I>
  static void func(const std::string& sep = "") {
    constexpr auto param_pack_empty = (sizeof...(Ts) == I);
    if constexpr (!param_pack_empty) {  // NOLINT(readability-braces-around-statements,bugprone-suspicious-semicolon)
      using current_type = NthTypeOf<I, Ts...>;
      if constexpr (is_zero_wrapper<
                        current_type>::value) {  // NOLINT(readability-braces-around-statements,bugprone-suspicious-semicolon)
        // unwrap the zero_wrapper and put it back into the recursion, it could be anything
        print_guard<typename strip_zero_wrapper<current_type>::type>::template func<0>();
      } else {  // NOLINT(readability-misleading-indentation)
        // it's just a functor, print it
        std::cout << sml::aux::string<current_type>{}.c_str();
      }

      // if we're not at the end, print the separator
      if constexpr (I + 1 < sizeof...(Ts)) {  // NOLINT(readability-braces-around-statements,bugprone-suspicious-semicolon)
        if (!sep.empty()) {
          std::cout << sep;
        }
      }

      // keep the recursion going, call for the next type in the parameter pack
      print_guard<Ts...>::template func<I + 1>(sep);
    }
  }
};
template <class T>
struct print_guard<sml::front::not_<T>> {  // NOLINT(readability-identifier-naming)
  template <int I>
  static void func(const std::string& /*sep*/ = "") {
    std::cout << "!" << sml::aux::string<typename strip_zero_wrapper<T>::type>{}.c_str();
  }
};
template <class... Ts>
struct print_guard<sml::front::and_<Ts...>> {  // NOLINT(readability-identifier-naming)
  template <int I>
  static void func(const std::string& /*sep*/ = "") {
    constexpr auto param_pack_empty = (sizeof...(Ts) == I);
    if constexpr (!param_pack_empty) {
      print_guard<Ts...>::template func<I>(" &&\\n ");
    }
  }
};
template <class... Ts>
struct print_guard<sml::front::or_<Ts...>> {  // NOLINT(readability-identifier-naming)
  template <int I>
  static void func(const std::string& /*sep*/ = "") {
    constexpr auto param_pack_empty = (sizeof...(Ts) == I);
    if constexpr (!param_pack_empty) {
      print_guard<Ts...>::template func<I>(" ||\\n ");
    }
  }
};

// forward declaration of dump_transitions
template <typename...>
struct dump_transitions;

template <int N, class T>
void dump_transition() noexcept {
  constexpr auto src_is_sub_sm =
      !sml::aux::is_same<sml::aux::type_list<>, sml::back::get_sub_sms<typename T::src_state>>::value;
  constexpr auto dst_is_sub_sm =
      !sml::aux::is_same<sml::aux::type_list<>, sml::back::get_sub_sms<typename T::dst_state>>::value;

  std::string src_state, dst_state;
  // NOLINTNEXTLINE(readability-braces-around-statements)
  if constexpr (src_is_sub_sm) {
    src_state = std::string{sml::aux::string<typename submachine_type<typename T::src_state>::type>{}.c_str()};
  } else {  // NOLINT(readability-misleading-indentation)
    src_state = std::string{sml::aux::string<typename T::src_state>{}.c_str()};
  }

  // NOLINTNEXTLINE(readability-braces-around-statements)
  if constexpr (dst_is_sub_sm) {
    dst_state = std::string{sml::aux::string<typename submachine_type<typename T::dst_state>::type>{}.c_str()};
  } else {  // NOLINT(readability-misleading-indentation)
    dst_state = std::string{sml::aux::string<typename T::dst_state>{}.c_str()};
  }

  const auto dst_internal = sml::aux::is_same<typename T::dst_state, sml::front::internal>::value;

  const auto has_event = !sml::aux::is_same<typename T::event, sml::anonymous>::value;
  const auto has_guard = !sml::aux::is_same<typename T::guard, sml::front::always>::value;
  const auto has_action = !sml::aux::is_same<typename T::action, sml::front::none>::value;

  if (has_event && has_action && sml::aux::is_same<typename T::action::type, sml::front::actions::defer>::value) {
    do_indent(N);
    std::cout << src_state << " : " << boost::sml::aux::get_type_name<typename T::event>() << " / defer" << std::endl;
    return;
  }

  if (dst_state == "terminate") {
    dst_state = "[*]";
  }

  if (T::initial) {
    if (state_initialized) {  // create an orthogonal section
      do_indent(N);
      std::cout << "--" << std::endl;
    }

    state_initialized = true;
    do_indent(N);
    std::cout << "[*] --> " << src_state << std::endl;
  }

  // NOLINTNEXTLINE(readability-braces-around-statements, bugprone-suspicious-semicolon)
  if constexpr (src_is_sub_sm) {
    auto already_in =
        std::find(completed_submachines.begin(), completed_submachines.end(), src_state) != completed_submachines.end();
    if (!already_in) {
      completed_submachines.push_back(src_state);
      constexpr int indent = N + 2;
      do_indent(N);
      std::cout << "state " << src_state << " {" << std::endl;
      bool prev_state = state_initialized;
      state_initialized = false;
      dump_transitions<typename T::src_state::transitions>::template func<indent>();
      do_indent(N);
      std::cout << "}" << std::endl;
      state_initialized = prev_state;
    }
  }

  do_indent(N);
  std::cout << src_state;
  if (!dst_internal) {
    std::cout << " --> " << dst_state;
  }

  if (has_event || has_guard || has_action) {
    std::cout << " :";
  }

  if (has_event) {
    std::cout << " " << std::string{sml::aux::string<typename T::event>{}.c_str()};
  }

  if (has_guard) {
    std::cout << "\\n [";
    print_guard<typename T::guard::type>::template func<0>();
    std::cout << "]";
  }

  if (has_action) {
    std::cout << " /\\n ";

    if constexpr (is_seq_<typename T::action::type>::value) {  // NOLINT(readability-braces-around-statements)
      std::cout << "(";
      print_seq_types<typename T::action::type>::template func<0>();
      std::cout << ")";
    } else {  // NOLINT(readability-misleading-indentation)
      std::cout << sml::aux::string<typename T::action::type>{}.c_str();
    }
  }

  std::cout << std::endl;

  // NOLINTNEXTLINE(readability-braces-around-statements, bugprone-suspicious-semicolon)
  if constexpr (dst_is_sub_sm) {
    auto already_in =
        std::find(completed_submachines.begin(), completed_submachines.end(), dst_state) != completed_submachines.end();
    if (!already_in) {
      completed_submachines.push_back(dst_state);
      constexpr int indent = N + 2;
      do_indent(N);
      std::cout << "state " << dst_state << " {" << std::endl;
      bool prev_state = state_initialized;
      state_initialized = false;
      dump_transitions<typename T::dst_state::transitions>::template func<indent>();
      do_indent(N);
      std::cout << "}" << std::endl;
      state_initialized = prev_state;
    }
  }
}

// this template allows iterating through the types in the parameter pack Ts...
// I is the counter
// INDENT is the current indentation level (for the state machine or sub-state machine)
template <int INDENT, int I, class... Ts>
void apply_dump_transition() {
  // iteration is finished when I == the size of the parameter pack
  constexpr auto param_pack_empty = (sizeof...(Ts) == I);
  if constexpr (!param_pack_empty) {  // NOLINT(readability-braces-around-statements,bugprone-suspicious-semicolon)
    // run the dump_transition function to print this sml::front::transition type
    dump_transition<INDENT, NthTypeOf<I, Ts...>>();
    // iteration isn't finished, keep going
    apply_dump_transition<INDENT, I + 1, Ts...>();
  }
}

// SFINAE type
template <typename...>
struct dump_transitions {  // NOLINT(readability-identifier-naming)
  template <int INDENT>
  static void func() {}
};

// Partial specialization for sml::aux::type_list<Ts...>. This grants access to the
// types inside the type list, which are sml::front::transition types, so they can
// be passed to apply_dump_transition.
template <typename... Ts>
struct dump_transitions<typename sml::aux::type_list<Ts...>> {  // NOLINT(readability-identifier-naming)
  template <int INDENT>
  static void func() {
    apply_dump_transition<INDENT, 0, Ts...>();
  }
};

// API to use to dump states:
// example:
// int main() { dump<plant_uml>(); }
template <class T>
void dump() noexcept {
  std::cout << "@startuml" << std::endl << std::endl;
  dump_transitions<typename sml::sm<T>::transitions>::template func<0>();
  std::cout << std::endl << "@enduml" << std::endl;
}