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();
}