From 7cbfd6a98449c3b005ad0e4c35408728f0957499 Mon Sep 17 00:00:00 2001 From: Mateusz Piesta Date: Fri, 22 Jul 2022 12:21:58 +0200 Subject: [PATCH] [BH-1356] Meditation stats backend Added meditation statc backend. Added temporary widget for testing purposes and corresponding modules (meditation stats presenter, window,model). --- image/user/db/meditation_stats_001.sql | 9 ++ module-db/Database/sqlite3vfs.cpp | 32 +++- module-services/service-db/DatabaseAgent.cpp | 8 + .../agents/settings/SettingsAgent.cpp | 13 -- .../agents/settings/SettingsAgent.hpp | 6 +- .../include/service-db/DatabaseAgent.hpp | 13 +- .../CMakeLists.txt | 3 + .../MeditationTimer.cpp | 2 +- .../models/Statistics.cpp | 53 +++++++ .../models/Statistics.hpp | 36 +++++ .../presenter/StatisticsPresenter.cpp | 42 ++++- .../presenter/StatisticsPresenter.hpp | 11 +- .../widgets/SummaryListItem.cpp | 46 ++++++ .../widgets/SummaryListItem.hpp | 26 +++ .../windows/StatisticsWindow.cpp | 33 ++-- .../windows/StatisticsWindow.hpp | 7 +- .../BellHybrid/services/db/CMakeLists.txt | 21 ++- products/BellHybrid/services/db/ServiceDB.cpp | 21 ++- .../db/agents/MeditationStatsAgent.cpp | 66 ++++++++ .../db/agents/MeditationStatsAgent.hpp | 38 +++++ .../services/db/databases/CMakeLists.txt | 13 ++ .../db/databases/MeditationStatisticsDB.cpp | 11 ++ .../db/databases/MeditationStatisticsDB.hpp | 19 +++ .../databases/MeditationStatisticsTable.cpp | 150 ++++++++++++++++++ .../databases/MeditationStatisticsTable.hpp | 44 +++++ .../db/include/db/MeditationStatsMessages.hpp | 48 ++++++ .../services/db/tests/CMakeLists.txt | 10 ++ .../tests/MeditationStatisticsTable_tests.cpp | 102 ++++++++++++ products/PurePhone/services/db/ServiceDB.cpp | 3 +- 29 files changed, 805 insertions(+), 81 deletions(-) create mode 100644 image/user/db/meditation_stats_001.sql create mode 100644 products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.cpp create mode 100644 products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.hpp create mode 100644 products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.cpp create mode 100644 products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.hpp create mode 100644 products/BellHybrid/services/db/agents/MeditationStatsAgent.cpp create mode 100644 products/BellHybrid/services/db/agents/MeditationStatsAgent.hpp create mode 100644 products/BellHybrid/services/db/databases/CMakeLists.txt create mode 100644 products/BellHybrid/services/db/databases/MeditationStatisticsDB.cpp create mode 100644 products/BellHybrid/services/db/databases/MeditationStatisticsDB.hpp create mode 100644 products/BellHybrid/services/db/databases/MeditationStatisticsTable.cpp create mode 100644 products/BellHybrid/services/db/databases/MeditationStatisticsTable.hpp create mode 100644 products/BellHybrid/services/db/include/db/MeditationStatsMessages.hpp create mode 100644 products/BellHybrid/services/db/tests/CMakeLists.txt create mode 100644 products/BellHybrid/services/db/tests/MeditationStatisticsTable_tests.cpp diff --git a/image/user/db/meditation_stats_001.sql b/image/user/db/meditation_stats_001.sql new file mode 100644 index 0000000000000000000000000000000000000000..cf6c2b56a1f5a17c67258c45553ab44161000018 --- /dev/null +++ b/image/user/db/meditation_stats_001.sql @@ -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 +); diff --git a/module-db/Database/sqlite3vfs.cpp b/module-db/Database/sqlite3vfs.cpp index 393419f0fb11dbdaac3946a87d0bcd98f1e60f04..bdb18c55cf6c7d5080e448df7873581db28651c1 100644 --- a/module-db/Database/sqlite3vfs.cpp +++ b/module-db/Database/sqlite3vfs.cpp @@ -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; + using time_point = std::chrono::time_point; + + 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; } diff --git a/module-services/service-db/DatabaseAgent.cpp b/module-services/service-db/DatabaseAgent.cpp index 538dcdddb2b96a421da06f62669dd4dd50dcb2d2..c77cdbf8882b156d02b656a3ab49200bf87017be 100644 --- a/module-services/service-db/DatabaseAgent.cpp +++ b/module-services/service-db/DatabaseAgent.cpp @@ -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; +} diff --git a/module-services/service-db/agents/settings/SettingsAgent.cpp b/module-services/service-db/agents/settings/SettingsAgent.cpp index db8d982a61b12eaba489de8d3d28954ff24de5cd..776d5147435136ce1b5b52aeed531c26929b6edd 100644 --- a/module-services/service-db/agents/settings/SettingsAgent.cpp +++ b/module-services/service-db/agents/settings/SettingsAgent.cpp @@ -28,10 +28,7 @@ SettingsAgent::SettingsAgent(sys::Service *parentService, const std::string dbNa } database = std::make_unique(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(); diff --git a/module-services/service-db/agents/settings/SettingsAgent.hpp b/module-services/service-db/agents/settings/SettingsAgent.hpp index b4eb321fc7c7b9dd35c9fa659292ca816472dab8..c61e5cfc4a6f5e3991718375edc3e313e3bb1d42 100644 --- a/module-services/service-db/agents/settings/SettingsAgent.hpp +++ b/module-services/service-db/agents/settings/SettingsAgent.hpp @@ -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; diff --git a/module-services/service-db/include/service-db/DatabaseAgent.hpp b/module-services/service-db/include/service-db/DatabaseAgent.hpp index 416f30ce96614c75a615f619c6258e35722f89cb..b1d7460cd20dba8f97ee439b04215009c7623134 100644 --- a/module-services/service-db/include/service-db/DatabaseAgent.hpp +++ b/module-services/service-db/include/service-db/DatabaseAgent.hpp @@ -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; }; diff --git a/products/BellHybrid/apps/application-bell-meditation-timer/CMakeLists.txt b/products/BellHybrid/apps/application-bell-meditation-timer/CMakeLists.txt index 914fa815be23983375d73c3eeab9248c991d74eb..5d2bc754deb43bec8033044ae86dcd2d322e8b00 100644 --- a/products/BellHybrid/apps/application-bell-meditation-timer/CMakeLists.txt +++ b/products/BellHybrid/apps/application-bell-meditation-timer/CMakeLists.txt @@ -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 diff --git a/products/BellHybrid/apps/application-bell-meditation-timer/MeditationTimer.cpp b/products/BellHybrid/apps/application-bell-meditation-timer/MeditationTimer.cpp index e5f611a5b9934a11717f08ce20fe15b18e5d0ac7..c2e6620886055f40041c364b31746b84405a2d60 100644 --- a/products/BellHybrid/apps/application-bell-meditation-timer/MeditationTimer.cpp +++ b/products/BellHybrid/apps/application-bell-meditation-timer/MeditationTimer.cpp @@ -67,7 +67,7 @@ namespace app }); windowsFactory.attach(meditation::StatisticsWindow::name, [](ApplicationCommon *app, const std::string &name) { - auto presenter = std::make_unique(); + auto presenter = std::make_unique(app); return std::make_unique(app, std::move(presenter)); }); diff --git a/products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.cpp b/products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fde7c5163503b3d2a9182f2d87f8b91dcfac218b --- /dev/null +++ b/products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.cpp @@ -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 +#include +#include + +namespace +{ + using namespace service::db::meditation_stats; + std::optional sendDBRequest(sys::Service *serv, std::shared_ptr &&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(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(Entry(utcTimestamp, duration)), service::name::db); + addRequest->execute(app, this, [this](sys::ResponseMessage *) { return true; }); + } + + std::optional Statistics::getSummary(const std::uint32_t days) + { + const auto result = sendDBRequest(app, std::make_shared(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 diff --git a/products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.hpp b/products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.hpp new file mode 100644 index 0000000000000000000000000000000000000000..458e5084ff4c9bffeddf2d26985e38b4d5368df7 --- /dev/null +++ b/products/BellHybrid/apps/application-bell-meditation-timer/models/Statistics.hpp @@ -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 + +#include +#include + +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 getSummary(std::uint32_t days); + + private: + app::ApplicationCommon *app{nullptr}; + }; + +} // namespace app::meditation::models diff --git a/products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.cpp b/products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.cpp index c11cdc8e88810b1e843b53a02b39110c70ff1880..4e8e94a185f09ecb414b33b5aed2cc19fb7500d5 100644 --- a/products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.cpp +++ b/products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.cpp @@ -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 +#include +#include +#include namespace app::meditation { - StatisticsPresenter::StatisticsPresenter() - {} + StatisticsPresenter::StatisticsPresenter(app::ApplicationCommon *app) + { + const auto model = std::make_unique(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::Items{reinterpret_cast(entry1), + reinterpret_cast(entry2), + reinterpret_cast(entry3), + reinterpret_cast(entry4)}); + } void StatisticsPresenter::eraseProviderData() - {} + { + listItemsProvider->clearData(); + } void StatisticsPresenter::loadData() {} void StatisticsPresenter::saveData() {} auto StatisticsPresenter::getPagesProvider() const -> std::shared_ptr { - return std::shared_ptr(); + 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 diff --git a/products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.hpp b/products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.hpp index 6dc3c319c707a8deb292f84ad6c9731cee12bbf4..b97635daddd52e77ed50d089faf2153b31977b1e 100644 --- a/products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.hpp +++ b/products/BellHybrid/apps/application-bell-meditation-timer/presenter/StatisticsPresenter.hpp @@ -4,14 +4,21 @@ #pragma once #include "data/Contract.hpp" + #include +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 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 listItemsProvider; }; } // namespace app::meditation diff --git a/products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.cpp b/products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.cpp new file mode 100644 index 0000000000000000000000000000000000000000..01f3b64f93733237a91901418b7da3fdb2c49220 --- /dev/null +++ b/products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.cpp @@ -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 +#include + +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 diff --git a/products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.hpp b/products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.hpp new file mode 100644 index 0000000000000000000000000000000000000000..861460cfe2d4f214657d941e9072e40682a425b7 --- /dev/null +++ b/products/BellHybrid/apps/application-bell-meditation-timer/widgets/SummaryListItem.hpp @@ -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 + +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 diff --git a/products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.cpp b/products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.cpp index 06a88d9ffc6a698f3ee92648a46bb5d7996224f0..b1364721fa5feaede35bc449faf32cf646b4703e 100644 --- a/products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.cpp +++ b/products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.cpp @@ -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 +#include #include #include #include -#include #include +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); } diff --git a/products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.hpp b/products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.hpp index 7de35fff49d2de4c32456e26d8fee47288f7bf58..2698bfbcacc676aca914cd44f969af3b603056d0 100644 --- a/products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.hpp +++ b/products/BellHybrid/apps/application-bell-meditation-timer/windows/StatisticsWindow.hpp @@ -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 presenter; }; } // namespace app::meditation diff --git a/products/BellHybrid/services/db/CMakeLists.txt b/products/BellHybrid/services/db/CMakeLists.txt index 813fdf897c86e37ad2740805f1fcd0afd670c144..a1c1a4835c5bb1cd2c0c3e5a8c8ec8b17e795cd9 100644 --- a/products/BellHybrid/services/db/CMakeLists.txt +++ b/products/BellHybrid/services/db/CMakeLists.txt @@ -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 - $ + 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 () diff --git a/products/BellHybrid/services/db/ServiceDB.cpp b/products/BellHybrid/services/db/ServiceDB.cpp index 5c2d11598fa3e2df60622eea55e2a1f7a1df161a..898d61e905caac362f101560e8c98269ee7e07ca 100644 --- a/products/BellHybrid/services/db/ServiceDB.cpp +++ b/products/BellHybrid/services/db/ServiceDB.cpp @@ -3,6 +3,8 @@ #include +#include "agents/MeditationStatsAgent.hpp" + #include #include @@ -82,9 +84,9 @@ sys::ReturnCodes ServiceDB::InitHandler() std::make_unique(multimediaFilesDB.get()); databaseAgents.emplace(std::make_unique(this, "settings_bell.db")); + databaseAgents.emplace(std::make_unique(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; + }); } diff --git a/products/BellHybrid/services/db/agents/MeditationStatsAgent.cpp b/products/BellHybrid/services/db/agents/MeditationStatsAgent.cpp new file mode 100644 index 0000000000000000000000000000000000000000..8193079358f3ba3b2cb178ce1bffb93d05c5b9a2 --- /dev/null +++ b/products/BellHybrid/services/db/agents/MeditationStatsAgent.cpp @@ -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 +#include +#include + +namespace +{ + using namespace service::db::meditation_stats; + + std::vector transformRecords(const std::vector &records) + { + std::vector 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(req)) { + db.table.add(::db::meditation_stats::TableRow{Record{}, msg->entry.timestamp, msg->entry.duration}); + } + return std::make_shared(); + } + sys::MessagePointer MeditationStats::handleGetByDays(const sys::Message *req) + { + if (auto msg = dynamic_cast(req)) { + const auto records = db.table.getByDays(msg->days); + return std::make_shared(transformRecords(records)); + } + return std::make_shared(); + } +} // namespace service::db::agents \ No newline at end of file diff --git a/products/BellHybrid/services/db/agents/MeditationStatsAgent.hpp b/products/BellHybrid/services/db/agents/MeditationStatsAgent.hpp new file mode 100644 index 0000000000000000000000000000000000000000..b0027f972f7dafe4e2894bf253a037b7f31edc2d --- /dev/null +++ b/products/BellHybrid/services/db/agents/MeditationStatsAgent.hpp @@ -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 + +#include +#include +#include + +#include + +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 diff --git a/products/BellHybrid/services/db/databases/CMakeLists.txt b/products/BellHybrid/services/db/databases/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..bf8eacc648b80dc139448e75624e03c7d4e6d849 --- /dev/null +++ b/products/BellHybrid/services/db/databases/CMakeLists.txt @@ -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 diff --git a/products/BellHybrid/services/db/databases/MeditationStatisticsDB.cpp b/products/BellHybrid/services/db/databases/MeditationStatisticsDB.cpp new file mode 100644 index 0000000000000000000000000000000000000000..4d418ec9ea1b6551136a15ab879eee3c89c204f6 --- /dev/null +++ b/products/BellHybrid/services/db/databases/MeditationStatisticsDB.cpp @@ -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 diff --git a/products/BellHybrid/services/db/databases/MeditationStatisticsDB.hpp b/products/BellHybrid/services/db/databases/MeditationStatisticsDB.hpp new file mode 100644 index 0000000000000000000000000000000000000000..4fded9d5401fc5be3b8c35f6bee1e331e9cfbd79 --- /dev/null +++ b/products/BellHybrid/services/db/databases/MeditationStatisticsDB.hpp @@ -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 + +namespace db::meditation_stats +{ + class MeditationStatisticsDB : public Database + { + public: + explicit MeditationStatisticsDB(const char *name); + + MeditationStatsTable table; + }; + +} // namespace db::meditation_stats diff --git a/products/BellHybrid/services/db/databases/MeditationStatisticsTable.cpp b/products/BellHybrid/services/db/databases/MeditationStatisticsTable.cpp new file mode 100644 index 0000000000000000000000000000000000000000..daf81817a1ca473ded9cbd8403a7af456d7fb442 --- /dev/null +++ b/products/BellHybrid/services/db/databases/MeditationStatisticsTable.cpp @@ -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 +#include +#include + +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 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 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 + { + 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(entry.duration.count())); + } + auto MeditationStatsTable::getLimitOffset(const uint32_t offset, const uint32_t limit) -> std::vector + { + 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 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 + { + return get_by_x(db, "days", days); + } +} // namespace db::meditation_stats \ No newline at end of file diff --git a/products/BellHybrid/services/db/databases/MeditationStatisticsTable.hpp b/products/BellHybrid/services/db/databases/MeditationStatisticsTable.hpp new file mode 100644 index 0000000000000000000000000000000000000000..e5a1d5c743ebff361487b5174f2eed71d61aa257 --- /dev/null +++ b/products/BellHybrid/services/db/databases/MeditationStatisticsTable.hpp @@ -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 +#include +#include + +namespace db::meditation_stats +{ + struct TableRow : public Record + { + time_t timestamp; + std::chrono::minutes duration; + }; + + enum class TableFields + { + timestamp, + duration + }; + + class MeditationStatsTable : public Table + { + 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 override; + auto getLimitOffsetByField(uint32_t offset, uint32_t limit, TableFields field, const char *str) + -> std::vector 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; + }; +} // namespace db::meditation_stats diff --git a/products/BellHybrid/services/db/include/db/MeditationStatsMessages.hpp b/products/BellHybrid/services/db/include/db/MeditationStatsMessages.hpp new file mode 100644 index 0000000000000000000000000000000000000000..54c5cbfc88ad182557d4bd8a00e98ddc043e88ca --- /dev/null +++ b/products/BellHybrid/services/db/include/db/MeditationStatsMessages.hpp @@ -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 +#include +#include +#include + +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 &entries) : entries{entries} + {} + + std::vector entries{}; + }; + + } // namespace messages +} // namespace service::db::meditation_stats \ No newline at end of file diff --git a/products/BellHybrid/services/db/tests/CMakeLists.txt b/products/BellHybrid/services/db/tests/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..cece19c8951db15db36c94b04929b495a17bce75 --- /dev/null +++ b/products/BellHybrid/services/db/tests/CMakeLists.txt @@ -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 diff --git a/products/BellHybrid/services/db/tests/MeditationStatisticsTable_tests.cpp b/products/BellHybrid/services/db/tests/MeditationStatisticsTable_tests.cpp new file mode 100644 index 0000000000000000000000000000000000000000..37cd9f9a88a161576f0f435e2337a03252d7eb44 --- /dev/null +++ b/products/BellHybrid/services/db/tests/MeditationStatisticsTable_tests.cpp @@ -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 + +#include + +#include +#include + +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 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(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; + }; +} // namespace + +TEST_CASE("Meditation statistics Table - API basic checks") +{ + TestDatabase 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 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); +} diff --git a/products/PurePhone/services/db/ServiceDB.cpp b/products/PurePhone/services/db/ServiceDB.cpp index ef3766f7b65828c168ef176e1cd3e098fac2e525..ff94504b8d7b19034654ebb755d8f94e213ad185 100644 --- a/products/PurePhone/services/db/ServiceDB.cpp +++ b/products/PurePhone/services/db/ServiceDB.cpp @@ -234,7 +234,7 @@ sys::ReturnCodes ServiceDB::InitHandler() notificationsDB = std::make_unique((purefs::dir::getUserDiskPath() / "notifications.db").c_str()); predefinedQuotesDB = std::make_unique((purefs::dir::getUserDiskPath() / "predefined_quotes.db").c_str()); customQuotesDB = std::make_unique((purefs::dir::getUserDiskPath() / "custom_quotes.db").c_str()); - multimediaFilesDB = std::make_unique( + multimediaFilesDB = std::make_unique( (purefs::dir::getUserDiskPath() / "multimedia.db").c_str()); // Create record interfaces @@ -253,7 +253,6 @@ sys::ReturnCodes ServiceDB::InitHandler() databaseAgents.emplace(std::make_unique(this, "settings_v2.db")); for (auto &dbAgent : databaseAgents) { - dbAgent->initDb(); dbAgent->registerMessages(); }