~aleteoryx/muditaos

cf019e3d9604bb7599f3bfb49d4cda47aff9815d — Alek Rudnik 4 years ago 10f2732
[EGD-6776] Music Player All Songs Window

Full MVP design.
Removed redundant Empty Window.
Added play/pause/resume/stop control.
35 files changed, 905 insertions(+), 272 deletions(-)

A art/phone/application_musicplayer/now_playing_icon_list.png
A art/phone/application_musicplayer/now_playing_icon_pause_list.png
A image/assets/images/now_playing_icon_list.vpi
A image/assets/images/now_playing_icon_pause_list.vpi
M image/assets/lang/English.json
M module-apps/application-music-player/ApplicationMusicPlayer.cpp
A module-apps/application-music-player/AudioNotificationsHandler.cpp
A module-apps/application-music-player/AudioNotificationsHandler.hpp
M module-apps/application-music-player/CMakeLists.txt
M module-apps/application-music-player/data/MusicPlayerStyle.hpp
A module-apps/application-music-player/models/SongContext.cpp
A module-apps/application-music-player/models/SongContext.hpp
M module-apps/application-music-player/models/SongsModel.cpp
M module-apps/application-music-player/models/SongsModel.hpp
M module-apps/application-music-player/models/SongsModelInterface.hpp
M module-apps/application-music-player/models/SongsRepository.cpp
M module-apps/application-music-player/models/SongsRepository.hpp
M module-apps/application-music-player/presenters/AudioOperations.cpp
M module-apps/application-music-player/presenters/AudioOperations.hpp
M module-apps/application-music-player/presenters/SongsPresenter.cpp
M module-apps/application-music-player/presenters/SongsPresenter.hpp
A module-apps/application-music-player/tests/CMakeLists.txt
A module-apps/application-music-player/tests/MockSongsRepository.hpp
A module-apps/application-music-player/tests/MockTagsFetcher.hpp
A module-apps/application-music-player/tests/unittest.cpp
A module-apps/application-music-player/tests/unittest_songrepository.cpp
A module-apps/application-music-player/tests/unittest_songsmodel.cpp
M module-apps/application-music-player/widgets/SongItem.cpp
M module-apps/application-music-player/widgets/SongItem.hpp
M module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.cpp
M module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.hpp
D module-apps/application-music-player/windows/MusicPlayerEmptyWindow.cpp
D module-apps/application-music-player/windows/MusicPlayerEmptyWindow.hpp
M module-services/service-audio/ServiceAudio.cpp
M module-services/service-audio/include/service-audio/AudioMessage.hpp
A art/phone/application_musicplayer/now_playing_icon_list.png => art/phone/application_musicplayer/now_playing_icon_list.png +0 -0
A art/phone/application_musicplayer/now_playing_icon_pause_list.png => art/phone/application_musicplayer/now_playing_icon_pause_list.png +0 -0
A image/assets/images/now_playing_icon_list.vpi => image/assets/images/now_playing_icon_list.vpi +0 -0
A image/assets/images/now_playing_icon_pause_list.vpi => image/assets/images/now_playing_icon_pause_list.vpi +0 -0
M image/assets/lang/English.json => image/assets/lang/English.json +1 -0
@@ 513,6 513,7 @@
  "app_meditation_minutes": "MINUTES",
  "app_music_player_all_songs": "All songs",
  "app_music_player_play": "PLAY",
  "app_music_player_music_library_window_name": "Music Library",
  "app_music_player_music_library": "MUSIC LIBRARY",
  "app_music_player_quit": "QUIT",
  "app_music_player_music_empty_window_notification": "Press Music Library to choose\n a song from the library",

M module-apps/application-music-player/ApplicationMusicPlayer.cpp => module-apps/application-music-player/ApplicationMusicPlayer.cpp +16 -8
@@ 2,8 2,10 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <application-music-player/ApplicationMusicPlayer.hpp>

#include "AudioNotificationsHandler.hpp"

#include <windows/MusicPlayerAllSongsWindow.hpp>
#include <windows/MusicPlayerEmptyWindow.hpp>
#include <presenters/AudioOperations.hpp>
#include <presenters/SongsPresenter.hpp>
#include <models/SongsRepository.hpp>


