~aleteoryx/muditaos

7cbfd6a98449c3b005ad0e4c35408728f0957499 — Mateusz Piesta 3 years ago fcc30ee
[BH-1356] Meditation stats backend

Added meditation statc backend.
Added temporary widget for testing
purposes and corresponding modules
(meditation stats presenter, window,model).
29 files changed, 805 insertions(+), 81 deletions(-)

A image/user/db/meditation_stats_001.sql
M module-db/Database/sqlite3vfs.cpp
M module-services/service-db/DatabaseAgent.cpp
M module-services/service-db/agents/settings/SettingsAgent.cpp
M module-services/service-db/agents/settings/SettingsAgent.hpp
M module-services/service-db/include/service-db/DatabaseAgent.hpp
M products/BellHybrid/apps/application-bell-meditation-timer/CMakeLists.txt
M products/BellHybrid/apps/application-bell-meditation-timer/MeditationTimer.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.hpp
M products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.cpp
M products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.hpp
A products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.cpp
A products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.hpp
M products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.cpp
M products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.hpp
M products/BellHybrid/services/db/CMakeLists.txt
M products/BellHybrid/services/db/ServiceDB.cpp
A products/BellHybrid/services/db/agents/MeditationStatsAgent.cpp
A products/BellHybrid/services/db/agents/MeditationStatsAgent.hpp
A products/BellHybrid/services/db/databases/CMakeLists.txt
A products/BellHybrid/services/db/databases/MeditationStatisticsDB.cpp
A products/BellHybrid/services/db/databases/MeditationStatisticsDB.hpp
A products/BellHybrid/services/db/databases/MeditationStatisticsTable.cpp
A products/BellHybrid/services/db/databases/MeditationStatisticsTable.hpp
A products/BellHybrid/services/db/include/db/MeditationStatsMessages.hpp
A products/BellHybrid/services/db/tests/CMakeLists.txt
A products/BellHybrid/services/db/tests/MeditationStatisticsTable_tests.cpp
M products/PurePhone/services/db/ServiceDB.cpp
A image/user/db/meditation_stats_001.sql => image/user/db/meditation_stats_001.sql +9 -0
@@ 0,0 1,9 @@
-- Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
-- For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

CREATE TABLE IF NOT EXISTS meditation_stats
(
    _id         INTEGER PRIMARY KEY,
    timestamp   TEXT,
    duration    INTEGER
);

M module-db/Database/sqlite3vfs.cpp => module-db/Database/sqlite3vfs.cpp +24 -8
@@ 754,22 754,38 @@ static int ecophoneSleep(sqlite3_vfs *pVfs, int nMicro)
    return nMicro;
}

struct julian_clock
{
    using rep        = double;
    using period     = std::ratio<86400>; /// Number of seconds per day
    using duration   = std::chrono::duration<rep, period>;
    using time_point = std::chrono::time_point<julian_clock>;

    static constexpr bool is_steady = false;

    /// Returns difference in hours between start of UTC epoch(01/01/1970) and Julian epoch(-4713-11-24 12:00:00)
    static constexpr auto jdiff()
    {
        using namespace std::chrono_literals;
        return 58574100h;
    }

    static time_point now() noexcept
    {
        using namespace std::chrono;
        return time_point{duration{system_clock::now().time_since_epoch()} + jdiff()};
    }
};

/*
 ** Set *pTime to the current UTC time expressed as a Julian day. Return
 ** SQLITE_OK if successful, or an error code otherwise.
 **
 **   http://en.wikipedia.org/wiki/Julian_day
 **
 ** This implementation is not very good. The current time is rounded to
 ** an integer number of seconds. Also, assuming time_t is a signed 32-bit
 ** value, it will stop working some time in the year 2038 AD (the so-called
 ** "year 2038" problem that afflicts systems that store time this way).
 */
static int ecophoneCurrentTime(sqlite3_vfs *pVfs, double *pTime)
{

    time_t t = time(0);
    *pTime   = t / 86400.0 + 2440587.5;
    *pTime = julian_clock::now().time_since_epoch().count();
    return SQLITE_OK;
}


M module-services/service-db/DatabaseAgent.cpp => module-services/service-db/DatabaseAgent.cpp +8 -0
@@ 10,3 10,11 @@ namespace sys

DatabaseAgent::DatabaseAgent(sys::Service *parentService) : parentService(parentService)
{}
bool DatabaseAgent::storeIntoFile(const std::filesystem::path &file)
{
    if (database != nullptr) {
        return database->storeIntoFile(file);
    }

    return false;
}

M module-services/service-db/agents/settings/SettingsAgent.cpp => module-services/service-db/agents/settings/SettingsAgent.cpp +0 -13
@@ 28,10 28,7 @@ SettingsAgent::SettingsAgent(sys::Service *parentService, const std::string dbNa
    }

    database = std::make_unique<Database>(getDbFilePath().c_str());
}

void SettingsAgent::initDb()
{
    factorySettings.initDb(database.get());

    // first approach -> take care about big amount of variables


@@ 49,11 46,6 @@ void SettingsAgent::initDb()
    } while (allVars->nextRow());
}

