~aleteoryx/muditaos

4f3366ee808e63a8d4e439e22a43d30c73be12f1 — Piotr Tański 5 years ago 4490d0d
[EGD-4157] Actions queue added

All actions shall be queued in order to process them later.
M enabled_unittests => enabled_unittests +12 -0
@@ 15,6 15,18 @@

declare -A TESTS_LIST

TESTS_LIST["catch2-actions-registry-tests"]="
    Given actions registry when initialise it incorrectly then verify;
    Given empty actions registry when verify its state then no pending actions;
    Given actions registry when enqueue then check pending action;
    Given actions registry when enqueue then check if action was processed;
    Given actions registry when finished queued action then no pending actions;
    Given actions registry when enqueue multiple actions sequentially then count handled actions;
    Given actions registry when enqueue multiple actions at once then count handled actions;
    Given actions registry when enqueue multiple actions at once then wait for them to expire.;
    Given actions registry when enqueue multiple actions at once then process the specific ones only.;
"
#---------
TESTS_LIST["catch2-audio-test"]="
    Test audio tags;
    Audio settings string creation;

M module-apps/Application.cpp => module-apps/Application.cpp +11 -2
@@ 44,6 44,15 @@ namespace gui

namespace app
{
    auto actionHandled() -> ActionResult
    {
        return std::make_shared<ActionHandledResponse>();
    }

    auto actionNotHandled() -> ActionResult
    {
        return std::make_shared<ActionHandledResponse>(sys::ReturnCodes::Failure);
    }

    const char *Application::stateStr(Application::State st)
    {


@@ 321,12 330,12 @@ namespace app
        try {
            const auto &actionHandler = receivers.at(action);
            auto &data                = msg->getData();
            actionHandler(std::move(data));
            return actionHandler(std::move(data));
        }
        catch (const std::out_of_range &) {
            LOG_ERROR("Application %s is not able to handle action #%d", GetName().c_str(), action);
        }
        return msgNotHandled();
        return actionNotHandled();
    }

    sys::MessagePointer Application::handleApplicationSwitch(sys::Message *msgl)

M module-apps/Application.hpp => module-apps/Application.hpp +6 -1
@@ 73,6 73,11 @@ namespace app
        return std::make_shared<sys::ResponseMessage>(sys::ReturnCodes::Unresolved);
    }

    class ActionHandledResponse; // Forward declaration
    using ActionResult = std::shared_ptr<ActionHandledResponse>;
    auto actionHandled() -> ActionResult;
    auto actionNotHandled() -> ActionResult;

    using ApplicationName = std::string;

    enum class StartupStatus


@@ 156,7 161,7 @@ namespace app
        /// c_str() function for Application::State
        static const char *stateStr(State st);

        using OnActionReceived = std::function<sys::MessagePointer(manager::actions::ActionParamsPtr &&)>;
        using OnActionReceived = std::function<ActionResult(manager::actions::ActionParamsPtr &&)>;

      private:
        std::string default_window;

M module-apps/application-calendar/ApplicationCalendar.cpp => module-apps/application-calendar/ApplicationCalendar.cpp +1 -1
@@ 52,7 52,7 @@ namespace app
        bus.channels.push_back(sys::BusChannel::ServiceDBNotifications);
        addActionReceiver(manager::actions::ShowReminder, [this](auto &&data) {
            switchWindow(style::window::calendar::name::event_reminder_window, std::move(data));
            return msgHandled();
            return actionHandled();
        });
    }