@@ 40,14 42,17 @@ namespace app
    {
        LOG_INFO("ApplicationMusicPlayer::create");

        auto songsRepository = std::make_unique<app::music_player::SongsRepository>(this);
        bus.channels.push_back(sys::BusChannel::ServiceAudioNotifications);

        auto tagsFetcher     = std::make_unique<app::music_player::ServiceAudioTagsFetcher>(this);
        auto songsRepository = std::make_unique<app::music_player::SongsRepository>(std::move(tagsFetcher));
        priv->songsModel     = std::make_unique<app::music_player::SongsModel>(std::move(songsRepository));
        auto audioOperations = std::make_unique<app::music_player::AudioOperations>(this);
        priv->songsPresenter =
            std::make_unique<app::music_player::SongsPresenter>(priv->songsModel, std::move(audioOperations));

        // callback used when playing state is changed
        using SongState                                 = music_player::SongsModelInterface::SongState;
        using SongState                                 = app::music_player::SongState;
        std::function<void(SongState)> autolockCallback = [this](SongState isPlaying) {
            if (isPlaying == SongState::Playing) {
                LOG_DEBUG("Preventing autolock while playing track.");


@@ 64,6 69,12 @@ namespace app
        // callback used when track is not played and we are in DetermineByAppState
        std::function<bool()> stateLockCallback = []() -> bool { return true; };
        lockPolicyHandler.setPreventsAutoLockByStateCallback(std::move(stateLockCallback));

        connect(typeid(AudioStopNotification), [&](sys::Message *msg) -> sys::MessagePointer {
            auto notification = static_cast<AudioStopNotification *>(msg);
            music_player::AudioNotificationsHandler audioNotificationHandler{priv->songsPresenter};
            return audioNotificationHandler.handleAudioStopNotification(notification);
        });
    }

    ApplicationMusicPlayer::~ApplicationMusicPlayer() = default;


@@ 96,24 107,21 @@ namespace app
        }

        createUserInterface();

        return ret;
    }

    sys::ReturnCodes ApplicationMusicPlayer::DeinitHandler()
    {
        priv->songsPresenter->getMusicPlayerItemProvider()->clearData();
        priv->songsPresenter->stop();
        return Application::DeinitHandler();
    }

    void ApplicationMusicPlayer::createUserInterface()
    {
        windowsFactory.attach(gui::name::window::all_songs_window, [&](Application *app, const std::string &name) {
        windowsFactory.attach(gui::name::window::main_window, [&](Application *app, const std::string &name) {
            return std::make_unique<gui::MusicPlayerAllSongsWindow>(app, priv->songsPresenter);
        });
        windowsFactory.attach(gui::name::window::main_window, [](Application *app, const std::string &name) {
            return std::make_unique<gui::MusicPlayerEmptyWindow>(app);
        });

        attachPopups(
            {gui::popup::ID::Volume, gui::popup::ID::Tethering, gui::popup::ID::PhoneModes, gui::popup::ID::PhoneLock});

A module-apps/application-music-player/AudioNotificationsHandler.cpp => module-apps/application-music-player/AudioNotificationsHandler.cpp +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

#include "AudioNotificationsHandler.hpp"

#include <service-audio/AudioMessage.hpp>

namespace app::music_player
{

    AudioNotificationsHandler::AudioNotificationsHandler(
        std::shared_ptr<app::music_player::SongsContract::Presenter> presenter)
        : presenter(presenter)
    {}

    sys::MessagePointer AudioNotificationsHandler::handleAudioStopNotification(
        const AudioStopNotification *notification)
    {
        if (notification == nullptr) {
            return sys::msgNotHandled();
        }

        return presenter->handleAudioStopNotifiaction(notification->token) ? sys::msgNotHandled() : sys::msgHandled();
    }

} // namespace app::music_player

A module-apps/application-music-player/AudioNotificationsHandler.hpp => module-apps/application-music-player/AudioNotificationsHandler.hpp +21 -0
@@ 0,0 1,21 @@
// 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 <presenters/SongsPresenter.hpp>

class AudioStopNotification;
namespace app::music_player
{
    class AudioNotificationsHandler
    {
      public:
        explicit AudioNotificationsHandler(std::shared_ptr<app::music_player::SongsContract::Presenter> presenter);

        sys::MessagePointer handleAudioStopNotification(const AudioStopNotification *notification);

      private:
        std::shared_ptr<app::music_player::SongsContract::Presenter> presenter;
    };
} // namespace app::music_player

M module-apps/application-music-player/CMakeLists.txt => module-apps/application-music-player/CMakeLists.txt +8 -2
@@ 13,6 13,8 @@ target_include_directories(application-music-player
target_sources(application-music-player
    PRIVATE
        ApplicationMusicPlayer.cpp
        AudioNotificationsHandler.cpp
        models/SongContext.cpp
        models/SongsModel.cpp
        models/SongsRepository.cpp
        presenters/AudioOperations.cpp


@@ 20,9 22,10 @@ target_sources(application-music-player
        widgets/Action.cpp
        widgets/SongItem.cpp
        windows/MusicPlayerAllSongsWindow.cpp
        windows/MusicPlayerEmptyWindow.cpp
    PRIVATE
        AudioNotificationsHandler.hpp
        data/MusicPlayerStyle.hpp
        models/SongContext.hpp
        models/SongsModel.hpp
        models/SongsRepository.hpp
        models/SongsModelInterface.hpp


@@ 31,7 34,6 @@ target_sources(application-music-player
        widgets/Action.hpp
        widgets/SongItem.hpp
        windows/MusicPlayerAllSongsWindow.hpp
        windows/MusicPlayerEmptyWindow.hpp
    PUBLIC
        include/application-music-player/ApplicationMusicPlayer.hpp
)


@@ 54,3 56,7 @@ target_link_libraries(application-music-player
        apps-common
        module-audio
)

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

M module-apps/application-music-player/data/MusicPlayerStyle.hpp => module-apps/application-music-player/data/MusicPlayerStyle.hpp +4 -4
@@ 101,13 101,13 @@ namespace musicPlayerStyle
        constexpr uint32_t w = style::window::default_body_width;
        constexpr uint32_t h = 100;

        constexpr uint32_t bold_text_h = 24;
        constexpr uint32_t text_h      = 22;
        constexpr uint32_t bold_text_h = 33;
        constexpr uint32_t text_h      = 33;
        constexpr uint32_t duration_w  = 50;

        constexpr int32_t topMargin   = 18;
        constexpr int32_t topMargin   = 16;
        constexpr int32_t leftMargin  = 10;
        constexpr int32_t rightMargin = 10;
        constexpr int32_t rightMargin = 4;

    } // namespace songItem


A module-apps/application-music-player/models/SongContext.cpp => module-apps/application-music-player/models/SongContext.cpp +30 -0
@@ 0,0 1,30 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SongContext.hpp"
#include <optional>

namespace app::music_player
{
    void SongContext::clear()
    {
        currentSongState = SongState::NotPlaying;
        currentFileToken = std::nullopt;
        filePath         = "";
    }

    bool SongContext::isValid() const
    {
        return (currentFileToken && currentFileToken->IsValid() && !filePath.empty());
    }

    bool SongContext::isPlaying() const
    {
        return isValid() && currentSongState == SongState::Playing;
    }

    bool SongContext::isPaused() const
    {
        return isValid() && currentSongState == SongState::NotPlaying;
    }
} // namespace app::music_player

A module-apps/application-music-player/models/SongContext.hpp => module-apps/application-music-player/models/SongContext.hpp +30 -0
@@ 0,0 1,30 @@
// 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 <Audio/decoder/Decoder.hpp>

namespace app::music_player
{

    enum class SongState
    {
        Playing,
        NotPlaying
    };

    struct SongContext
    {
      public:
        SongState currentSongState = SongState::NotPlaying;
        std::optional<audio::Token> currentFileToken;
        std::string filePath;

        void clear();

        bool isPlaying() const;
        bool isPaused() const;
        bool isValid() const;
    };

} // namespace app::music_player

M module-apps/application-music-player/models/SongsModel.cpp => module-apps/application-music-player/models/SongsModel.cpp +81 -16
@@ 2,6 2,7 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "SongsModel.hpp"
#include "Style.hpp"
#include "application-music-player/widgets/SongItem.hpp"

#include <ListView.hpp>


@@ 22,7 23,7 @@ namespace app::music_player

    auto SongsModel::getMinimalItemSpaceRequired() const -> unsigned int
    {
        return musicPlayerStyle::songItem::h;
        return musicPlayerStyle::songItem::h + style::margins::small * 2;
    }

    void SongsModel::requestRecords(const uint32_t offset, const uint32_t limit)


@@ 36,51 37,115 @@ namespace app::music_player
        return getRecord(order);
    }

    void SongsModel::createData(std::function<bool(const std::string &fileName)> func)
    void SongsModel::createData(OnShortReleaseCallback shortReleaseCallback,
                                OnLongPressCallback longPressCallback,
                                OnSetBottomBarTemporaryCallback bottomBarTemporaryMode,
                                OnRestoreBottomBarTemporaryCallback bottomBarRestoreFromTemporaryMode)
    {
        songsRepository->scanMusicFilesList();
        auto songsList = songsRepository->getMusicFilesList();
        for (const auto &song : songsList) {
            auto item = new gui::SongItem(song.artist, song.title, utils::time::Duration(song.total_duration_s).str());
            auto item = new gui::SongItem(song.artist,
                                          song.title,
                                          utils::time::Duration(song.total_duration_s).str(),
                                          bottomBarTemporaryMode,
                                          bottomBarRestoreFromTemporaryMode);

            item->activatedCallback = [=](gui::Item &) {
                func(song.filePath);
                shortReleaseCallback(song.filePath);
                return true;
            };

            item->inputCallback = [longPressCallback](gui::Item &, const gui::InputEvent &event) {
                if (event.isLongRelease(gui::KeyCode::KEY_ENTER)) {
                    longPressCallback();
                    return true;
                }
                return false;
            };

            internalData.push_back(item);
        }

        for (auto &item : internalData) {
            item->deleteByList = false;
        }
    }

        list->rebuildList();
    bool SongsModel::isSongPlaying() const noexcept
    {
        return songContext.currentSongState == SongState::Playing;
    }

    void SongsModel::clearData()
    void SongsModel::setCurrentSongState(SongState songState) noexcept
    {
        list->reset();
        songContext.currentSongState = songState;
        updateCurrentItemState();
    }

        list->rebuildList();
    std::optional<audio::Token> SongsModel::getCurrentFileToken() const noexcept
    {
        return songContext.currentFileToken;
    }

    bool SongsModel::isSongPlaying() const noexcept
    size_t SongsModel::getCurrentIndex() const
    {
        return currentSongState == SongState::Playing;
        auto index = songsRepository->getFileIndex(songContext.filePath);
        return index == std::numeric_limits<size_t>::max() ? 0 : index;
    }

    void SongsModel::setCurrentSongState(SongState songState) noexcept
    SongContext SongsModel::getCurrentSongContext() const noexcept
    {
        currentSongState = songState;
        return songContext;
    }

    std::optional<audio::Token> SongsModel::getCurrentFileToken() const noexcept
    void SongsModel::setCurrentSongContext(SongContext context)
    {
        return currentFileToken;
        using namespace gui;
        clearCurrentItemState();

        songContext = context;

        updateCurrentItemState();
    }

    void SongsModel::setCurrentFileToken(std::optional<audio::Token> token) noexcept
    void SongsModel::clearCurrentSongContext()
    {
        currentFileToken = token;
        clearCurrentItemState();
        songContext.clear();
    }

    void SongsModel::clearCurrentItemState()
    {
        using namespace gui;
        const auto songIndex = getCurrentIndex();
        if (songIndex < internalData.size()) {
            internalData[songIndex]->setState(SongItem::ItemState::None);
        }
    }

    void SongsModel::updateCurrentItemState()
    {
        using namespace gui;
        const auto songIndex = getCurrentIndex();
        if (songIndex >= internalData.size()) {
            return;
        }

        if (songContext.isPlaying()) {
            internalData[songIndex]->setState(SongItem::ItemState::Playing);
        }
        else if (songContext.isPaused()) {
            internalData[songIndex]->setState(SongItem::ItemState::Paused);
        }
        else {
            internalData[songIndex]->setState(SongItem::ItemState::None);
        }
    }

    void SongsModel::clearData()
    {
        list->reset();
        eraseInternalData();
    }
} // namespace app::music_player

M module-apps/application-music-player/models/SongsModel.hpp => module-apps/application-music-player/models/SongsModel.hpp +18 -7
@@ 5,6 5,7 @@

#include "module-apps/application-music-player/data/MusicPlayerStyle.hpp"

#include "SongContext.hpp"
#include "SongsRepository.hpp"
#include "SongsModelInterface.hpp"



@@ 17,8 18,10 @@ namespace app::music_player
      public:
        explicit SongsModel(std::shared_ptr<AbstractSongsRepository> songsRepository);

        void clearData();
        void createData(std::function<bool(const std::string &fileName)>) override;
        void createData(OnShortReleaseCallback shortReleaseCallback,
                        OnLongPressCallback longPressCallback,
                        OnSetBottomBarTemporaryCallback bottomBarTemporaryMode,
                        OnRestoreBottomBarTemporaryCallback bottomBarRestoreFromTemporaryMode) override;

        [[nodiscard]] auto requestRecordsCount() -> unsigned int override;



@@ 26,17 29,25 @@ namespace app::music_player

        auto getItem(gui::Order order) -> gui::ListItem * override;

        void requestRecords(const uint32_t offset, const uint32_t limit) override;
        void requestRecords(uint32_t offset, uint32_t limit) override;

        size_t getCurrentIndex() const override;

        bool isSongPlaying() const noexcept override;
        void setCurrentSongState(SongState songState) noexcept override;
        std::optional<audio::Token> getCurrentFileToken() const noexcept override;
        void setCurrentFileToken(std::optional<audio::Token> token) noexcept override;

      protected:
        SongState currentSongState = SongState::NotPlaying;
        SongContext getCurrentSongContext() const noexcept override;
        void setCurrentSongContext(SongContext context) override;
        void clearCurrentSongContext() override;

        void clearData() override;

      private:
        void clearCurrentItemState();
        void updateCurrentItemState();
        SongContext songContext;

        std::optional<audio::Token> currentFileToken;
        std::shared_ptr<AbstractSongsRepository> songsRepository;
    };
} // namespace app::music_player

M module-apps/application-music-player/models/SongsModelInterface.hpp => module-apps/application-music-player/models/SongsModelInterface.hpp +17 -9
@@ 3,18 3,30 @@

#pragma once

#include "SongContext.hpp"
#include <widgets/SongItem.hpp>
#include <InternalModel.hpp>
#include <ListItemProvider.hpp>
#include <apps-common/Application.hpp>

namespace app::music_player
{
    class SongsListItemProvider : public app::InternalModel<gui::ListItem *>, public gui::ListItemProvider
    class SongsListItemProvider : public app::InternalModel<gui::SongItem *>, public gui::ListItemProvider
    {
      public:
        using OnShortReleaseCallback              = std::function<bool(const std::string &fileName)>;
        using OnLongPressCallback                 = std::function<void()>;
        using OnSetBottomBarTemporaryCallback     = std::function<void(const UTF8 &)>;
        using OnRestoreBottomBarTemporaryCallback = std::function<void()>;

        virtual ~SongsListItemProvider() noexcept = default;

        virtual void createData(std::function<bool(const std::string &fileName)>) = 0;
        virtual void createData(OnShortReleaseCallback shortReleaseCallback,
                                OnLongPressCallback longPressCallback,
                                OnSetBottomBarTemporaryCallback bottomBarTemporaryMode,
                                OnRestoreBottomBarTemporaryCallback bottomBarRestoreFromTemporaryMode) = 0;
        virtual size_t getCurrentIndex() const                                                         = 0;
        virtual void clearData()                                                                       = 0;
    };

    class SongsModelInterface : public SongsListItemProvider


@@ 22,15 34,11 @@ namespace app::music_player
      public:
        virtual ~SongsModelInterface() noexcept = default;

        enum class SongState
        {
            Playing,
            NotPlaying
        };

        virtual bool isSongPlaying() const noexcept                              = 0;
        virtual void setCurrentSongState(SongState songState) noexcept           = 0;
        virtual std::optional<audio::Token> getCurrentFileToken() const noexcept = 0;
        virtual void setCurrentFileToken(std::optional<audio::Token>) noexcept   = 0;
        virtual SongContext getCurrentSongContext() const noexcept               = 0;
        virtual void setCurrentSongContext(SongContext context)                  = 0;
        virtual void clearCurrentSongContext()                                   = 0;
    };
} // namespace app::music_player