void SettingsAgent::deinitDb()
{
    database->deinitialize();
}

void SettingsAgent::registerMessages()
{
    // connect handler & message in parent service


@@ 76,11 68,6 @@ void SettingsAgent::unRegisterMessages()
    parentService->disconnect(typeid(settings::Messages::UnregisterOnVariableChange));
}

auto SettingsAgent::getDbInitString() -> const std::string
{
    return {};
}

auto SettingsAgent::getDbFilePath() -> const std::string
{
    return (purefs::dir::getUserDiskPath() / dbName).string();

M module-services/service-db/agents/settings/SettingsAgent.hpp => module-services/service-db/agents/settings/SettingsAgent.hpp +1 -5
@@ 26,11 26,9 @@ namespace sys
class SettingsAgent : public DatabaseAgent
{
  public:
    SettingsAgent(sys::Service *parentService, const std::string dbName, settings::SettingsCache *cache = nullptr);
    SettingsAgent(sys::Service *parentService, std::string dbName, settings::SettingsCache *cache = nullptr);
    ~SettingsAgent() = default;

    void initDb() override;
    void deinitDb() override;
    void registerMessages() override;
    void unRegisterMessages() override;
    auto getAgentName() -> const std::string override;


@@ 53,8 51,6 @@ class SettingsAgent : public DatabaseAgent
    auto dbRegisterValueChange(const settings::EntryPath &path) -> bool;
    auto dbUnregisterValueChange(const settings::EntryPath &path) -> bool;

    auto getDbInitString() -> const std::string override;

    // msg handlers
    // variable
    auto handleGetVariable(sys::Message *req) -> sys::MessagePointer;

M module-services/service-db/include/service-db/DatabaseAgent.hpp => module-services/service-db/include/service-db/DatabaseAgent.hpp +2 -11
@@ 18,29 18,20 @@ namespace sys
class DatabaseAgent
{
  public:
    DatabaseAgent(sys::Service *parentService);
    explicit DatabaseAgent(sys::Service *parentService);
    virtual ~DatabaseAgent() = default;

    virtual void initDb()                                          = 0;
    virtual void deinitDb()                                        = 0;
    virtual void registerMessages()                                = 0;
    virtual void unRegisterMessages()                              = 0;
    [[nodiscard]] virtual auto getAgentName() -> const std::string = 0;
    bool storeIntoFile(const std::filesystem::path &file)
    {
        if (database != nullptr)
            return database->storeIntoFile(file);

        return false;
    }
    bool storeIntoFile(const std::filesystem::path &file);
    [[nodiscard]] virtual auto getDbFilePath() -> const std::string = 0;

    static constexpr auto ZERO_ROWS_FOUND = 0;
    static constexpr auto ONE_ROW_FOUND   = 1;

  protected:
    [[nodiscard]] virtual auto getDbInitString() -> const std::string = 0;

    sys::Service *parentService;
    std::unique_ptr<Database> database;
};

M products/BellHybrid/apps/application-bell-meditation-timer/CMakeLists.txt => products/BellHybrid/apps/application-bell-meditation-timer/CMakeLists.txt +3 -0
@@ 22,6 22,7 @@ target_sources(application-bell-meditation-timer
        models/ChimeInterval.cpp
        models/ChimeVolume.cpp
        models/StartDelay.cpp
        models/Statistics.cpp
        presenter/MeditationCountdownPresenter.cpp
        presenter/MeditationProgressPresenter.cpp
        presenter/MeditationTimerPresenter.cpp


@@ 35,6 36,7 @@ target_sources(application-bell-meditation-timer
        windows/MeditationTimerWindow.cpp
        windows/SettingsWindow.cpp
        windows/StatisticsWindow.cpp
        widgets/SummaryListItem.cpp
    PUBLIC
        include/application-bell-meditation-timer/MeditationTimer.hpp
)


@@ 42,6 44,7 @@ target_sources(application-bell-meditation-timer
target_link_libraries(application-bell-meditation-timer
    PRIVATE
        app
        bell::db
        bell::audio
        bell::app-common
        bell::app-main

M products/BellHybrid/apps/application-bell-meditation-timer/MeditationTimer.cpp => products/BellHybrid/apps/application-bell-meditation-timer/MeditationTimer.cpp +1 -1
@@ 67,7 67,7 @@ namespace app
                              });

        windowsFactory.attach(meditation::StatisticsWindow::name, [](ApplicationCommon *app, const std::string &name) {
            auto presenter = std::make_unique<app::meditation::StatisticsPresenter>();
            auto presenter = std::make_unique<app::meditation::StatisticsPresenter>(app);
            return std::make_unique<meditation::StatisticsWindow>(app, std::move(presenter));
        });


A products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.cpp => products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.cpp +53 -0
@@ 0,0 1,53 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "Statistics.hpp"
#include <ApplicationCommon.hpp>
#include <db/ServiceDB.hpp>
#include <db/MeditationStatsMessages.hpp>

namespace
{
    using namespace service::db::meditation_stats;
    std::optional<messages::Response> sendDBRequest(sys::Service *serv, std::shared_ptr<sys::Message> &&msg)
    {
        const auto ret = serv->bus.sendUnicastSync(std::move(msg), service::name::db, sys::BusProxy::defaultTimeout);
        if (ret.first == sys::ReturnCodes::Success) {
            if (auto resp = std::dynamic_pointer_cast<messages::Response>(ret.second)) {
                return *resp;
            }
        }
        return std::nullopt;
    }
} // namespace

namespace app::meditation::models
{

    Statistics::Statistics(app::ApplicationCommon *app) : app::AsyncCallbackReceiver{app}, app{app}
    {}

    void Statistics::addEntry(const time_t utcTimestamp, const std::chrono::minutes duration)
    {
        const auto addRequest = AsyncRequest::createFromMessage(
            std::make_unique<messages::Add>(Entry(utcTimestamp, duration)), service::name::db);
        addRequest->execute(app, this, [this](sys::ResponseMessage *) { return true; });
    }

    std::optional<Summary> Statistics::getSummary(const std::uint32_t days)
    {
        const auto result = sendDBRequest(app, std::make_shared<messages::GetByDays>(days));
        if (not result) {
            return std::nullopt;
        }

        const auto sum   = std::accumulate(result->entries.cbegin(),
                                         result->entries.cend(),
                                         std::chrono::minutes{},
                                         [](const auto &sum, const auto &e) { return sum + e.duration; });
        const auto avg   = std::chrono::minutes{sum / days};
        const auto count = result->entries.size();

        return Summary{sum, avg, count};
    }
} // namespace app::meditation::models
\ No newline at end of file

A products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.hpp => products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.hpp +36 -0
@@ 0,0 1,36 @@
// 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 <apps-common/AsyncTask.hpp>

#include <chrono>
#include <optional>

namespace app
{
    class ApplicationCommon;
}

namespace app::meditation::models
{
    struct Summary
    {
        std::chrono::minutes sum;
        std::chrono::minutes avg;
        std::size_t count;
    };

    class Statistics : public AsyncCallbackReceiver
    {
      public:
        explicit Statistics(app::ApplicationCommon *app);
        void addEntry(time_t utcTimestamp, std::chrono::minutes duration);
        std::optional<Summary> getSummary(std::uint32_t days);

      private:
        app::ApplicationCommon *app{nullptr};
    };

} // namespace app::meditation::models

M products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.cpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.cpp +36 -6
@@ 2,24 2,54 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "StatisticsPresenter.hpp"
#include "widgets/SummaryListItem.hpp"
#include "models/Statistics.hpp"
#include "MeditationMainWindow.hpp"

#include <db/MeditationStatsMessages.hpp>
#include <ApplicationCommon.hpp>
#include <common/windows/BellFinishedWindow.hpp>
#include <common/BellListItemProvider.hpp>

namespace app::meditation
{
    StatisticsPresenter::StatisticsPresenter()
    {}
    StatisticsPresenter::StatisticsPresenter(app::ApplicationCommon *app)
    {
        const auto model = std::make_unique<models::Statistics>(app);

        const auto t1      = cpp_freertos::Ticks::TicksToMs(cpp_freertos::Ticks::GetTicks());
        const auto summary = model->getSummary(500);
        const auto t2      = cpp_freertos::Ticks::TicksToMs(cpp_freertos::Ticks::GetTicks());

        auto entry1 = new SummaryListItem("Total [min]", std::to_string(summary->sum.count()));
        auto entry2 = new SummaryListItem("Avg [min]", std::to_string(summary->avg.count()));
        auto entry3 = new SummaryListItem("Entries", std::to_string(summary->count));
        auto entry4 = new SummaryListItem("Query took [ms]", std::to_string(t2 - t1));

        listItemsProvider = std::make_shared<BellListItemProvider>(
            BellListItemProvider::Items{reinterpret_cast<gui::BellSideListItemWithCallbacks *>(entry1),
                                        reinterpret_cast<gui::BellSideListItemWithCallbacks *>(entry2),
                                        reinterpret_cast<gui::BellSideListItemWithCallbacks *>(entry3),
                                        reinterpret_cast<gui::BellSideListItemWithCallbacks *>(entry4)});
    }
    void StatisticsPresenter::eraseProviderData()
    {}
    {
        listItemsProvider->clearData();
    }
    void StatisticsPresenter::loadData()
    {}
    void StatisticsPresenter::saveData()
    {}
    auto StatisticsPresenter::getPagesProvider() const -> std::shared_ptr<gui::ListItemProvider>
    {
        return std::shared_ptr<gui::ListItemProvider>();
        return listItemsProvider;
    }
    void StatisticsPresenter::handleEnter()
    {}

    {
        app->switchWindow(
            gui::window::bell_finished::defaultName,
            gui::BellFinishedWindowData::Factory::create("circle_success_big", MeditationMainWindow::defaultName));
    }
    void StatisticsPresenter::exitWithoutSave()
    {}
} // namespace app::meditation

M products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.hpp => products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.hpp +10 -1
@@ 4,14 4,21 @@
#pragma once

#include "data/Contract.hpp"

#include <memory>

namespace app
{
    class ApplicationCommon;
    class BellListItemProvider;
} // namespace app

namespace app::meditation
{
    class StatisticsPresenter : public contract::Presenter
    {
      public:
        StatisticsPresenter();
        explicit StatisticsPresenter(app::ApplicationCommon *app);
        auto getPagesProvider() const -> std::shared_ptr<gui::ListItemProvider> override;
        void loadData() override;
        void saveData() override;


@@ 20,5 27,7 @@ namespace app::meditation
        void exitWithoutSave() override;

      private:
        app::ApplicationCommon *app{nullptr};
        std::shared_ptr<BellListItemProvider> listItemsProvider;
    };
} // namespace app::meditation

A products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.cpp => products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.cpp +46 -0
@@ 0,0 1,46 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SummaryListItem.hpp"

#include <Text.hpp>
#include <BoxLayout.hpp>

namespace
{
    constexpr auto width        = style::window::default_body_width;
    constexpr auto height       = 66;
    constexpr auto title_height = 33;
    constexpr auto value_height = 33;
} // namespace

namespace app::meditation
{
    using namespace gui;
    SummaryListItem::SummaryListItem(const std::string &titleText, const std::string &valueText)
    {
        setMinimumSize(width, height);
        setMargins(Margins(0, style::margins::big, 0, style::margins::huge));
        activeItem = false;

        body = new VBox(this, 0, 0, 0, 0);
        body->setEdges(RectangleEdge::None);

        title = new Text(body, 0, 0, 0, 0);
        title->setMinimumSize(width, title_height);
        title->setFont(style::window::font::bigbold);
        title->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Top));
        title->setText(titleText);

        value = new Text(body, 0, 0, 0, 0);
        value->setMinimumSize(width, value_height);
        value->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Bottom));
        value->setFont(style::window::font::big);
        value->setText(valueText);

        dimensionChangedCallback = [&]([[maybe_unused]] Item &item, const BoundingBox &newDim) -> bool {
            body->setArea({0, 0, newDim.w, newDim.h});
            return true;
        };
    }
} // namespace app::meditation