M module-apps/application-call/ApplicationCall.cpp => module-apps/application-call/ApplicationCall.cpp +5 -5
@@ 40,15 40,15 @@ namespace app
                                         Indicator::NetworkAccessTechnology});
        addActionReceiver(manager::actions::Call, [this](auto &&data) {
            switchWindow(window::name_call, std::forward<decltype(data)>(data));
            return msgHandled();
            return actionHandled();
        });
        addActionReceiver(manager::actions::Dial, [this](auto &&data) {
            switchWindow(window::name_enterNumber, std::forward<decltype(data)>(data));
            return msgHandled();
            return actionHandled();
        });
        addActionReceiver(manager::actions::EmergencyDial, [this](auto &&data) {
            switchWindow(app::window::name_emergencyCall, std::forward<decltype(data)>(data));
            return msgHandled();
            return actionHandled();
        });
        addActionReceiver(manager::actions::NotAnEmergencyNotification, [this](auto &&data) {
            auto buttonAction = [=]() -> bool {


@@ 59,7 59,7 @@ namespace app
            auto textNoEmergency           = utils::localize.get("app_call_wrong_emergency");
            utils::findAndReplaceAll(textNoEmergency, "$NUMBER", data->getDescription());
            showNotification(buttonAction, iconNoEmergency, textNoEmergency);
            return msgHandled();
            return actionHandled();
        });
        addActionReceiver(manager::actions::NoSimNotification, [this](auto &&data) {
            auto buttonAction = [=]() -> bool {


@@ 69,7 69,7 @@ namespace app
            constexpr auto iconNoSim = "info_big_circle_W_G";
            const auto textNoSim     = utils::localize.get("app_call_no_sim");
            showNotification(buttonAction, iconNoSim, textNoSim);
            return msgHandled();
            return actionHandled();
        });
    }


M module-apps/application-calllog/ApplicationCallLog.cpp => module-apps/application-calllog/ApplicationCallLog.cpp +1 -1
@@ 28,7 28,7 @@ namespace app
    {
        addActionReceiver(manager::actions::ShowCallLog, [this](auto &&data) {
            switchWindow(gui::name::window::main_window, std::move(data));
            return msgHandled();
            return actionHandled();
        });
    }


M module-apps/application-desktop/ApplicationDesktop.cpp => module-apps/application-desktop/ApplicationDesktop.cpp +11 -11
@@ 48,57 48,57 @@ namespace app

        addActionReceiver(app::manager::actions::RequestPin, [this](auto &&data) {
            lockHandler.handlePasscodeRequest(gui::PinLock::LockType::SimPin, std::move(data));
            return msgHandled();
            return actionHandled();
        });

        addActionReceiver(app::manager::actions::RequestPuk, [this](auto &&data) {
            lockHandler.handlePasscodeRequest(gui::PinLock::LockType::SimPuk, std::move(data));
            return msgHandled();
            return actionHandled();
        });

        addActionReceiver(app::manager::actions::RequestPinChange, [this](auto &&data) {
            lockHandler.handlePinChangeRequest(std::move(data));
            return msgHandled();
            return actionHandled();
        });

        addActionReceiver(app::manager::actions::BlockSim, [this](auto &&data) {
            lockHandler.handleSimBlocked(std::move(data));
            return msgHandled();
            return actionHandled();
        });

        addActionReceiver(app::manager::actions::UnlockSim, [this](auto &&data) {
            lockHandler.handleUnlockSim(std::move(data));
            return msgHandled();
            return actionHandled();
        });

        addActionReceiver(app::manager::actions::DisplayCMEError, [this](auto &&data) {
            lockHandler.handleCMEError(std::move(data));
            return msgHandled();
            return actionHandled();
        });

        addActionReceiver(app::manager::actions::ShowMMIResponse, [this](auto &&data) {
            switchWindow(app::window::name::desktop_mmi_pull, std::move(data));
            return msgHandled();
            return actionHandled();
        });

        addActionReceiver(app::manager::actions::ShowMMIPush, [this](auto &&data) {
            switchWindow(app::window::name::desktop_mmi_push, std::move(data));
            return msgHandled();
            return actionHandled();
        });

        addActionReceiver(app::manager::actions::ShowMMIResult, [this](auto &&data) {
            switchWindow(app::window::name::desktop_mmi_internal, std::move(data));
            return msgHandled();
            return actionHandled();
        });

        addActionReceiver(app::manager::actions::DisplayLowBatteryNotification, [this](auto &&data) {
            handleLowBatteryNotification(std::move(data));
            return msgHandled();
            return actionHandled();
        });

        addActionReceiver(app::manager::actions::SystemBrownout, [this](auto &&data) {
            switchWindow(app::window::name::dead_battery, std::move(data));
            return msgHandled();
            return actionHandled();
        });
    }