M module-apps/application-music-player/models/SongsRepository.cpp => module-apps/application-music-player/models/SongsRepository.cpp +32 -12
@@ 3,47 3,67 @@

#include "SongsRepository.hpp"

#include <algorithm>
#include <log.hpp>
#include <service-audio/AudioServiceAPI.hpp>
#include <service-audio/AudioServiceName.hpp>
#include <time/ScopedTime.hpp>
#include <purefs/filesystem_paths.hpp>
#include <service-audio/AudioMessage.hpp>

#include <filesystem>

namespace app::music_player
{
    SongsRepository::SongsRepository(Application *application) : application(application)
    ServiceAudioTagsFetcher::ServiceAudioTagsFetcher(Application *application) : application(application)
    {}

    std::vector<audio::Tags> SongsRepository::getMusicFilesList()
    std::optional<audio::Tags> ServiceAudioTagsFetcher::getFileTags(const std::string &filePath) const
    {
        const auto musicFolder = purefs::dir::getUserDiskPath() / "music";
        std::vector<audio::Tags> musicFiles;
        LOG_INFO("Scanning music folder: %s", musicFolder.c_str());
        return AudioServiceAPI::GetFileTags(application, filePath);
    }

    SongsRepository::SongsRepository(std::unique_ptr<AbstractTagsFetcher> tagsFetcher, std::string musicFolderName)
        : tagsFetcher(std::move(tagsFetcher)), musicFolderName(std::move(musicFolderName))
    {}

    void SongsRepository::scanMusicFilesList()
    {
        musicFiles.clear();

        LOG_INFO("Scanning music folder: %s", musicFolderName.c_str());
        {
            auto time = utils::time::Scoped("fetch tags time");
            for (const auto &entry : std::filesystem::directory_iterator(musicFolder)) {
            for (const auto &entry : std::filesystem::directory_iterator(musicFolderName)) {
                if (!std::filesystem::is_directory(entry)) {
                    const auto &filePath = entry.path();
                    const auto fileTags  = getFileTags(filePath);
                    const auto fileTags  = tagsFetcher->getFileTags(filePath);
                    if (fileTags) {
                        musicFiles.push_back(*fileTags);
                        LOG_DEBUG(" - file %s found", entry.path().c_str());
                    }
                    else {
                        LOG_ERROR("Not an audio file %s", entry.path().c_str());
                        LOG_ERROR("Scanned not an audio file, skipped");
                    }
                }
            }
        }
        LOG_INFO("Total number of music files found: %u", static_cast<unsigned int>(musicFiles.size()));
    }

    std::vector<audio::Tags> SongsRepository::getMusicFilesList() const
    {
        return musicFiles;
    }

    std::optional<audio::Tags> SongsRepository::getFileTags(const std::string &filePath)
    std::size_t SongsRepository::getFileIndex(const std::string &filePath) const
    {
        return AudioServiceAPI::GetFileTags(application, filePath);
        auto it = std::find_if(musicFiles.begin(), musicFiles.end(), [filePath](const auto &musicFile) {
            return musicFile.filePath == filePath;
        });

        if (it != musicFiles.end()) {
            return std::distance(musicFiles.begin(), it);
        }

        return std::numeric_limits<size_t>::max();
    }
} // namespace app::music_player

M module-apps/application-music-player/models/SongsRepository.hpp => module-apps/application-music-player/models/SongsRepository.hpp +40 -6
@@ 5,27 5,61 @@

#include <apps-common/Application.hpp>
#include <Audio/decoder/Decoder.hpp>
#include <purefs/filesystem_paths.hpp>

#include <memory>
#include <optional>
#include <string>
#include <vector>

#include <cstddef>

namespace app::music_player
{
    class AbstractTagsFetcher
    {
      public:
        virtual ~AbstractTagsFetcher() noexcept = default;