A products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.hpp => products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.hpp +26 -0
@@ 0,0 1,26 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include <ListItem.hpp>

namespace gui
{
    class Text;
    class VBox;
} // namespace gui

namespace app::meditation
{
    class SummaryListItem : public gui::ListItem
    {
      public:
        SummaryListItem(const std::string &title, const std::string &value);

      private:
        gui::VBox *body{};
        gui::Text *title{};
        gui::Text *value{};
    };
} // namespace app::meditation

M products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.cpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.cpp +21 -12
@@ 1,17 1,23 @@
// 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 "StatisticsWindow.hpp"

#include "MeditationMainWindow.hpp"

#include <common/windows/BellFinishedWindow.hpp>
#include <ListView.hpp>
#include <common/data/StyleCommon.hpp>
#include <apps-common/ApplicationCommon.hpp>
#include <module-gui/gui/input/InputEvent.hpp>
#include <module-gui/gui/widgets/SideListView.hpp>
#include <apps-common/InternalModel.hpp>

namespace
{
    constexpr auto height     = 400;
    constexpr auto width      = 380;
    constexpr auto top_margin = 41;
} // namespace

namespace app::meditation
{
    using namespace gui;


@@ 35,23 41,26 @@ namespace app::meditation
        statusBar->setVisible(false);
        header->setTitleVisibility(false);
        navBar->setVisible(false);