M module-apps/application-messages/ApplicationMessages.cpp => module-apps/application-messages/ApplicationMessages.cpp +3 -3
@@ 45,11 45,11 @@ namespace app
        bus.channels.push_back(sys::BusChannel::ServiceDBNotifications);
        addActionReceiver(manager::actions::CreateSms, [this](auto &&data) {
            switchWindow(gui::name::window::new_sms, std::move(data));
            return msgHandled();
            return actionHandled();
        });
        addActionReceiver(manager::actions::ShowSmsTemplates, [this](auto &&data) {
            switchWindow(gui::name::window::sms_templates, std::move(data));
            return msgHandled();
            return actionHandled();
        });
        addActionReceiver(manager::actions::SmsRejectNoSim, [this](auto &&data) {
            auto action = [this]() {


@@ 57,7 57,7 @@ namespace app
                return true;
            };
            showNotification(action);
            return msgHandled();
            return actionHandled();
        });
    }


M module-apps/application-phonebook/ApplicationPhonebook.cpp => module-apps/application-phonebook/ApplicationPhonebook.cpp +5 -5
@@ 27,23 27,23 @@ namespace app
        bus.channels.push_back(sys::BusChannel::ServiceDBNotifications);
        addActionReceiver(manager::actions::ShowContacts, [this](auto &&data) {
            switchWindow(gui::name::window::main_window, std::move(data));
            return msgHandled();
            return actionHandled();
        });
        addActionReceiver(manager::actions::AddContact, [this](auto &&data) {
            switchWindow(gui::window::name::new_contact, std::move(data));
            return msgHandled();
            return actionHandled();
        });
        addActionReceiver(manager::actions::EditContact, [this](auto &&data) {
            switchWindow(gui::window::name::new_contact, std::move(data));
            return msgHandled();
            return actionHandled();
        });
        addActionReceiver(manager::actions::ShowContactDetails, [this](auto &&data) {
            switchWindow(gui::window::name::contact, std::move(data));
            return msgHandled();
            return actionHandled();
        });
        addActionReceiver(manager::actions::ShowEmergencyContacts, [this](auto &&data) {
            switchWindow(gui::window::name::ice_contacts, std::move(data));
            return msgHandled();
            return actionHandled();
        });
    }


M module-apps/application-settings/ApplicationSettings.cpp => module-apps/application-settings/ApplicationSettings.cpp +1 -1
@@ 46,7 46,7 @@ namespace app
        bus.channels.push_back(sys::BusChannel::AntennaNotifications);
        addActionReceiver(manager::actions::SelectSimCard, [this](auto &&data) {
            switchWindow(app::sim_select);
            return msgHandled();
            return actionHandled();
        });
    }