        virtual std::optional<audio::Tags> getFileTags(const std::string &filePath) const = 0;
    };

    class ServiceAudioTagsFetcher : public AbstractTagsFetcher
    {
      public:
        explicit ServiceAudioTagsFetcher(Application *application);

        std::optional<audio::Tags> getFileTags(const std::string &filePath) const final;

      private:
        Application *application = nullptr;
    };

    class AbstractSongsRepository
    {
      public:
        virtual ~AbstractSongsRepository() noexcept = default;

        virtual std::vector<audio::Tags> getMusicFilesList()                        = 0;
        virtual std::optional<audio::Tags> getFileTags(const std::string &filePath) = 0;
        virtual void scanMusicFilesList()                                   = 0;
        virtual std::vector<audio::Tags> getMusicFilesList() const          = 0;
        virtual std::size_t getFileIndex(const std::string &filePath) const = 0;
    };

    class SongsRepository : public AbstractSongsRepository
    {
        static constexpr auto musicSubfolderName = "music";

      public:
        explicit SongsRepository(Application *application);
        explicit SongsRepository(std::unique_ptr<AbstractTagsFetcher> tagsFetcher,
                                 std::string musicFolderName = purefs::dir::getUserDiskPath() / musicSubfolderName);

        std::vector<audio::Tags> getMusicFilesList() override;
        std::optional<audio::Tags> getFileTags(const std::string &filePath) override;
        void scanMusicFilesList() override;
        std::vector<audio::Tags> getMusicFilesList() const override;
        std::size_t getFileIndex(const std::string &filePath) const override;

      private:
        Application *application = nullptr;
        std::unique_ptr<AbstractTagsFetcher> tagsFetcher;
        std::string musicFolderName;
        std::vector<audio::Tags> musicFiles;
    };
} // namespace app::music_player

M module-apps/application-music-player/presenters/AudioOperations.cpp => module-apps/application-music-player/presenters/AudioOperations.cpp +33 -10
@@ 28,7 28,7 @@ namespace app::music_player
                return false;
            }
            if (callback) {
                callback(result->token);
                callback(result->retCode, result->token);
            }
            return true;
        };