        list = new ListView(this,
                            style::window::default_left_margin,
                            top_margin,
                            width,
                            height,
                            presenter->getPagesProvider(),
                            listview::ScrollBarType::Fixed);
        list->setAlignment(Alignment(Alignment::Horizontal::Center, Alignment::Vertical::Center));

        list->rebuildList();
    }

    void StatisticsWindow::onBeforeShow(gui::ShowMode mode, gui::SwitchData *data)
    {
        setFocusItem(sideListView);
        setFocusItem(list);
    }

    bool StatisticsWindow::onInput(const gui::InputEvent &inputEvent)
    {
        if (sideListView->onInput(inputEvent)) {
            return true;
        }
        if (inputEvent.isShortRelease(KeyCode::KEY_ENTER)) {
            presenter->handleEnter();
            return true;
        }

        return AppWindow::onInput(inputEvent);
    }


M products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.hpp => products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.hpp +4 -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


@@ 10,7 10,8 @@
namespace gui
{
    class SideListView;
}
    class ListView;
} // namespace gui

namespace app::meditation
{


@@ 27,7 28,7 @@ namespace app::meditation
        void rebuild() override;

      private:
        gui::SideListView *sideListView{};
        gui::ListView *list{};
        std::unique_ptr<app::meditation::contract::Presenter> presenter;
    };
} // namespace app::meditation

M products/BellHybrid/services/db/CMakeLists.txt => products/BellHybrid/services/db/CMakeLists.txt +14 -7
@@ 1,21 1,28 @@
add_library(db STATIC)
add_library(bell::db ALIAS db)
add_library(databases STATIC)
add_library(bell::db ALIAS databases)