M module-apps/application-special-input/ApplicationSpecialInput.cpp => module-apps/application-special-input/ApplicationSpecialInput.cpp +1 -1
@@ 14,7 14,7 @@ ApplicationSpecialInput::ApplicationSpecialInput(std::string name,
{
    addActionReceiver(manager::actions::ShowSpecialInput, [this](auto &&data) {
        switchWindow(app::char_select, std::move(data));
        return msgHandled();
        return actionHandled();
    });

    windowsFactory.attach(app::char_select, [](Application *app, const std::string &name) {

M module-apps/messages/AppMessage.hpp => module-apps/messages/AppMessage.hpp +8 -0
@@ 35,6 35,14 @@ namespace app
        manager::actions::ActionParamsPtr data;
    };

    class ActionHandledResponse : public sys::ResponseMessage
    {
      public:
        explicit ActionHandledResponse(sys::ReturnCodes retCode = sys::ReturnCodes::Success)
            : sys::ResponseMessage{retCode, MessageType::AppAction}
        {}
    };

    // this message is used to notify application about switching event. Application will gain or lose focus upon
    // receiving this message. Application gains focus when it was in init or active background state. Application loose
    // focus when it was in active foreground state. if no window is specified it is assumed that MainWindow is the

M module-services/service-appmgr/CMakeLists.txt => module-services/service-appmgr/CMakeLists.txt +4 -0
@@ 9,6 9,7 @@ set(SOURCES
    model/ApplicationManager.cpp
    model/ApplicationHandle.cpp
    model/ApplicationsRegistry.cpp
    model/ActionsRegistry.cpp
    messages/ActionRequest.cpp
    messages/ApplicationCloseRequest.cpp
    messages/ApplicationInitialised.cpp


@@ 41,3 42,6 @@ target_include_directories(${PROJECT_NAME}
        ${CMAKE_CURRENT_LIST_DIR}
)

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

A module-services/service-appmgr/model/ActionsRegistry.cpp => module-services/service-appmgr/model/ActionsRegistry.cpp +122 -0
@@ 0,0 1,122 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <service-appmgr/model/ActionsRegistry.hpp>

#include <module-utils/log/log.hpp>
#include <module-utils/magic_enum/include/magic_enum.hpp>

#include <cassert>
#include <stdexcept>

namespace app::manager
{
    namespace
    {
        constexpr auto ExpirationTimeout = std::chrono::seconds{30};
    } // namespace

    ActionEntry::ActionEntry(actions::ActionId action, actions::ActionParamsPtr &&params, Timestamp creationTime)
        : actionId{action}, params{std::move(params)}, createdAt{creationTime}
    {}

    void ActionEntry::setTargetApplication(std::string targetApplication) noexcept
    {
        target = std::move(targetApplication);
    }

    auto ActionEntry::isExpired() const noexcept -> bool
    {
        const auto diff = Clock::now() - createdAt;
        return diff > ExpirationTimeout;
    }

    bool operator==(const ActionEntry &lhs, const ActionEntry &rhs) noexcept
    {
        return lhs.actionId == rhs.actionId && lhs.createdAt == rhs.createdAt && lhs.params == rhs.params;
    }

    bool operator!=(const ActionEntry &lhs, const ActionEntry &rhs) noexcept
    {
        return !operator==(lhs, rhs);
    }

    ActionsRegistry::ActionsRegistry(OnNextActionReadyCallback &&callback) : nextActionReady{std::move(callback)}
    {
        if (!nextActionReady) {
            throw std::invalid_argument{"No OnNextActionReadyCallback passed."};
        }
    }

    void ActionsRegistry::enqueue(ActionEntry &&action)
    {
        LOG_INFO("Enqueue action %s", magic_enum::enum_name(action.actionId).data());

        addAction(std::move(action));
        if (!isCurrentlyProcessing()) {
            notifyAboutNextAction();
        }
    }

    void ActionsRegistry::addAction(ActionEntry &&action)
    {
        actions.push_back(std::move(action));
    }

    auto ActionsRegistry::isCurrentlyProcessing() const noexcept -> bool
    {
        return hasPendingAction();
    }

    void ActionsRegistry::notifyAboutNextAction()
    {
        for (auto &action : actions) {
            if (const auto handled = nextActionReady(action); handled) {
                LOG_INFO(
                    "Pending action %s to %s", magic_enum::enum_name(action.actionId).data(), action.target.c_str());
                actionInProgress = &action;
                return;
            }
        }
    }

    void ActionsRegistry::finished()
    {
        assert(isCurrentlyProcessing());

        LOG_INFO("Finished action %s on %s",
                 magic_enum::enum_name(actionInProgress->actionId).data(),
                 actionInProgress->target.c_str());
        if (const auto it = std::find(actions.begin(), actions.end(), *actionInProgress); it != actions.end()) {
            actions.erase(it);
        }

        actionInProgress = nullptr;
        onFinished();
    }

    void ActionsRegistry::onFinished()
    {
        removeExpiredActions();
        if (!actions.empty()) {
            notifyAboutNextAction();
        }
    }

    void ActionsRegistry::removeExpiredActions()
    {
        actions.erase(
            std::remove_if(actions.begin(), actions.end(), [](const auto &action) { return action.isExpired(); }),
            actions.end());
    }

    auto ActionsRegistry::hasPendingAction() const noexcept -> bool
    {
        return actionInProgress != nullptr;
    }

    auto ActionsRegistry::getPendingAction() noexcept -> ActionEntry *
    {
        return actionInProgress;
    }
} // namespace app::manager

M module-services/service-appmgr/model/ApplicationManager.cpp => module-services/service-appmgr/model/ApplicationManager.cpp +57 -28
@@ 106,6 106,7 @@ namespace app::manager
                                           std::vector<std::unique_ptr<app::ApplicationLauncher>> &&launchers,
                                           const ApplicationName &_rootApplicationName)
        : Service{serviceName}, ApplicationManagerBase(std::move(launchers)), rootApplicationName{_rootApplicationName},
          actionsRegistry{[this](ActionEntry &action) { return handleAction(action); }},
          blockingTimer{std::make_unique<sys::Timer>(
              timerBlock, this, std::numeric_limits<sys::ms>::max(), sys::Timer::Type::SingleShot)},
          shutdownDelay{std::make_unique<sys::Timer>(timerShutdownDelay, this, shutdown_delay_ms)},


@@ 285,9 286,13 @@ namespace app::manager
        });
        connect(typeid(ActionRequest), [this](sys::Message *request) {
            auto actionMsg = static_cast<ActionRequest *>(request);
            handleAction(actionMsg);
            handleActionRequest(actionMsg);
            return std::make_shared<sys::ResponseMessage>();
        });
        connect(typeid(ActionHandledResponse), [this](sys::Message *response) {
            actionsRegistry.finished();
            return nullptr;
        });
        connect(typeid(GetCurrentDisplayLanguageRequest), [&](sys::Message *request) {
            return std::make_shared<GetCurrentDisplayLanguageResponse>(utils::localize.getDisplayLanguage());
        });


@@ 463,37 468,42 @@ namespace app::manager
        }
    }

    auto ApplicationManager::handleAction(ActionRequest *actionMsg) -> bool
    void ApplicationManager::handleActionRequest(ActionRequest *actionMsg)
    {
        switch (const auto action = actionMsg->getAction(); action) {
        ActionEntry entry{actionMsg->getAction(), std::move(actionMsg->getData())};
        actionsRegistry.enqueue(std::move(entry));
    }

    bool ApplicationManager::handleAction(ActionEntry &action)
    {
        switch (action.actionId) {
        case actions::Home:
            return handleHomeAction();
        case actions::Launch: {
            auto params = static_cast<ApplicationLaunchData *>(actionMsg->getData().get());
            return handleLaunchAction(params);
        }
            return handleHomeAction(action);
        case actions::Launch:
            return handleLaunchAction(action);
        case actions::CloseSystem:
            return handleCloseSystem();
        default: {
            auto &actionParams = actionMsg->getData();
            return handleCustomAction(action, std::move(actionParams));
        }
        default:
            return handleCustomAction(action);
        }
    }

    auto ApplicationManager::handleHomeAction() -> bool
    auto ApplicationManager::handleHomeAction(ActionEntry &action) -> bool
    {
        action.setTargetApplication(rootApplicationName);
        SwitchRequest switchRequest(ServiceName, rootApplicationName, gui::name::window::main_window, nullptr);
        return handleSwitchApplication(&switchRequest);
    }

    auto ApplicationManager::handleLaunchAction(ApplicationLaunchData *launchParams) -> bool
    auto ApplicationManager::handleLaunchAction(ActionEntry &action) -> bool
    {
        auto launchParams = static_cast<ApplicationLaunchData *>(action.params.get());
        auto targetApp = getApplication(launchParams->getTargetApplicationName());
        if (targetApp == nullptr || !targetApp->handles(actions::Launch)) {
            return false;
        }

        action.setTargetApplication(targetApp->name());
        SwitchRequest switchRequest(ServiceName, targetApp->name(), gui::name::window::main_window, nullptr);
        return handleSwitchApplication(&switchRequest);
    }


@@ 506,12 516,11 @@ namespace app::manager
        return true;
    }

    auto ApplicationManager::handleCustomAction(actions::ActionId action, actions::ActionParamsPtr &&actionParams)
        -> bool
    auto ApplicationManager::handleCustomAction(ActionEntry &action) -> bool
    {
        const auto actionHandlers = applications.findByAction(action);
        const auto actionHandlers = applications.findByAction(action.actionId);
        if (actionHandlers.empty()) {
            LOG_ERROR("No applications handling action #%d.", action);
            LOG_ERROR("No applications handling action #%d.", action.actionId);
            return false;
        }
        if (actionHandlers.size() > 1) {


@@ 520,16 529,16 @@ namespace app::manager
        }

        const auto targetApp = actionHandlers.front();
        action.setTargetApplication(targetApp->name());
        auto &actionParams = action.params;
        if (targetApp->state() != ApplicationHandle::State::ACTIVE_FORGROUND) {
            const auto focusedAppClose = !(actionParams && actionParams->disableAppClose);
            pendingAction              = std::make_tuple(targetApp->name(), action, std::move(actionParams));

            SwitchRequest switchRequest(
                ServiceName, targetApp->name(), targetApp->switchWindow, std::move(targetApp->switchData));
            return handleSwitchApplication(&switchRequest, focusedAppClose);
        }

        app::Application::requestAction(this, targetApp->name(), action, std::move(actionParams));
        app::Application::requestAction(this, targetApp->name(), action.actionId, std::move(actionParams));
        return true;
    }