@@ 36,26 36,49 @@ namespace app::music_player
        return true;
    }

    bool AudioOperations::pause(const audio::Token &token)
    bool AudioOperations::pause(const audio::Token &token, const OnPauseCallback &callback)
    {
        return AudioServiceAPI::Pause(application, token);
        auto msg  = std::make_unique<AudioPauseRequest>(token);
        auto task = app::AsyncRequest::createFromMessage(std::move(msg), service::name::audio);
        auto cb   = [callback](auto response) {
            auto result = dynamic_cast<AudioPauseResponse *>(response);
            if (result == nullptr) {
                return false;
            }
            if (callback) {
                callback(result->retCode, result->token);
            }
            return true;
        };
        task->execute(application, this, cb);
        return true;
    }
    bool AudioOperations::resume(const audio::Token &token)
    bool AudioOperations::resume(const audio::Token &token, const OnResumeCallback &callback)
    {
        return AudioServiceAPI::Resume(application, token);
        auto msg  = std::make_unique<AudioResumeRequest>(token);
        auto task = app::AsyncRequest::createFromMessage(std::move(msg), service::name::audio);
        auto cb   = [callback](auto response) {
            auto result = dynamic_cast<AudioResumeResponse *>(response);
            if (result == nullptr) {
                return false;
            }
            if (callback) {
                callback(result->retCode, result->token);
            }
            return true;
        };
        task->execute(application, this, cb);
        return true;
    }
    bool AudioOperations::stop(const audio::Token &token, const OnStopCallback &callback)
    bool AudioOperations::stop(const audio::Token &token, [[maybe_unused]] const OnStopCallback &callback)
    {
        auto msg  = std::make_unique<AudioStopRequest>(token);
        auto task = app::AsyncRequest::createFromMessage(std::move(msg), service::name::audio);
        auto cb   = [callback](auto response) {
        auto cb   = [](auto response) {
            auto result = dynamic_cast<AudioStopResponse *>(response);
            if (result == nullptr) {
                return false;
            }
            if (callback) {
                callback(result->token);
            }
            return true;
        };
        task->execute(application, this, cb);

M module-apps/application-music-player/presenters/AudioOperations.hpp => module-apps/application-music-player/presenters/AudioOperations.hpp +10 -8
@@ 13,15 13,17 @@ namespace app::music_player
    class AbstractAudioOperations
    {
      public:
        using OnPlayCallback = std::function<void(audio::Token token)>;
        using OnStopCallback = OnPlayCallback;
        using OnPlayCallback   = std::function<void(audio::RetCode retCode, audio::Token token)>;
        using OnStopCallback   = OnPlayCallback;
        using OnPauseCallback  = OnPlayCallback;
        using OnResumeCallback = OnPlayCallback;

        virtual ~AbstractAudioOperations() noexcept = default;

        virtual bool play(const std::string &filePath, const OnPlayCallback &callback) = 0;
        virtual bool pause(const audio::Token &token)                                  = 0;
        virtual bool resume(const audio::Token &token)                                 = 0;
        virtual bool stop(const audio::Token &token, const OnStopCallback &callback)   = 0;
        virtual bool play(const std::string &filePath, const OnPlayCallback &callback)   = 0;
        virtual bool pause(const audio::Token &token, const OnPauseCallback &callback)   = 0;
        virtual bool resume(const audio::Token &token, const OnResumeCallback &callback) = 0;
        virtual bool stop(const audio::Token &token, const OnStopCallback &callback)     = 0;
    };

    class AudioOperations : public AbstractAudioOperations, public app::AsyncCallbackReceiver


@@ 30,8 32,8 @@ namespace app::music_player
        explicit AudioOperations(Application *application);

        bool play(const std::string &filePath, const OnPlayCallback &callback) override;
        bool pause(const audio::Token &token) override;
        bool resume(const audio::Token &token) override;
        bool pause(const audio::Token &token, const OnPauseCallback &callback) override;
        bool resume(const audio::Token &token, const OnResumeCallback &callback) override;
        bool stop(const audio::Token &token, const OnStopCallback &callback) override;

      private:

M module-apps/application-music-player/presenters/SongsPresenter.cpp => module-apps/application-music-player/presenters/SongsPresenter.cpp +105 -36
@@ 3,10 3,10 @@

#include "SongsPresenter.hpp"

#include <service-audio/AudioMessage.hpp>

namespace app::music_player
{
    using SongState = SongsModelInterface::SongState;

    SongsPresenter::SongsPresenter(std::shared_ptr<app::music_player::SongsModelInterface> songsModelInterface,
                                   std::unique_ptr<AbstractAudioOperations> &&audioOperations)
        : songsModelInterface{std::move(songsModelInterface)}, audioOperations{std::move(audioOperations)}


@@ 17,31 17,54 @@ namespace app::music_player
        return songsModelInterface;
    }

    void SongsPresenter::createData(std::function<bool(const std::string &fileName)> fn)
    void SongsPresenter::createData()
    {
        songsModelInterface->createData(fn);
        songsModelInterface->createData([this](const std::string &fileName) { return requestAudioOperation(fileName); },
                                        [this]() { stop(); },
                                        [this](const UTF8 &text) { setViewBottomBarTemporaryMode(text); },
                                        [this]() { restoreViewBottomBarFromTemporaryMode(); });
        updateViewSongState();
    }

    bool SongsPresenter::play(const std::string &filePath)
    {
        songsModelInterface->setCurrentSongState(SongState::Playing);
        if (changePlayingStateCallback != nullptr) {
            changePlayingStateCallback(SongState::Playing);
        }

        return audioOperations->play(filePath,
                                     [this](audio::Token token) { songsModelInterface->setCurrentFileToken(token); });
        return audioOperations->play(filePath, [this, filePath](audio::RetCode retCode, audio::Token token) {
            if (retCode != audio::RetCode::Success || !token.IsValid()) {
                LOG_ERROR("Playback audio operation failed, retcode = %s, token validity = %d",
                          str(retCode).c_str(),
                          token.IsValid());
                return;
            }
            SongContext songToken{SongState::Playing, token, filePath};
            songsModelInterface->setCurrentSongContext(songToken);
            if (changePlayingStateCallback != nullptr) {
                changePlayingStateCallback(SongState::Playing);
            }
            updateViewSongState();
        });
    }

    bool SongsPresenter::pause()
    {
        auto currentFileToken = songsModelInterface->getCurrentFileToken();
        if (currentFileToken) {
            songsModelInterface->setCurrentSongState(SongState::NotPlaying);
            if (changePlayingStateCallback != nullptr) {
                changePlayingStateCallback(SongState::NotPlaying);
            }
            return audioOperations->pause(currentFileToken.value());
            return audioOperations->pause(currentFileToken.value(), [this](audio::RetCode retCode, audio::Token token) {
                if (retCode != audio::RetCode::Success || !token.IsValid()) {
                    LOG_ERROR("Pause audio operation failed, retcode = %s, token validity = %d",
                              str(retCode).c_str(),
                              token.IsValid());
                    return;
                }
                if (token != songsModelInterface->getCurrentFileToken()) {
                    LOG_ERROR("Pause audio operation failed, wrong token");
                    return;
                }
                songsModelInterface->setCurrentSongState(SongState::NotPlaying);
                if (changePlayingStateCallback != nullptr) {
                    changePlayingStateCallback(SongState::NotPlaying);
                }
                updateViewSongState();
            });
        }
        return false;
    }


@@ 50,11 73,24 @@ namespace app::music_player
    {
        auto currentFileToken = songsModelInterface->getCurrentFileToken();
        if (currentFileToken) {
            songsModelInterface->setCurrentSongState(SongState::Playing);
            if (changePlayingStateCallback != nullptr) {
                changePlayingStateCallback(SongState::Playing);
            }
            return audioOperations->resume(currentFileToken.value());
            return audioOperations->resume(
                currentFileToken.value(), [this](audio::RetCode retCode, audio::Token token) {
                    if (retCode != audio::RetCode::Success || !token.IsValid()) {
                        LOG_ERROR("Resume audio operation failed, retcode = %s, token validity = %d",
                                  str(retCode).c_str(),
                                  token.IsValid());
                        return;
                    }
                    if (token != songsModelInterface->getCurrentFileToken()) {
                        LOG_ERROR("Resume audio operation failed, wrong token");
                        return;
                    }
                    songsModelInterface->setCurrentSongState(SongState::Playing);
                    if (changePlayingStateCallback != nullptr) {
                        changePlayingStateCallback(SongState::Playing);
                    }
                    updateViewSongState();
                });
        }
        return false;
    }


@@ 63,34 99,67 @@ namespace app::music_player
    {
        auto currentFileToken = songsModelInterface->getCurrentFileToken();
        if (currentFileToken) {
            songsModelInterface->setCurrentSongState(SongState::NotPlaying);
            return audioOperations->stop(currentFileToken.value(), [](audio::RetCode, audio::Token) {
                // The answer will come via multicast and will be handled in the application
            });
        }
        return false;
    }

    void SongsPresenter::setPlayingStateCallback(std::function<void(SongState)> cb)
    {
        changePlayingStateCallback = std::move(cb);
    }

    bool SongsPresenter::handleAudioStopNotifiaction(audio::Token token)
    {
        if (token == songsModelInterface->getCurrentFileToken()) {
            songsModelInterface->clearCurrentSongContext();
            if (changePlayingStateCallback != nullptr) {
                changePlayingStateCallback(SongState::NotPlaying);
            }

            return audioOperations->stop(currentFileToken.value(), [this](audio::Token token) {
                if (token == songsModelInterface->getCurrentFileToken()) {
                    songsModelInterface->setCurrentFileToken(std::nullopt);
                    songsModelInterface->setCurrentSongState(SongState::NotPlaying);
                }
            });
            updateViewSongState();
            refreshView();
            return true;
        }
        return false;
    }

    void SongsPresenter::togglePlaying()
    bool SongsPresenter::requestAudioOperation(const std::string &filePath)
    {
        if (songsModelInterface->isSongPlaying()) {
            pause();
        auto currentSongContext = songsModelInterface->getCurrentSongContext();

        if (currentSongContext.isValid() && (filePath.empty() || currentSongContext.filePath == filePath)) {
            return currentSongContext.isPlaying() ? pause() : resume();
        }
        else {
            resume();
        return play(filePath);
    }

    void SongsPresenter::setViewBottomBarTemporaryMode(const std::string &text)
    {
        if (auto view = getView(); view != nullptr) {
            view->setBottomBarTemporaryMode(text);
        }
    }

    void SongsPresenter::setPlayingStateCallback(std::function<void(SongState)> cb)
    void SongsPresenter::restoreViewBottomBarFromTemporaryMode()
    {
        changePlayingStateCallback = std::move(cb);
        if (auto view = getView(); view != nullptr) {
            view->restoreFromBottomBarTemporaryMode();
        }
    }

    void SongsPresenter::updateViewSongState()
    {
        if (auto view = getView(); view != nullptr) {
            view->updateSongsState();
        }
    }

    void SongsPresenter::refreshView()
    {
        if (auto view = getView(); view != nullptr) {
            view->refreshWindow();
        }
    }
} // namespace app::music_player

M module-apps/application-music-player/presenters/SongsPresenter.hpp => module-apps/application-music-player/presenters/SongsPresenter.hpp +22 -8
@@ 17,23 17,29 @@ namespace app::music_player
        class View
        {
          public:
            virtual ~View() noexcept = default;
            virtual ~View() noexcept                                        = default;
            virtual void updateSongsState()                                 = 0;
            virtual void refreshWindow()                                    = 0;
            virtual void setBottomBarTemporaryMode(const std::string &text) = 0;
            virtual void restoreFromBottomBarTemporaryMode()                = 0;
        };
        class Presenter : public BasePresenter<SongsContract::View>
        {
          public:
            using OnPlayingStateChangeCallback = std::function<void(SongState)>;

            virtual ~Presenter() noexcept = default;

            virtual std::shared_ptr<SongsListItemProvider> getMusicPlayerItemProvider() const = 0;
            virtual void createData(std::function<bool(const std::string &fileName)>)         = 0;
            virtual void createData()                                                         = 0;

            virtual bool play(const std::string &filePath) = 0;
            virtual bool pause()                           = 0;
            virtual bool resume()                          = 0;
            virtual bool stop()                            = 0;
            virtual void togglePlaying()                   = 0;

            virtual void setPlayingStateCallback(std::function<void(SongsModelInterface::SongState)> cb) = 0;
            virtual void setPlayingStateCallback(OnPlayingStateChangeCallback cb) = 0;
            virtual bool handleAudioStopNotifiaction(audio::Token token)          = 0;
        };
    };



@@ 45,18 51,26 @@ namespace app::music_player

        std::shared_ptr<SongsListItemProvider> getMusicPlayerItemProvider() const override;

        void createData(std::function<bool(const std::string &fileName)>) override;
        void createData() override;

        bool play(const std::string &filePath) override;
        bool pause() override;
        bool resume() override;
        bool stop() override;
        void togglePlaying() override;
        void setPlayingStateCallback(std::function<void(SongsModelInterface::SongState)> cb) override;

        void setPlayingStateCallback(std::function<void(SongState)> cb) override;
        bool handleAudioStopNotifiaction(audio::Token token) override;

      private:
        void updateViewSongState();
        void refreshView();

        /// Request state dependant audio operation
        bool requestAudioOperation(const std::string &filePath = "");
        void setViewBottomBarTemporaryMode(const std::string &text);
        void restoreViewBottomBarFromTemporaryMode();
        std::shared_ptr<SongsModelInterface> songsModelInterface;
        std::unique_ptr<AbstractAudioOperations> audioOperations;
        std::function<void(SongsModelInterface::SongState)> changePlayingStateCallback = nullptr;
        std::function<void(SongState)> changePlayingStateCallback = nullptr;
    };
} // namespace app::music_player