target_sources(db
add_subdirectory(databases)

target_sources(databases
    PRIVATE
        ServiceDB.cpp
        agents/MeditationStatsAgent.cpp
    PUBLIC
        include/db/ServiceDB.hpp
        include/db/SystemSettings.hpp
)

target_include_directories(db
target_include_directories(databases
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        include
)

target_link_libraries(db
target_link_libraries(databases
   PRIVATE
        module-db
        bell::db::meditation_stats
        service-db
)

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

M products/BellHybrid/services/db/ServiceDB.cpp => products/BellHybrid/services/db/ServiceDB.cpp +9 -12
@@ 3,6 3,8 @@

#include <db/ServiceDB.hpp>

#include "agents/MeditationStatsAgent.hpp"

#include <module-db/Databases/EventsDB.hpp>
#include <module-db/Databases/MultimediaFilesDB.hpp>



@@ 82,9 84,9 @@ sys::ReturnCodes ServiceDB::InitHandler()
        std::make_unique<db::multimedia_files::MultimediaFilesRecordInterface>(multimediaFilesDB.get());

    databaseAgents.emplace(std::make_unique<SettingsAgent>(this, "settings_bell.db"));
    databaseAgents.emplace(std::make_unique<service::db::agents::MeditationStats>(this, "meditation_stats.db"));

    for (auto &dbAgent : databaseAgents) {
        dbAgent->initDb();
        dbAgent->registerMessages();
    }



@@ 98,16 100,11 @@ bool ServiceDB::StoreIntoBackup(const std::filesystem::path &backupPath)
        return false;
    }

    for (auto &db : databaseAgents) {
        if (db.get() && db.get()->getAgentName() == "settingsAgent") {

            if (db->storeIntoFile(backupPath / std::filesystem::path(db->getDbFilePath()).filename()) == false) {
                LOG_ERROR("settingsAgent backup failed");
                return false;
            }
            break;
    return std::all_of(databaseAgents.cbegin(), databaseAgents.cend(), [&backupPath](const auto &agent) {
        if (not agent->storeIntoFile(backupPath / std::filesystem::path(agent->getDbFilePath()).filename())) {
            LOG_ERROR("%s backup failed", agent->getAgentName().c_str());
            return false;
        }
    }

    return true;
        return true;
    });
}

A products/BellHybrid/services/db/agents/MeditationStatsAgent.cpp => products/BellHybrid/services/db/agents/MeditationStatsAgent.cpp +66 -0
@@ 0,0 1,66 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "MeditationStatsAgent.hpp"
#include "db/MeditationStatsMessages.hpp"

#include <module-sys/Service/include/Service/Service.hpp>
#include <module-vfs/paths/include/purefs/filesystem_paths.hpp>
#include <MeditationStatisticsDB.hpp>

namespace
{
    using namespace service::db::meditation_stats;

    std::vector<Entry> transformRecords(const std::vector<db::meditation_stats::TableRow> &records)
    {
        std::vector<Entry> ret;
        std::transform(records.cbegin(), records.cend(), std::back_inserter(ret), [](const auto &record) {
            return Entry(record.timestamp, record.duration);
        });
        return ret;
    }
} // namespace

namespace service::db::agents
{
    MeditationStats::MeditationStats(sys::Service *parentService, const std::string dbName)
        : DatabaseAgent(parentService), dbName{dbName}, db{getDbFilePath().c_str()}
    {}

    void MeditationStats::registerMessages()
    {
        parentService->connect(messages::Add({}), [this](const auto &req) { return handleAdd(req); });
        parentService->connect(messages::GetByDays({}), [this](const auto &req) { return handleGetByDays(req); });
    }

    void MeditationStats::unRegisterMessages()
    {
        parentService->disconnect(typeid(messages::Add));
        parentService->disconnect(typeid(messages::GetByDays));
    }

    auto MeditationStats::getDbFilePath() -> const std::string
    {
        return (purefs::dir::getUserDiskPath() / dbName).string();
    }
    auto MeditationStats::getAgentName() -> const std::string
    {
        return dbName + "_agent";
    }
    sys::MessagePointer MeditationStats::handleAdd(const sys::Message *req)
    {
        if (auto msg = dynamic_cast<const messages::Add *>(req)) {
            db.table.add(::db::meditation_stats::TableRow{Record{}, msg->entry.timestamp, msg->entry.duration});
        }
        return std::make_shared<sys::ResponseMessage>();
    }
    sys::MessagePointer MeditationStats::handleGetByDays(const sys::Message *req)
    {
        if (auto msg = dynamic_cast<const messages::GetByDays *>(req)) {
            const auto records = db.table.getByDays(msg->days);
            return std::make_shared<messages::Response>(transformRecords(records));
        }
        return std::make_shared<sys::ResponseMessage>();
    }
} // namespace service::db::agents
\ No newline at end of file

A products/BellHybrid/services/db/agents/MeditationStatsAgent.hpp => products/BellHybrid/services/db/agents/MeditationStatsAgent.hpp +38 -0
@@ 0,0 1,38 @@
// 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 <MeditationStatisticsDB.hpp>

#include <service-db/DatabaseAgent.hpp>
#include <module-services/service-db/include/service-db/SettingsMessages.hpp>
#include <module-sys/Service/include/Service/Message.hpp>

#include <string>

namespace sys
{
    class Service;
} // namespace sys

namespace service::db::agents
{
    class MeditationStats : public DatabaseAgent
    {
      public:
        MeditationStats(sys::Service *parentService, std::string dbName);

        void registerMessages() override;
        void unRegisterMessages() override;
        auto getAgentName() -> const std::string override;
        auto getDbFilePath() -> const std::string override;

      private:
        sys::MessagePointer handleAdd(const sys::Message *req);
        sys::MessagePointer handleGetByDays(const sys::Message *req);
        std::string dbName;
        ::db::meditation_stats::MeditationStatisticsDB db;
    };

} // namespace service::db::agents

A products/BellHybrid/services/db/databases/CMakeLists.txt => products/BellHybrid/services/db/databases/CMakeLists.txt +13 -0
@@ 0,0 1,13 @@
add_library(meditation_stats_db
        MeditationStatisticsTable.cpp
        MeditationStatisticsDB.cpp
        )

add_library(bell::db::meditation_stats ALIAS meditation_stats_db)

target_link_libraries(meditation_stats_db
        PUBLIC
        module-db
        )

target_include_directories(meditation_stats_db PUBLIC .)
\ No newline at end of file

A products/BellHybrid/services/db/databases/MeditationStatisticsDB.cpp => products/BellHybrid/services/db/databases/MeditationStatisticsDB.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 "MeditationStatisticsDB.hpp"

namespace db::meditation_stats
{
    MeditationStatisticsDB::MeditationStatisticsDB(const char *name) : Database(name), table(this)
    {}

} // namespace db::meditation_stats

A products/BellHybrid/services/db/databases/MeditationStatisticsDB.hpp => products/BellHybrid/services/db/databases/MeditationStatisticsDB.hpp +19 -0
@@ 0,0 1,19 @@
// 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 "MeditationStatisticsTable.hpp"
#include <module-db/Database/Database.hpp>

namespace db::meditation_stats
{
    class MeditationStatisticsDB : public Database
    {
      public:
        explicit MeditationStatisticsDB(const char *name);

        MeditationStatsTable table;
    };

} // namespace db::meditation_stats

A products/BellHybrid/services/db/databases/MeditationStatisticsTable.cpp => products/BellHybrid/services/db/databases/MeditationStatisticsTable.cpp +150 -0
@@ 0,0 1,150 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "MeditationStatisticsTable.hpp"
#include <time.h>
#include <chrono>
#include <date/date.h>

namespace
{
    using namespace db::meditation_stats;
    constexpr auto tableName = "meditation_stats";

    /// We could use the SQLite function "datetime(timestamp,'unixepoch') to generate the correct time string.
    /// Unfortunately, SQLite v3.38.1 has some problems converting UNIX timestamps. That's why we decided we would store
    /// timestamps using string format, as it doesn't cause any issues.

    std::string prepare_timestamp(time_t unix_timestamp)
    {
        char time_buf[64]{};
        struct tm ts
        {};
        ts = *gmtime(&unix_timestamp);
        strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S", &ts);

        return time_buf;
    }

    time_t to_unixepoch_timestamp(std::string_view str)
    {
        std::istringstream in{str.data()};
        date::sys_seconds tp;
        in >> date::parse("%Y-%m-%d %H:%M:%S", tp);
        return tp.time_since_epoch().count();
    }

    std::vector<TableRow> get_by_x(Database *db, const std::string_view modifier, const std::uint32_t x)
    {
        if (x == 0) {
            return {};
        }

        const auto retQuery = db->query("SELECT * from %s where timestamp BETWEEN "
                                        "datetime('now','-%lu %s') and datetime('now');",
                                        tableName,
                                        x,
                                        modifier.data());

        if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) {
            return {};
        }

        std::vector<TableRow> ret;
        ret.reserve(retQuery->getRowCount());

        do {
            ret.push_back(TableRow{
                {(*retQuery)[0].getUInt32()},                       // ID
                to_unixepoch_timestamp((*retQuery)[1].getString()), // timestamp
                std::chrono::minutes{(*retQuery)[2].getUInt32()}    // duration
            });
        } while (retQuery->nextRow());

        return ret;
    }
} // namespace

namespace db::meditation_stats
{
    MeditationStatsTable::MeditationStatsTable(Database *db) : Table(db)
    {}

    auto MeditationStatsTable::create() -> bool
    {
        return true;
    }
    auto MeditationStatsTable::removeById([[maybe_unused]] uint32_t id) -> bool
    {
        return false;
    }
    auto MeditationStatsTable::removeByField([[maybe_unused]] const TableFields field, [[maybe_unused]] const char *str)
        -> bool
    {
        return false;
    }
    auto MeditationStatsTable::update([[maybe_unused]] const TableRow row) -> bool
    {
        return false;
    }
    auto MeditationStatsTable::getLimitOffsetByField([[maybe_unused]] const uint32_t offset,
                                                     [[maybe_unused]] const uint32_t limit,
                                                     [[maybe_unused]] const TableFields field,
                                                     [[maybe_unused]] const char *str) -> std::vector<TableRow>
    {
        return {};
    }
    auto MeditationStatsTable::countByFieldId([[maybe_unused]] const char *field, [[maybe_unused]] const uint32_t id)
        -> uint32_t
    {
        return 0;
    }
    auto MeditationStatsTable::getById([[maybe_unused]] const uint32_t id) -> TableRow
    {
        return {};
    }

    auto MeditationStatsTable::add(const TableRow entry) -> bool
    {
        return db->execute("INSERT INTO '%s' (timestamp,duration) "
                           "VALUES('%s', '%lu') ",
                           tableName,
                           prepare_timestamp(entry.timestamp).c_str(),
                           static_cast<std::uint32_t>(entry.duration.count()));
    }
    auto MeditationStatsTable::getLimitOffset(const uint32_t offset, const uint32_t limit) -> std::vector<TableRow>
    {
        const auto retQuery =
            db->query("SELECT * from '%s' ORDER BY timestamp DESC LIMIT %lu OFFSET %lu;", tableName, limit, offset);

        if ((retQuery == nullptr) || (retQuery->getRowCount() == 0)) {
            return {};
        }

        std::vector<TableRow> ret;
        ret.reserve(retQuery->getRowCount());

        do {
            ret.push_back(TableRow{
                (*retQuery)[0].getUInt32(),                         // ID
                to_unixepoch_timestamp((*retQuery)[1].getString()), // timestamp
                std::chrono::minutes{(*retQuery)[2].getUInt32()}    // duration
            });
        } while (retQuery->nextRow());

        return ret;
    }
    auto MeditationStatsTable::count() -> uint32_t
    {
        const auto queryRet = db->query("SELECT COUNT(*) FROM '%s';", tableName);
        if (queryRet == nullptr || queryRet->getRowCount() == 0) {
            return 0;
        }

        return (*queryRet)[0].getUInt32();
    }
    auto MeditationStatsTable::getByDays(const uint32_t days) -> std::vector<TableRow>
    {
        return get_by_x(db, "days", days);
    }
} // namespace db::meditation_stats
\ No newline at end of file

A products/BellHybrid/services/db/databases/MeditationStatisticsTable.hpp => products/BellHybrid/services/db/databases/MeditationStatisticsTable.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 <module-db/Tables/Record.hpp>
#include <module-db/Tables/Table.hpp>
#include <module-db/Database/Database.hpp>

namespace db::meditation_stats
{
    struct TableRow : public Record
    {
        time_t timestamp;
        std::chrono::minutes duration;
    };

    enum class TableFields
    {
        timestamp,
        duration
    };

    class MeditationStatsTable : public Table<TableRow, TableFields>
    {
      public:
        explicit MeditationStatsTable(Database *db);
        virtual ~MeditationStatsTable() = default;

        auto create() -> bool override;
        auto add(TableRow entry) -> bool override;
        auto removeById(uint32_t id) -> bool override;
        auto removeByField(TableFields field, const char *str) -> bool override;
        auto update(TableRow entry) -> bool override;
        auto getById(uint32_t id) -> TableRow override;
        auto getLimitOffset(uint32_t offset, uint32_t limit) -> std::vector<TableRow> override;
        auto getLimitOffsetByField(uint32_t offset, uint32_t limit, TableFields field, const char *str)
            -> std::vector<TableRow> override;
        auto count() -> uint32_t override;
        auto countByFieldId(const char *field, uint32_t id) -> uint32_t override;

        auto getByDays(std::uint32_t days) -> std::vector<TableRow>;
    };
} // namespace db::meditation_stats

A products/BellHybrid/services/db/include/db/MeditationStatsMessages.hpp => products/BellHybrid/services/db/include/db/MeditationStatsMessages.hpp +48 -0
@@ 0,0 1,48 @@
// 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 <MessageType.hpp>
#include <Service/Message.hpp>
#include <chrono>
#include <vector>

namespace service::db::meditation_stats
{
    struct Entry
    {
        Entry() = default;
        Entry(const time_t timestamp, const std::chrono::minutes duration) : timestamp{timestamp}, duration{duration}
        {}
        time_t timestamp{};
        std::chrono::minutes duration{};
    };

    namespace messages
    {
        struct Add : public sys::DataMessage
        {
            explicit Add(const Entry &entry) : entry{entry}
            {}
            Entry entry;
        };

        struct GetByDays : public sys::DataMessage
        {
            explicit GetByDays(std::uint32_t days) : days{days}
            {}
            std::uint32_t days;
        };

        struct Response : public sys::ResponseMessage
        {
            Response() = default;
            explicit Response(const std::vector<Entry> &entries) : entries{entries}
            {}

            std::vector<Entry> entries{};
        };

    } // namespace messages
} // namespace service::db::meditation_stats
\ No newline at end of file

A products/BellHybrid/services/db/tests/CMakeLists.txt => products/BellHybrid/services/db/tests/CMakeLists.txt +10 -0
@@ 0,0 1,10 @@
add_catch2_executable(
        NAME
        bell-db
        SRCS
        MeditationStatisticsTable_tests.cpp

        LIBS
        bell::db::meditation_stats
        USE_FS
)
\ No newline at end of file

A products/BellHybrid/services/db/tests/MeditationStatisticsTable_tests.cpp => products/BellHybrid/services/db/tests/MeditationStatisticsTable_tests.cpp +102 -0
@@ 0,0 1,102 @@
// Copyright (c) 2017-2022, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <catch2/catch.hpp>

#include <MeditationStatisticsDB.hpp>

#include <filesystem>
#include <iostream>

namespace
{
    using namespace db::meditation_stats;

    time_t subtract_time(std::chrono::minutes minutes)
    {
        return std::chrono::system_clock::to_time_t(std::chrono::system_clock::now() - minutes);
    }

    time_t get_utc_time()
    {
        return std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
    }

    template <typename Db> class TestDatabase
    {
      public:
        explicit TestDatabase(std::filesystem::path name)
        {
            Database::initialize();

            if (std::filesystem::exists(name)) {
                REQUIRE(std::filesystem::remove(name));
            }

            db = std::make_unique<MeditationStatisticsDB>(name.c_str());

            if (not db->isInitialized()) {
                throw std::runtime_error("Could not initialize database");
            }
        }

        ~TestDatabase()
        {
            Database::deinitialize();
        }

        Db &get()
        {
            return *db;
        }

      private:
        std::filesystem::path name;
        std::unique_ptr<Db> db;
    };
} // namespace

TEST_CASE("Meditation statistics Table - API basic checks")
{
    TestDatabase<MeditationStatisticsDB> db{"meditation_stats.db"};

    const auto timestamp = get_utc_time();
    REQUIRE(db.get().table.add({Record{0}, timestamp, std::chrono::minutes{20}}));
    REQUIRE(db.get().table.add({Record{0}, timestamp, std::chrono::minutes{10}}));
    REQUIRE(db.get().table.count() == 2);

    const auto query_result = db.get().table.getByDays(1);
    REQUIRE(query_result.size() == 2);
    REQUIRE(query_result[0].duration == std::chrono::minutes{20});
    REQUIRE(query_result[1].duration == std::chrono::minutes{10});
    REQUIRE(query_result[0].timestamp == timestamp);
    REQUIRE(query_result[1].timestamp == timestamp);

    REQUIRE(db.get().table.getLimitOffset(0, 3).size() == 2);
    REQUIRE(db.get().table.getLimitOffset(0, 1).size() == 1);
    REQUIRE(db.get().table.getLimitOffset(1, 1).size() == 1);

    /// Not implemented as it does not make sense to have such calls for meditation statistics table.
    REQUIRE(db.get().table.create());
    REQUIRE(not db.get().table.removeById(1));
    REQUIRE(not db.get().table.removeByField(TableFields::duration, "10"));
    REQUIRE(not db.get().table.getById(1).isValid());
    REQUIRE(db.get().table.countByFieldId("duration", 1) == 0);
    REQUIRE(db.get().table.getLimitOffsetByField(0, 2, TableFields::duration, "10").empty());
}

TEST_CASE("Meditation statistics Table - get by days")
{
    TestDatabase<MeditationStatisticsDB> db{"meditation_stats.db"};

    REQUIRE(db.get().table.add({Record{0}, subtract_time(std::chrono::hours{23}), std::chrono::minutes{1}}));
    REQUIRE(db.get().table.getByDays(1).size() == 1);

    REQUIRE(db.get().table.add({Record{0}, subtract_time(std::chrono::hours{23}), std::chrono::minutes{2}}));
    REQUIRE(db.get().table.getByDays(1).size() == 2);

    REQUIRE(db.get().table.add({Record{0}, subtract_time(std::chrono::hours{24}), std::chrono::minutes{3}}));
    REQUIRE(db.get().table.add({Record{0}, subtract_time(std::chrono::hours{25}), std::chrono::minutes{4}}));
    REQUIRE(db.get().table.getByDays(1).size() == 3);
    REQUIRE(db.get().table.getByDays(2).size() == 4);
}

M products/PurePhone/services/db/ServiceDB.cpp => products/PurePhone/services/db/ServiceDB.cpp +1 -2
@@ 234,7 234,7 @@ sys::ReturnCodes ServiceDB::InitHandler()
    notificationsDB = std::make_unique<NotificationsDB>((purefs::dir::getUserDiskPath() / "notifications.db").c_str());
    predefinedQuotesDB = std::make_unique<Database>((purefs::dir::getUserDiskPath() / "predefined_quotes.db").c_str());
    customQuotesDB     = std::make_unique<Database>((purefs::dir::getUserDiskPath() / "custom_quotes.db").c_str());
    multimediaFilesDB = std::make_unique<db::multimedia_files::MultimediaFilesDB>(
    multimediaFilesDB  = std::make_unique<db::multimedia_files::MultimediaFilesDB>(
        (purefs::dir::getUserDiskPath() / "multimedia.db").c_str());

    // Create record interfaces


@@ 253,7 253,6 @@ sys::ReturnCodes ServiceDB::InitHandler()
    databaseAgents.emplace(std::make_unique<SettingsAgent>(this, "settings_v2.db"));

    for (auto &dbAgent : databaseAgents) {
        dbAgent->initDb();
        dbAgent->registerMessages();
    }