@@ 734,12 743,7 @@ namespace app::manager
            app.setState(ApplicationHandle::State::ACTIVE_FORGROUND);
            setState(State::Running);
            EventManager::messageSetApplication(this, app.name());

            auto &[appName, action, data] = pendingAction;
            if (appName == app.name()) {
                app::Application::requestAction(this, appName, action, std::move(data));
                pendingAction = std::make_tuple(ApplicationName{}, 0, nullptr);
            }
            onLaunchFinished(app);
            return true;
        }
        if (getState() == State::AwaitingLostFocusConfirmation) {


@@ 756,6 760,31 @@ namespace app::manager
        return false;
    }

    void ApplicationManager::onLaunchFinished(ApplicationHandle &app)
    {
        if (!actionsRegistry.hasPendingAction()) {
            return;
        }

        auto action = actionsRegistry.getPendingAction();
        if (app.name() != action->target) {
            return;
        }

        switch (action->actionId) {
        case actions::Home:
            [[fallthrough]];
        case actions::Launch:
            actionsRegistry.finished();
            break;
        default: {
            auto &params = action->params;
            app::Application::requestAction(this, app.name(), action->actionId, std::move(params));
            break;
        }
        }
    }

    auto ApplicationManager::handleCloseConfirmation(CloseConfirmation *msg) -> bool
    {
        auto senderApp = getApplication(msg->getSenderName());


@@ 791,7 820,7 @@ namespace app::manager
            return std::make_shared<sys::ResponseMessage>(sys::ReturnCodes::Failure);
        }
        auto action = actionMsg->toAction();
        handleAction(action.get());
        handleActionRequest(action.get());

        return std::make_shared<sys::ResponseMessage>();
    }