A module-apps/application-music-player/tests/CMakeLists.txt => module-apps/application-music-player/tests/CMakeLists.txt +12 -0
@@ 0,0 1,12 @@
add_gtest_executable(
    NAME 
        app-music-player
    SRCS
        unittest.cpp
        unittest_songrepository.cpp
        unittest_songsmodel.cpp
    LIBS
        application-music-player
    INCLUDE
        ${CMAKE_CURRENT_LIST_DIR}/..
)

A module-apps/application-music-player/tests/MockSongsRepository.hpp => module-apps/application-music-player/tests/MockSongsRepository.hpp +24 -0
@@ 0,0 1,24 @@
// 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 <gtest/gtest.h>
#include <gmock/gmock.h>

#include <models/SongsRepository.hpp>

#include <optional>
#include <string>
#include <vector>

namespace testing::app::music_player
{
    class MockSongsRepository : public ::app::music_player::AbstractSongsRepository
    {
      public:
        MOCK_METHOD(void, scanMusicFilesList, (), (override));
        MOCK_METHOD(std::vector<audio::Tags>, getMusicFilesList, (), (const override));
        MOCK_METHOD(std::size_t, getFileIndex, (const std::string &filePath), (const override));
    };
}; // namespace testing::app::music_player

A module-apps/application-music-player/tests/MockTagsFetcher.hpp => module-apps/application-music-player/tests/MockTagsFetcher.hpp +20 -0
@@ 0,0 1,20 @@
// 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 <gtest/gtest.h>
#include <gmock/gmock.h>

#include <models/SongsRepository.hpp>

#include <optional>

namespace testing::app::music_player
{
    class MockTagsFetcher : public ::app::music_player::AbstractTagsFetcher
    {
      public:
        MOCK_METHOD(std::optional<audio::Tags>, getFileTags, (const std::string &filePath), (const override));
    };
}; // namespace testing::app::music_player

A module-apps/application-music-player/tests/unittest.cpp => module-apps/application-music-player/tests/unittest.cpp +10 -0
@@ 0,0 1,10 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <gtest/gtest.h>

int main(int argc, char **argv)
{
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

A module-apps/application-music-player/tests/unittest_songrepository.cpp => module-apps/application-music-player/tests/unittest_songrepository.cpp +153 -0
@@ 0,0 1,153 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include "MockTagsFetcher.hpp"

#include <models/SongsRepository.hpp>

#include <filesystem>
#include <fstream>
#include <stdexcept>

using ::testing::Return;
using ::testing::app::music_player::MockTagsFetcher;
namespace fs = std::filesystem;

constexpr auto testDir  = "appmusic-test";
constexpr auto emptyDir = "empty";
constexpr auto musicDir = "music";
constexpr auto bazDir   = "bazdir";
auto testDirPath        = fs::path(testDir);
auto emptyDirPath       = testDirPath / emptyDir;
auto musicDirPath       = testDirPath / musicDir;
auto bazDirPath         = musicDirPath / bazDir;

class SongsRepositoryFixture : public ::testing::Test
{
  protected:
    static void SetUpTestSuite()
    {
        if (fs::exists(testDirPath)) {
            TearDownTestSuite();
        }

        fs::create_directory(testDirPath);
        fs::create_directory(emptyDirPath);
        fs::create_directory(musicDirPath);

        createFile(musicDirPath / "foo");
        createFile(musicDirPath / "bar");

        fs::create_directory(bazDirPath);

        createFile(bazDirPath / "baz");

        fs::create_directory(musicDirPath / "bazzinga");
    }

    static void createFile(const std::string &path)
    {
        std::ofstream file(path);
        file << "app music test file";
    }

    static void TearDownTestSuite()
    {
        fs::remove_all(testDir);
    }

    auto getMockedRepository(const std::string &directoryToScan)
    {
        return std::make_unique<app::music_player::SongsRepository>(std::make_unique<MockTagsFetcher>(),
                                                                    directoryToScan);
    }
};

TEST_F(SongsRepositoryFixture, LazyInit)
{
    auto repo       = getMockedRepository(testDir);
    auto musicFiles = repo->getMusicFilesList();
    EXPECT_EQ(musicFiles.size(), 0);
}

TEST_F(SongsRepositoryFixture, Empty)
{
    auto repo = getMockedRepository(testDirPath / emptyDir);
    repo->scanMusicFilesList();

    auto musicFiles = repo->getMusicFilesList();
    EXPECT_EQ(musicFiles.size(), 0);
}

TEST_F(SongsRepositoryFixture, ScanEmptyFiles)
{
    auto tagsFetcherMock = std::make_unique<MockTagsFetcher>();
    auto rawMock         = tagsFetcherMock.get();
    auto repo = std::make_unique<app::music_player::SongsRepository>(std::move(tagsFetcherMock), musicDirPath);

    EXPECT_CALL(*rawMock, getFileTags).Times(2);
    repo->scanMusicFilesList();

    auto musicFiles = repo->getMusicFilesList();
    EXPECT_EQ(musicFiles.size(), 0);
}

TEST_F(SongsRepositoryFixture, ScanWithTagsReturn)
{
    auto tagsFetcherMock = std::make_unique<MockTagsFetcher>();
    auto rawMock         = tagsFetcherMock.get();
    auto repo = std::make_unique<app::music_player::SongsRepository>(std::move(tagsFetcherMock), musicDirPath);

    auto fooTags = ::audio::Tags();
    auto barTags = ::audio::Tags();

    fooTags.title = "foo";
    barTags.title = "bar";

    ON_CALL(*rawMock, getFileTags(fs::path(musicDirPath / "foo").c_str())).WillByDefault(Return(fooTags));
    ON_CALL(*rawMock, getFileTags(fs::path(musicDirPath / "bar").c_str())).WillByDefault(Return(barTags));
    EXPECT_CALL(*rawMock, getFileTags).Times(2);
    repo->scanMusicFilesList();

    auto musicFiles = repo->getMusicFilesList();
    EXPECT_EQ(musicFiles.size(), 2);
}

TEST_F(SongsRepositoryFixture, FileIndex)
{
    auto tagsFetcherMock = std::make_unique<MockTagsFetcher>();
    auto rawMock         = tagsFetcherMock.get();
    auto repo = std::make_unique<app::music_player::SongsRepository>(std::move(tagsFetcherMock), musicDirPath);

    auto fooPath = musicDirPath / "foo";
    auto barPath = musicDirPath / "bar";

    auto fooTags = ::audio::Tags();
    auto barTags = ::audio::Tags();

    fooTags.title    = "foo";
    fooTags.filePath = fooPath.c_str();

    barTags.title    = "bar";
    barTags.filePath = barPath.c_str();

    ON_CALL(*rawMock, getFileTags(fs::path(musicDirPath / "foo").c_str())).WillByDefault(Return(fooTags));
    ON_CALL(*rawMock, getFileTags(fs::path(musicDirPath / "bar").c_str())).WillByDefault(Return(barTags));
    EXPECT_CALL(*rawMock, getFileTags).Times(2);
    repo->scanMusicFilesList();

    auto fooIndex = repo->getFileIndex(fooPath);
    auto barIndex = repo->getFileIndex(barPath);

    EXPECT_NE(fooIndex, static_cast<std::size_t>(-1));
    EXPECT_LT(fooIndex, 2);

    EXPECT_NE(barIndex, static_cast<std::size_t>(-1));
    EXPECT_LT(barIndex, 2);

    auto bazIndex = repo->getFileIndex("baz");
    EXPECT_EQ(bazIndex, static_cast<std::size_t>(-1));
}

A module-apps/application-music-player/tests/unittest_songsmodel.cpp => module-apps/application-music-player/tests/unittest_songsmodel.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 <gtest/gtest.h>

#include "MockSongsRepository.hpp"

#include <models/SongsModel.hpp>

#include <memory>
#include <optional>

using ::app::music_player::SongsModel;
using ::testing::Return;
using ::testing::app::music_player::MockSongsRepository;

TEST(SongsModel, Init)
{
    auto mockRepo = std::make_shared<MockSongsRepository>();
    auto model    = SongsModel(mockRepo);

    EXPECT_EQ(model.requestRecordsCount(), 0);
    EXPECT_FALSE(model.isSongPlaying());
}

TEST(SongsModel, EmptyContext)
{
    auto mockRepo = std::make_shared<MockSongsRepository>();
    auto model    = SongsModel(mockRepo);

    auto ctx = model.getCurrentSongContext();

    EXPECT_EQ(ctx.currentFileToken, std::nullopt);
    EXPECT_TRUE(ctx.filePath.empty());
    EXPECT_EQ(ctx.currentSongState, app::music_player::SongState::NotPlaying);
}

TEST(SongsModel, createDataNoSongs)
{
    auto mockRepo = std::make_shared<MockSongsRepository>();
    auto model    = SongsModel(mockRepo);

    EXPECT_CALL(*mockRepo, scanMusicFilesList);
    EXPECT_CALL(*mockRepo, getMusicFilesList).WillRepeatedly(Return(std::vector<audio::Tags>()));
    model.createData([](const std::string &) { return true; }, []() {}, [](const UTF8 &) {}, []() {});
}

M module-apps/application-music-player/widgets/SongItem.cpp => module-apps/application-music-player/widgets/SongItem.cpp +59 -6
@@ 2,13 2,19 @@
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "module-apps/application-music-player/widgets/SongItem.hpp"
#include <i18n/i18n.hpp>

namespace gui
{

    using namespace musicPlayerStyle;

    SongItem::SongItem(const std::string &authorName, const std::string &songName, const std::string &duration)
    SongItem::SongItem(const std::string &authorName,
                       const std::string &songName,
                       const std::string &duration,
                       std::function<void(const UTF8 &)> setBtBarCallback,
                       std::function<void()> restoreBtBarCallback)
        : bottomBarTemporaryMode(setBtBarCallback), bottomBarRestoreFromTemporaryMode(restoreBtBarCallback)
    {
        setMinimumSize(songItem::w, songItem::h);
        setMargins(Margins(0, style::margins::small, 0, style::margins::small));


@@ 44,15 50,14 @@ namespace gui
        songText->setMargins(Margins(songItem::leftMargin, 0, 0, 0));
        songText->setEdges(RectangleEdge::None);
        songText->setUnderline(false);
        songText->setFont(style::window::font::verysmallbold);
        songText->setFont(style::window::font::bigbold);
        songText->setAlignment(Alignment(gui::Alignment::Horizontal::Left, gui::Alignment::Vertical::Center));
        songText->setEditMode(EditMode::Browse);
        songText->setText(songName);

        playedSong = new ImageBox(secondHBox, 0, 0, 0, 0, new Image("messages_error_W_M"));
        playedSong->setMinimumSize(songItem::duration_w, songItem::text_h);
        playedSong = new Image(secondHBox, 0, 0, "");
        playedSong->setAlignment(Alignment(gui::Alignment::Horizontal::Right, gui::Alignment::Vertical::Center));
        playedSong->setVisible(false);
        playedSong->setEdges(RectangleEdge::None);

        authorText = new TextFixedSize(secondHBox, 0, 0, 0, 0);
        authorText->setMinimumHeight(songItem::text_h);


@@ 60,7 65,7 @@ namespace gui
        authorText->setMargins(Margins(songItem::leftMargin, 0, 0, 0));
        authorText->setEdges(RectangleEdge::None);
        authorText->setUnderline(false);
        authorText->setFont(style::window::font::verysmall);
        authorText->setFont(style::window::font::medium);
        authorText->setAlignment(Alignment(gui::Alignment::Horizontal::Left, gui::Alignment::Vertical::Center));
        authorText->setEditMode(EditMode::Browse);
        authorText->setText(authorName);


@@ 69,5 74,53 @@ namespace gui
            vBox->setArea({0, 0, newDim.w, newDim.h});
            return true;
        };

        focusChangedCallback = [&](gui::Item &item) {
            if (item.focus) {
                std::string bottorBarText;
                switch (itemState) {
                case ItemState::Playing:
                    bottorBarText = utils::translate("common_pause");
                    break;
                case ItemState::Paused:
                    bottorBarText = utils::translate("common_resume");
                    break;
                case ItemState::None:
                    bottorBarText = utils::translate("app_music_player_play");
                    ;
                    break;
                }
                if (bottomBarTemporaryMode != nullptr) {
                    bottomBarTemporaryMode(bottorBarText);
                }
            }
            else {
                setFocusItem(nullptr);
                if (bottomBarRestoreFromTemporaryMode != nullptr) {
                    bottomBarRestoreFromTemporaryMode();
                }
            }
            return true;
        };
    }

    void SongItem::setState(ItemState state)
    {
        itemState = state;
        switch (state) {
        case ItemState::Paused:
            playedSong->set("now_playing_icon_pause_list");
            playedSong->setVisible(true);
            break;
        case ItemState::Playing:
            playedSong->set("now_playing_icon_list");
            playedSong->setVisible(true);
            break;
        case ItemState::None:
            playedSong->set("");
            playedSong->setVisible(false);
            break;
        }
        secondHBox->resizeItems();
    }
} /* namespace gui */

M module-apps/application-music-player/widgets/SongItem.hpp => module-apps/application-music-player/widgets/SongItem.hpp +25 -10
@@ 1,4 1,4 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once


@@ 8,7 8,7 @@
#include <ListItem.hpp>
#include <Text.hpp>
#include <TextFixedSize.hpp>
#include <ImageBox.hpp>
#include <Image.hpp>

namespace gui
{


@@ 16,16 16,31 @@ namespace gui
    {

      public:
        SongItem(const std::string &authorName, const std::string &songName, const std::string &duration);
        SongItem(const std::string &authorName,
                 const std::string &songName,
                 const std::string &duration,
                 std::function<void(const UTF8 &)> bottomBarTemporaryMode,
                 std::function<void()> bottomBarRestoreFromTemporaryMode);

        enum class ItemState
        {
            None,
            Playing,
            Paused
        };
        void setState(ItemState state);

      private:
        VBox *vBox                  = nullptr;
        HBox *firstHBox             = nullptr;
        HBox *secondHBox            = nullptr;
        TextFixedSize *authorText   = nullptr;
        TextFixedSize *songText     = nullptr;
        TextFixedSize *durationText = nullptr;
        ImageBox *playedSong        = nullptr;
        VBox *vBox                                                   = nullptr;
        HBox *firstHBox                                              = nullptr;
        HBox *secondHBox                                             = nullptr;
        TextFixedSize *authorText                                    = nullptr;
        TextFixedSize *songText                                      = nullptr;
        TextFixedSize *durationText                                  = nullptr;
        Image *playedSong                                            = nullptr;
        ItemState itemState                                          = ItemState::None;
        std::function<void(const UTF8 &text)> bottomBarTemporaryMode = nullptr;
        std::function<void()> bottomBarRestoreFromTemporaryMode      = nullptr;
    };

} /* namespace gui */

M module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.cpp => module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.cpp +54 -5
@@ 10,6 10,7 @@
#include <i18n/i18n.hpp>
#include <service-audio/AudioServiceAPI.hpp>
#include <gui/widgets/ListView.hpp>
#include <gui/widgets/Icon.hpp>

namespace gui
{


@@ 32,10 33,8 @@ namespace gui
    {
        AppWindow::buildInterface();

        setTitle(utils::translate("app_music_player_all_songs"));

        bottomBar->setText(BottomBar::Side::CENTER, utils::translate("app_music_player_play"));
        bottomBar->setText(BottomBar::Side::RIGHT, utils::translate(style::strings::common::back));
        bottomBar->setText(BottomBar::Side::CENTER, utils::translate("app_music_player_music_library"));
        bottomBar->setText(BottomBar::Side::RIGHT, utils::translate("app_music_player_quit"));

        songsList = new gui::ListView(this,
                                      musicPlayerStyle::allSongsWindow::x,


@@ 45,6 44,20 @@ namespace gui
                                      presenter->getMusicPlayerItemProvider(),
                                      listview::ScrollBarType::Fixed);

        emptyListIcon = new gui::Icon(this,
                                      ::style::window::default_left_margin,
                                      ::style::window::default_vertical_pos,
                                      ::style::window::default_body_width,
                                      ::style::window::default_body_height,
                                      "note",
                                      utils::translate("app_music_player_music_empty_window_notification"));

        emptyListIcon->setAlignment(Alignment::Horizontal::Center);
        songsList->emptyListCallback    = [this]() { emptyListIcon->setVisible(true); };
        songsList->notEmptyListCallback = [this]() {
            emptyListIcon->setVisible(false);
            setTitle(utils::translate("app_music_player_music_library_window_name"));
        };
        setFocusItem(songsList);
    }



@@ 55,7 68,43 @@ namespace gui