A module-services/service-appmgr/service-appmgr/model/ActionsRegistry.hpp => module-services/service-appmgr/service-appmgr/model/ActionsRegistry.hpp +65 -0
@@ 0,0 1,65 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <service-appmgr/Actions.hpp>

#include <chrono>
#include <deque>
#include <functional>

namespace app::manager
{
    struct ActionEntry
    {
      public:
        using Clock     = std::chrono::steady_clock;
        using Timestamp = std::chrono::time_point<Clock>;

        explicit ActionEntry(actions::ActionId action,
                             actions::ActionParamsPtr &&params = {},
                             Timestamp creationTime            = Clock::now());

        void setTargetApplication(std::string targetApplication) noexcept;
        [[nodiscard]] auto isExpired() const noexcept -> bool;

        actions::Action actionId;
        actions::ActionParamsPtr params;
        std::string target;

      private:
        friend bool operator==(const ActionEntry &lhs, const ActionEntry &rhs) noexcept;
        friend bool operator!=(const ActionEntry &lhs, const ActionEntry &rhs) noexcept;

        Timestamp createdAt = Clock::now();
    };

    bool operator==(const ActionEntry &lhs, const ActionEntry &rhs) noexcept;
    bool operator!=(const ActionEntry &lhs, const ActionEntry &rhs) noexcept;

    class ActionsRegistry
    {
      public:
        using OnNextActionReadyCallback = std::function<bool(ActionEntry &action)>;

        explicit ActionsRegistry(OnNextActionReadyCallback &&callback);

        void enqueue(ActionEntry &&action);
        void finished();

        [[nodiscard]] auto hasPendingAction() const noexcept -> bool;
        [[nodiscard]] auto getPendingAction() noexcept -> ActionEntry *;

      private:
        [[nodiscard]] auto isCurrentlyProcessing() const noexcept -> bool;
        void addAction(ActionEntry &&action);
        void notifyAboutNextAction();
        void removeExpiredActions();
        void onFinished();

        std::deque<ActionEntry> actions;
        OnNextActionReadyCallback nextActionReady;
        ActionEntry *actionInProgress = nullptr;
    };
} // namespace app::manager

M module-services/service-appmgr/service-appmgr/model/ApplicationManager.hpp => module-services/service-appmgr/service-appmgr/model/ApplicationManager.hpp +8 -7
@@ 5,6 5,7 @@

#include "ApplicationHandle.hpp"
#include "ApplicationsRegistry.hpp"
#include "ActionsRegistry.hpp"

#include <module-apps/Application.hpp>
#include <module-apps/ApplicationLauncher.hpp>


@@ 115,11 116,12 @@ namespace app::manager

        // Message handlers
        void registerMessageHandlers();
        auto handleAction(ActionRequest *actionMsg) -> bool;
        auto handleHomeAction() -> bool;
        auto handleLaunchAction(ApplicationLaunchData *launchParams) -> bool;
        bool handleAction(ActionEntry &action);
        void handleActionRequest(ActionRequest *actionMsg);
        auto handleHomeAction(ActionEntry &action) -> bool;
        auto handleLaunchAction(ActionEntry &action) -> bool;
        auto handleCloseSystem() -> bool;
        auto handleCustomAction(actions::ActionId action, actions::ActionParamsPtr &&actionParams) -> bool;
        auto handleCustomAction(ActionEntry &action) -> bool;
        auto handleSwitchApplication(SwitchRequest *msg, bool closeCurrentlyFocusedApp = true) -> bool;
        auto handleCloseConfirmation(CloseConfirmation *msg) -> bool;
        auto handleSwitchConfirmation(SwitchConfirmation *msg) -> bool;


@@ 144,18 146,17 @@ namespace app::manager
        void onApplicationInitialised(ApplicationHandle &app, StartInBackground startInBackground);
        void onApplicationInitFailure(ApplicationHandle &app);
        auto onSwitchConfirmed(ApplicationHandle &app) -> bool;
        void onLaunchFinished(ApplicationHandle &app);
        auto onCloseConfirmed(ApplicationHandle &app) -> bool;
        void onPhoneLocked();

        ApplicationName rootApplicationName;
        ActionsRegistry actionsRegistry;
        std::unique_ptr<sys::Timer> blockingTimer; //< timer to count time from last user's activity. If it reaches time
                                                   // defined in settings database application
                                                   // manager is sending signal to power manager and changing window to
                                                   // the desktop window in the blocked state.
        std::unique_ptr<sys::Timer> shutdownDelay;
        // Temporary solution - to be replaced with ActionsMiddleware.
        std::tuple<ApplicationName, actions::ActionId, actions::ActionParamsPtr> pendingAction;

        std::unique_ptr<settings::Settings> settings;
        std::unique_ptr<sys::phone_modes::Observer> phoneModeObserver;
        void displayLanguageChanged(std::string value);

A module-services/service-appmgr/tests/CMakeLists.txt => module-services/service-appmgr/tests/CMakeLists.txt +10 -0
@@ 0,0 1,10 @@
add_catch2_executable(
    NAME
        actions-registry-tests
    SRCS
        tests-main.cpp
        test-ActionsRegistry.cpp
    LIBS
        service-appmgr
        module-utils
)

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

#include <catch2/catch.hpp>

#include <module-services/service-appmgr/service-appmgr/model/ActionsRegistry.hpp>

using namespace app::manager;

TEST_CASE("Given actions registry when initialise it incorrectly then verify")
{
    REQUIRE_THROWS(ActionsRegistry{nullptr});
}

TEST_CASE("Given empty actions registry when verify its state then no pending actions")
{
    auto nextActionCallback = [](ActionEntry & /*entry*/) { return true; };
    ActionsRegistry registry{std::move(nextActionCallback)};

    REQUIRE(!registry.hasPendingAction());
}

TEST_CASE("Given actions registry when enqueue then check pending action")
{
    auto nextActionCallback = [](ActionEntry & /*entry*/) { return true; };
    ActionsRegistry registry{std::move(nextActionCallback)};

    registry.enqueue(ActionEntry{actions::Launch});
    REQUIRE(registry.hasPendingAction());
    REQUIRE(registry.getPendingAction()->actionId == actions::Launch);
}

TEST_CASE("Given actions registry when enqueue then check if action was processed")
{
    bool handled            = false;
    auto nextActionCallback = [&handled](ActionEntry & /*entry*/) {
        handled = true;
        return true;
    };
    ActionsRegistry registry{std::move(nextActionCallback)};

    registry.enqueue(ActionEntry{actions::Launch});
    REQUIRE(handled);
}

TEST_CASE("Given actions registry when finished queued action then no pending actions")
{
    auto nextActionCallback = [](ActionEntry & /*entry*/) { return true; };
    ActionsRegistry registry{std::move(nextActionCallback)};

    registry.enqueue(ActionEntry{actions::Launch});
    registry.finished();
    REQUIRE(!registry.hasPendingAction());
}

TEST_CASE("Given actions registry when enqueue multiple actions sequentially then count handled actions")
{
    int counter             = 0;
    auto nextActionCallback = [&counter](ActionEntry & /*entry*/) {
        ++counter;
        return true;
    };
    ActionsRegistry registry{std::move(nextActionCallback)};

    registry.enqueue(ActionEntry{actions::Launch});
    registry.finished();

    registry.enqueue(ActionEntry{actions::Launch});
    registry.finished();

    registry.enqueue(ActionEntry{actions::Launch});
    registry.finished();

    REQUIRE(counter == 3);
}

TEST_CASE("Given actions registry when enqueue multiple actions at once then count handled actions")
{
    int counter             = 0;
    auto nextActionCallback = [&counter](ActionEntry & /*entry*/) {
        ++counter;
        return true;
    };
    ActionsRegistry registry{std::move(nextActionCallback)};

    registry.enqueue(ActionEntry{actions::Launch});
    REQUIRE(counter == 1);
    REQUIRE(registry.hasPendingAction());

    registry.enqueue(ActionEntry{actions::Launch});
    registry.enqueue(ActionEntry{actions::Launch});

    registry.finished(); // Finished processing the 1st action
    REQUIRE(counter == 2);
    REQUIRE(registry.hasPendingAction());

    registry.finished(); // Finished processing the 2nd action
    REQUIRE(counter == 3);
    REQUIRE(registry.hasPendingAction());

    registry.finished(); // Finished processing the 3rd action
    REQUIRE(!registry.hasPendingAction());
}

TEST_CASE("Given actions registry when enqueue multiple actions at once then wait for them to expire.")
{
    auto nextActionCallback = [](ActionEntry & /*entry*/) { return true; };
    ActionsRegistry registry{std::move(nextActionCallback)};

    registry.enqueue(ActionEntry{actions::Launch});
    REQUIRE(registry.hasPendingAction());

    registry.enqueue(ActionEntry{actions::Launch, {}, std::chrono::steady_clock::now() - std::chrono::seconds{29}});
    registry.enqueue(ActionEntry{actions::Launch, {}, std::chrono::steady_clock::now() - std::chrono::seconds{30}});

    registry.finished(); // Finished processing the 1st action
    REQUIRE(registry.hasPendingAction());

    registry.finished();                   // Finished processing the 2nd action
    REQUIRE(!registry.hasPendingAction()); // 3rd action has expired.
}

TEST_CASE("Given actions registry when enqueue multiple actions at once then process the specific ones only.")
{
    int counter             = 0;
    auto nextActionCallback = [&counter](ActionEntry &entry) {
        if (entry.actionId == actions::UserAction) {
            ++counter;
            return true;
        }
        return false;
    };
    ActionsRegistry registry{std::move(nextActionCallback)};

    registry.enqueue(ActionEntry{actions::Launch});
    REQUIRE(!registry.hasPendingAction());

    registry.enqueue(ActionEntry{actions::UserAction});
    registry.enqueue(ActionEntry{actions::Launch});
    REQUIRE(counter == 1);

    registry.finished();
    REQUIRE(counter == 1);
    REQUIRE(!registry.hasPendingAction());
}

A module-services/service-appmgr/tests/tests-main.cpp => module-services/service-appmgr/tests/tests-main.cpp +5 -0
@@ 0,0 1,5 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#define CATCH_CONFIG_MAIN // This tells Catch to provide a main() - only do this in one cpp file
#include <catch2/catch.hpp>