    void MusicPlayerAllSongsWindow::onBeforeShow([[maybe_unused]] ShowMode mode, [[maybe_unused]] SwitchData *data)
    {
        presenter->createData([this](const std::string &fileName) { return presenter->play(fileName); });
        presenter->attach(this);
        auto index = presenter->getMusicPlayerItemProvider()->getCurrentIndex();

        songsList->rebuildList(listview::RebuildType::OnPageElement, index);
    }

    void MusicPlayerAllSongsWindow::updateSongsState()
    {
        songsList->rebuildList(gui::listview::RebuildType::InPlace);
    }

    void MusicPlayerAllSongsWindow::refreshWindow()
    {
        application->refreshWindow(gui::RefreshModes::GUI_REFRESH_FAST);
    }

    void MusicPlayerAllSongsWindow::setBottomBarTemporaryMode(const std::string &text)
    {
        bottomBarTemporaryMode(text, BottomBar::Side::CENTER, false);
    }

    void MusicPlayerAllSongsWindow::restoreFromBottomBarTemporaryMode()
    {
        bottomBarRestoreFromTemporaryMode();
    }

    bool MusicPlayerAllSongsWindow::onInput(const InputEvent &inputEvent)
    {
        if (AppWindow::onInput(inputEvent)) {
            return true;
        }

        if (inputEvent.isShortRelease(gui::KeyCode::KEY_ENTER)) {
            presenter->createData();
            return true;
        }

        return false;
    }
} /* namespace gui */

M module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.hpp => module-apps/application-music-player/windows/MusicPlayerAllSongsWindow.hpp +8 -1
@@ 10,21 10,28 @@
namespace gui
{
    class ListView;
    class Icon;
    class MusicPlayerAllSongsWindow : public AppWindow, public app::music_player::SongsContract::View
    {
        std::shared_ptr<app::music_player::SongsContract::Presenter> presenter;
        ListView *songsList = nullptr;
        Icon *emptyListIcon = nullptr;

      public:
        explicit MusicPlayerAllSongsWindow(app::Application *app,
                                           std::shared_ptr<app::music_player::SongsContract::Presenter> presenter);

        // virtual methods
        void onBeforeShow([[maybe_unused]] ShowMode mode, [[maybe_unused]] SwitchData *data) override;

        void rebuild() override;
        void buildInterface() override;
        void destroyInterface() override;
        bool onInput(const InputEvent &inputEvent) override;

        void updateSongsState() override;
        void refreshWindow() override;
        void setBottomBarTemporaryMode(const std::string &text) override;
        void restoreFromBottomBarTemporaryMode() override;
    };

} /* namespace gui */

D module-apps/application-music-player/windows/MusicPlayerEmptyWindow.cpp => module-apps/application-music-player/windows/MusicPlayerEmptyWindow.cpp +0 -80
@@ 1,80 0,0 @@
// Copyright (c) 2017-2021, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#include "MusicPlayerEmptyWindow.hpp"
#include "application-music-player/ApplicationMusicPlayer.hpp"
#include "application-music-player/data/MusicPlayerStyle.hpp"

#include <Style.hpp>
#include <i18n/i18n.hpp>
#include <log.hpp>
#include <service-audio/AudioServiceAPI.hpp>

namespace gui
{
    using namespace musicPlayerStyle::emptyWindow;

    MusicPlayerEmptyWindow::MusicPlayerEmptyWindow(app::Application *app)
        : AppWindow(app, gui::name::window::main_window)
    {
        buildInterface();
    }

    void MusicPlayerEmptyWindow::rebuild()
    {
        destroyInterface();
        buildInterface();
    }

    void MusicPlayerEmptyWindow::buildInterface()
    {
        AppWindow::buildInterface();

        bottomBar->setText(BottomBar::Side::LEFT, utils::translate("app_music_player_music_library"));
        bottomBar->setText(BottomBar::Side::CENTER, utils::translate("app_music_player_play"));
        bottomBar->setText(BottomBar::Side::RIGHT, utils::translate("app_music_player_quit"));

        img = new gui::Image(this, noteImg::x, noteImg::y, "note");

        text = new Text(this, infoText::x, infoText::y, infoText::w, infoText::h);
        text->setText(utils::translate("app_music_player_music_empty_window_notification"));
        text->setTextType(TextType::MultiLine);
        text->setEditMode(EditMode::Browse);
        text->setEdges(RectangleEdge::None);
        text->setFont(style::window::font::medium);
        text->setAlignment(gui::Alignment(gui::Alignment::Horizontal::Center, gui::Alignment::Vertical::Center));

        placeHolder = new gui::Image(this, placeHolderImg::x, placeHolderImg::y, "placeholder_player");
    }

    void MusicPlayerEmptyWindow::destroyInterface()
    {
        erase();
    }

    void MusicPlayerEmptyWindow::onBeforeShow(ShowMode mode, SwitchData *data)
    {}

    bool MusicPlayerEmptyWindow::onDatabaseMessage(sys::Message *msgl)
    {
        return false;
    }

    bool MusicPlayerEmptyWindow::onInput(const InputEvent &inputEvent)
    {
        if (AppWindow::onInput(inputEvent)) {
            return true;
        }

        if (inputEvent.isShortRelease(gui::KeyCode::KEY_LF)) {
            application->switchWindow(gui::name::window::all_songs_window);
            return true;
        }

        if (inputEvent.is(gui::KeyCode::KEY_ENTER) || inputEvent.is(gui::KeyCode::HEADSET_OK)) {
        }

        return false;
    }

} /* namespace gui */

D module-apps/application-music-player/windows/MusicPlayerEmptyWindow.hpp => module-apps/application-music-player/windows/MusicPlayerEmptyWindow.hpp +0 -35
@@ 1,35 0,0 @@
// Copyright (c) 2017-2020, Mudita Sp. z.o.o. All rights reserved.
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md

#pragma once

#include "AppWindow.hpp"
#include <Text.hpp>
#include <Image.hpp>

#include <vector>
#include <string>

namespace gui
{

    class MusicPlayerEmptyWindow : public AppWindow
    {
        Image *img         = nullptr;
        Text *text         = nullptr;
        Image *placeHolder = nullptr;

      public:
        MusicPlayerEmptyWindow(app::Application *app);

        // virtual methods
        void onBeforeShow(ShowMode mode, SwitchData *data) override;

        void rebuild() override;
        void buildInterface() override;
        void destroyInterface() override;
        bool onDatabaseMessage(sys::Message *msg) override;
        bool onInput(const InputEvent &inputEvent) final;
    };

} /* namespace gui */

M module-services/service-audio/ServiceAudio.cpp => module-services/service-audio/ServiceAudio.cpp +0 -2
@@ 288,8 288,6 @@ std::unique_ptr<AudioResponseMessage> ServiceAudio::HandlePause(std::optional<Au
        retCode  = audioInput->audio->Pause();
        retToken = audioInput->token;
        audioInput->DisableVibration();
        bus.sendMulticast(std::make_shared<AudioPausedNotification>(audioInput->token),
                          sys::BusChannel::ServiceAudioNotifications);
    }
    else {
        retCode = StopInput(audioInput);

M module-services/service-audio/include/service-audio/AudioMessage.hpp => module-services/service-audio/include/service-audio/AudioMessage.hpp +0 -7
@@ 56,13 56,6 @@ class AudioNotificationMessage : public AudioMessage
    const audio::Token token;
};

class AudioPausedNotification : public AudioNotificationMessage
{
  public:
    explicit AudioPausedNotification(audio::Token token) : AudioNotificationMessage{token}
    {}
};

class AudioStopNotification : public AudioNotificationMessage
{
  public: