diff options
Diffstat (limited to 'xbmc/pvr/guilib')
41 files changed, 12770 insertions, 0 deletions
diff --git a/xbmc/pvr/guilib/CMakeLists.txt b/xbmc/pvr/guilib/CMakeLists.txt new file mode 100644 index 0000000..3a73a68 --- /dev/null +++ b/xbmc/pvr/guilib/CMakeLists.txt @@ -0,0 +1,35 @@ +set(SOURCES GUIEPGGridContainer.cpp + GUIEPGGridContainerModel.cpp + PVRGUIActionListener.cpp + PVRGUIActionsChannels.cpp + PVRGUIActionsClients.cpp + PVRGUIActionsDatabase.cpp + PVRGUIActionsEPG.cpp + PVRGUIActionsUtils.cpp + PVRGUIActionsParentalControl.cpp + PVRGUIActionsPlayback.cpp + PVRGUIActionsPowerManagement.cpp + PVRGUIActionsRecordings.cpp + PVRGUIActionsTimers.cpp + PVRGUIChannelIconUpdater.cpp + PVRGUIChannelNavigator.cpp + PVRGUIProgressHandler.cpp) + +set(HEADERS GUIEPGGridContainer.h + GUIEPGGridContainerModel.h + PVRGUIActionListener.h + PVRGUIActionsChannels.h + PVRGUIActionsClients.h + PVRGUIActionsDatabase.h + PVRGUIActionsEPG.h + PVRGUIActionsUtils.h + PVRGUIActionsParentalControl.h + PVRGUIActionsPlayback.h + PVRGUIActionsPowerManagement.h + PVRGUIActionsRecordings.h + PVRGUIActionsTimers.h + PVRGUIChannelIconUpdater.h + PVRGUIChannelNavigator.h + PVRGUIProgressHandler.h) + +core_add_library(pvr_guilib) diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.cpp b/xbmc/pvr/guilib/GUIEPGGridContainer.cpp new file mode 100644 index 0000000..d88dc86 --- /dev/null +++ b/xbmc/pvr/guilib/GUIEPGGridContainer.cpp @@ -0,0 +1,2549 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIEPGGridContainer.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/DirtyRegion.h" +#include "guilib/GUIAction.h" +#include "guilib/GUIMessage.h" +#include "guilib/guiinfo/GUIInfoLabels.h" +#include "input/Key.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVRManager.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/guilib/GUIEPGGridContainerModel.h" +#include "utils/MathUtils.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +#include <algorithm> +#include <memory> +#include <mutex> +#include <string> +#include <utility> + +#include <tinyxml.h> + +using namespace PVR; + +#define BLOCKJUMP 4 // how many blocks are jumped with each analogue scroll action +static const int BLOCK_SCROLL_OFFSET = 60 / CGUIEPGGridContainerModel::MINSPERBLOCK; // how many blocks are jumped if we are at left/right edge of grid + +CGUIEPGGridContainer::CGUIEPGGridContainer(int parentID, + int controlID, + float posX, + float posY, + float width, + float height, + ORIENTATION orientation, + int scrollTime, + int preloadItems, + int timeBlocks, + int rulerUnit, + const CTextureInfo& progressIndicatorTexture) + : IGUIContainer(parentID, controlID, posX, posY, width, height), + m_orientation(orientation), + m_channelLayout(nullptr), + m_focusedChannelLayout(nullptr), + m_programmeLayout(nullptr), + m_focusedProgrammeLayout(nullptr), + m_rulerLayout(nullptr), + m_rulerDateLayout(nullptr), + m_pageControl(0), + m_rulerUnit(rulerUnit), + m_channelsPerPage(0), + m_programmesPerPage(0), + m_channelCursor(0), + m_channelOffset(0), + m_blocksPerPage(timeBlocks), + m_blockCursor(0), + m_blockOffset(0), + m_blockTravelAxis(0), + m_cacheChannelItems(preloadItems), + m_cacheProgrammeItems(preloadItems), + m_cacheRulerItems(preloadItems), + m_rulerDateHeight(0), + m_rulerDateWidth(0), + m_rulerPosX(0), + m_rulerPosY(0), + m_rulerHeight(0), + m_rulerWidth(0), + m_channelPosX(0), + m_channelPosY(0), + m_channelHeight(0), + m_channelWidth(0), + m_gridPosX(0), + m_gridPosY(0), + m_gridWidth(0), + m_gridHeight(0), + m_blockSize(0), + m_analogScrollCount(0), + m_guiProgressIndicatorTexture( + CGUITexture::CreateTexture(posX, posY, width, height, progressIndicatorTexture)), + m_scrollTime(scrollTime ? scrollTime : 1), + m_programmeScrollLastTime(0), + m_programmeScrollSpeed(0), + m_programmeScrollOffset(0), + m_channelScrollLastTime(0), + m_channelScrollSpeed(0), + m_channelScrollOffset(0), + m_gridModel(new CGUIEPGGridContainerModel) +{ + ControlType = GUICONTAINER_EPGGRID; +} + +CGUIEPGGridContainer::CGUIEPGGridContainer(const CGUIEPGGridContainer& other) + : IGUIContainer(other), + m_renderOffset(other.m_renderOffset), + m_orientation(other.m_orientation), + m_channelLayouts(other.m_channelLayouts), + m_focusedChannelLayouts(other.m_focusedChannelLayouts), + m_focusedProgrammeLayouts(other.m_focusedProgrammeLayouts), + m_programmeLayouts(other.m_programmeLayouts), + m_rulerLayouts(other.m_rulerLayouts), + m_rulerDateLayouts(other.m_rulerDateLayouts), + m_channelLayout(other.m_channelLayout), + m_focusedChannelLayout(other.m_focusedChannelLayout), + m_programmeLayout(other.m_programmeLayout), + m_focusedProgrammeLayout(other.m_focusedProgrammeLayout), + m_rulerLayout(other.m_rulerLayout), + m_rulerDateLayout(other.m_rulerDateLayout), + m_pageControl(other.m_pageControl), + m_rulerUnit(other.m_rulerUnit), + m_channelsPerPage(other.m_channelsPerPage), + m_programmesPerPage(other.m_programmesPerPage), + m_channelCursor(other.m_channelCursor), + m_channelOffset(other.m_channelOffset), + m_blocksPerPage(other.m_blocksPerPage), + m_blockCursor(other.m_blockCursor), + m_blockOffset(other.m_blockOffset), + m_blockTravelAxis(other.m_blockTravelAxis), + m_cacheChannelItems(other.m_cacheChannelItems), + m_cacheProgrammeItems(other.m_cacheProgrammeItems), + m_cacheRulerItems(other.m_cacheRulerItems), + m_rulerDateHeight(other.m_rulerDateHeight), + m_rulerDateWidth(other.m_rulerDateWidth), + m_rulerPosX(other.m_rulerPosX), + m_rulerPosY(other.m_rulerPosY), + m_rulerHeight(other.m_rulerHeight), + m_rulerWidth(other.m_rulerWidth), + m_channelPosX(other.m_channelPosX), + m_channelPosY(other.m_channelPosY), + m_channelHeight(other.m_channelHeight), + m_channelWidth(other.m_channelWidth), + m_gridPosX(other.m_gridPosX), + m_gridPosY(other.m_gridPosY), + m_gridWidth(other.m_gridWidth), + m_gridHeight(other.m_gridHeight), + m_blockSize(other.m_blockSize), + m_analogScrollCount(other.m_analogScrollCount), + m_guiProgressIndicatorTexture(other.m_guiProgressIndicatorTexture->Clone()), + m_lastItem(other.m_lastItem), + m_lastChannel(other.m_lastChannel), + m_scrollTime(other.m_scrollTime), + m_programmeScrollLastTime(other.m_programmeScrollLastTime), + m_programmeScrollSpeed(other.m_programmeScrollSpeed), + m_programmeScrollOffset(other.m_programmeScrollOffset), + m_channelScrollLastTime(other.m_channelScrollLastTime), + m_channelScrollSpeed(other.m_channelScrollSpeed), + m_channelScrollOffset(other.m_channelScrollOffset), + m_gridModel(new CGUIEPGGridContainerModel(*other.m_gridModel)), + m_updatedGridModel(other.m_updatedGridModel + ? new CGUIEPGGridContainerModel(*other.m_updatedGridModel) + : nullptr), + m_itemStartBlock(other.m_itemStartBlock) +{ +} + +bool CGUIEPGGridContainer::HasData() const +{ + return m_gridModel && m_gridModel->HasChannelItems(); +} + +void CGUIEPGGridContainer::AllocResources() +{ + IGUIContainer::AllocResources(); + m_guiProgressIndicatorTexture->AllocResources(); +} + +void CGUIEPGGridContainer::FreeResources(bool immediately) +{ + m_guiProgressIndicatorTexture->FreeResources(immediately); + IGUIContainer::FreeResources(immediately); +} + +void CGUIEPGGridContainer::SetPageControl(int id) +{ + m_pageControl = id; +} + +void CGUIEPGGridContainer::Process(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + ValidateOffset(); + + if (m_bInvalidated) + { + UpdateLayout(); + + if (m_pageControl) + { + int iItemsPerPage; + int iTotalItems; + + if (m_orientation == VERTICAL) + { + iItemsPerPage = m_channelsPerPage; + iTotalItems = m_gridModel->ChannelItemsSize(); + } + else + { + iItemsPerPage = m_blocksPerPage; + iTotalItems = m_gridModel->GridItemsSize(); + } + + CGUIMessage msg(GUI_MSG_LABEL_RESET, GetID(), m_pageControl, iItemsPerPage, iTotalItems); + SendWindowMessage(msg); + } + } + + UpdateScrollOffset(currentTime); + ProcessChannels(currentTime, dirtyregions); + ProcessRulerDate(currentTime, dirtyregions); + ProcessRuler(currentTime, dirtyregions); + ProcessProgrammeGrid(currentTime, dirtyregions); + ProcessProgressIndicator(currentTime, dirtyregions); + + if (m_pageControl) + { + int iItem = + (m_orientation == VERTICAL) + ? MathUtils::round_int(static_cast<double>(m_channelScrollOffset / m_channelHeight)) + : MathUtils::round_int( + static_cast<double>(m_programmeScrollOffset / (m_gridHeight / m_blocksPerPage))); + + CGUIMessage msg(GUI_MSG_ITEM_SELECT, GetID(), m_pageControl, iItem); + SendWindowMessage(msg); + } + + CGUIControl::Process(currentTime, dirtyregions); +} + +void CGUIEPGGridContainer::Render() +{ + RenderChannels(); + RenderRulerDate(); + RenderRuler(); + RenderProgrammeGrid(); + RenderProgressIndicator(); + + CGUIControl::Render(); +} + +void CGUIEPGGridContainer::ProcessChannels(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + HandleChannels(false, currentTime, dirtyregions); +} + +void CGUIEPGGridContainer::RenderChannels() +{ + // params not needed for render. + unsigned int dummyTime = 0; + CDirtyRegionList dummyRegions; + HandleChannels(true, dummyTime, dummyRegions); +} + +void CGUIEPGGridContainer::ProcessRulerDate(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + HandleRulerDate(false, currentTime, dirtyregions); +} + +void CGUIEPGGridContainer::RenderRulerDate() +{ + // params not needed for render. + unsigned int dummyTime = 0; + CDirtyRegionList dummyRegions; + HandleRulerDate(true, dummyTime, dummyRegions); +} + +void CGUIEPGGridContainer::ProcessRuler(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + HandleRuler(false, currentTime, dirtyregions); +} + +void CGUIEPGGridContainer::RenderRuler() +{ + // params not needed for render. + unsigned int dummyTime = 0; + CDirtyRegionList dummyRegions; + HandleRuler(true, dummyTime, dummyRegions); +} + +void CGUIEPGGridContainer::ProcessProgrammeGrid(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + HandleProgrammeGrid(false, currentTime, dirtyregions); +} + +void CGUIEPGGridContainer::RenderProgrammeGrid() +{ + // params not needed for render. + unsigned int dummyTime = 0; + CDirtyRegionList dummyRegions; + HandleProgrammeGrid(true, dummyTime, dummyRegions); +} + +float CGUIEPGGridContainer::GetCurrentTimePositionOnPage() const +{ + if (!m_gridModel->GetGridStart().IsValid()) + return -1.0f; + + const CDateTimeSpan startDelta(CDateTime::GetUTCDateTime() - m_gridModel->GetGridStart()); + const float fPos = (startDelta.GetSecondsTotal() * m_blockSize) / + (CGUIEPGGridContainerModel::MINSPERBLOCK * 60) - + GetProgrammeScrollOffsetPos(); + return std::min(fPos, m_orientation == VERTICAL ? m_gridWidth : m_gridHeight); +} + +float CGUIEPGGridContainer::GetProgressIndicatorWidth() const +{ + return (m_orientation == VERTICAL) ? GetCurrentTimePositionOnPage() : m_rulerWidth + m_gridWidth; +} + +float CGUIEPGGridContainer::GetProgressIndicatorHeight() const +{ + return (m_orientation == VERTICAL) ? m_rulerHeight + m_gridHeight : GetCurrentTimePositionOnPage(); +} + +void CGUIEPGGridContainer::ProcessProgressIndicator(unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + float width = GetProgressIndicatorWidth(); + float height = GetProgressIndicatorHeight(); + + if (width > 0 && height > 0) + { + m_guiProgressIndicatorTexture->SetVisible(true); + m_guiProgressIndicatorTexture->SetPosition(m_rulerPosX + m_renderOffset.x, + m_rulerPosY + m_renderOffset.y); + m_guiProgressIndicatorTexture->SetWidth(width); + m_guiProgressIndicatorTexture->SetHeight(height); + } + else + { + m_guiProgressIndicatorTexture->SetVisible(false); + } + + m_guiProgressIndicatorTexture->Process(currentTime); +} + +void CGUIEPGGridContainer::RenderProgressIndicator() +{ + if (CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, GetProgressIndicatorWidth(), GetProgressIndicatorHeight())) + { + m_guiProgressIndicatorTexture->SetDiffuseColor(m_diffuseColor); + m_guiProgressIndicatorTexture->Render(); + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } +} + +void CGUIEPGGridContainer::ProcessItem(float posX, float posY, const CFileItemPtr& item, CFileItemPtr& lastitem, + bool focused, CGUIListItemLayout* normallayout, CGUIListItemLayout* focusedlayout, + unsigned int currentTime, CDirtyRegionList& dirtyregions, float resize /* = -1.0f */) +{ + if (!normallayout || !focusedlayout) + return; + + // set the origin + CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(posX, posY); + + if (m_bInvalidated) + item->SetInvalid(); + + if (focused) + { + if (!item->GetFocusedLayout()) + { + item->SetFocusedLayout(std::make_unique<CGUIListItemLayout>(*focusedlayout, this)); + } + + if (resize != -1.0f) + { + if (m_orientation == VERTICAL) + item->GetFocusedLayout()->SetWidth(resize); + else + item->GetFocusedLayout()->SetHeight(resize); + } + + if (item != lastitem || !HasFocus()) + item->GetFocusedLayout()->SetFocusedItem(0); + + if (item != lastitem && HasFocus()) + { + item->GetFocusedLayout()->ResetAnimation(ANIM_TYPE_UNFOCUS); + + unsigned int subItem = 1; + if (lastitem && lastitem->GetFocusedLayout()) + subItem = lastitem->GetFocusedLayout()->GetFocusedItem(); + + item->GetFocusedLayout()->SetFocusedItem(subItem ? subItem : 1); + } + + item->GetFocusedLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions); + lastitem = item; + } + else + { + if (!item->GetLayout()) + { + item->SetLayout(std::make_unique<CGUIListItemLayout>(*normallayout, this)); + } + + if (resize != -1.0f) + { + if (m_orientation == VERTICAL) + item->GetLayout()->SetWidth(resize); + else + item->GetLayout()->SetHeight(resize); + } + + if (item->GetFocusedLayout()) + item->GetFocusedLayout()->SetFocusedItem(0); + + if (item->GetFocusedLayout() && item->GetFocusedLayout()->IsAnimating(ANIM_TYPE_UNFOCUS)) + item->GetFocusedLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions); + else + item->GetLayout()->Process(item.get(), m_parentID, currentTime, dirtyregions); + } + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin(); +} + +void CGUIEPGGridContainer::RenderItem(float posX, float posY, CGUIListItem* item, bool focused) +{ + // set the origin + CServiceBroker::GetWinSystem()->GetGfxContext().SetOrigin(posX, posY); + + if (focused) + { + if (item->GetFocusedLayout()) + item->GetFocusedLayout()->Render(item, m_parentID); + } + else + { + if (item->GetFocusedLayout() && item->GetFocusedLayout()->IsAnimating(ANIM_TYPE_UNFOCUS)) + item->GetFocusedLayout()->Render(item, m_parentID); + else if (item->GetLayout()) + item->GetLayout()->Render(item, m_parentID); + } + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreOrigin(); +} + +bool CGUIEPGGridContainer::OnAction(const CAction& action) +{ + switch (action.GetID()) + { + case ACTION_MOVE_LEFT: + case ACTION_MOVE_RIGHT: + case ACTION_MOVE_DOWN: + case ACTION_MOVE_UP: + case ACTION_NAV_BACK: + // use base class implementation + return CGUIControl::OnAction(action); + + case ACTION_NEXT_ITEM: + // skip +12h + ScrollToBlockOffset(m_blockOffset + (12 * 60 / CGUIEPGGridContainerModel::MINSPERBLOCK)); + SetBlock(m_blockCursor); + return true; + + case ACTION_PREV_ITEM: + // skip -12h + ScrollToBlockOffset(m_blockOffset - (12 * 60 / CGUIEPGGridContainerModel::MINSPERBLOCK)); + SetBlock(m_blockCursor); + return true; + + case REMOTE_0: + GoToNow(); + return true; + + case ACTION_PAGE_UP: + if (m_orientation == VERTICAL) + { + if (m_channelOffset == 0) + { + // already on the first page, so move to the first item + SetChannel(0); + } + else + { + // scroll up to the previous page + ChannelScroll(-m_channelsPerPage); + } + } + else + { + if (m_blockOffset == 0) + { + // already on the first page, so move to the first item + SetBlock(0); + } + else + { + // scroll up to the previous page + ProgrammesScroll(-m_blocksPerPage); + } + } + return true; + + case ACTION_PAGE_DOWN: + if (m_orientation == VERTICAL) + { + if (m_channelOffset == m_gridModel->ChannelItemsSize() - m_channelsPerPage || + m_gridModel->ChannelItemsSize() < m_channelsPerPage) + { + // already at the last page, so move to the last item. + SetChannel(m_gridModel->GetLastChannel() - m_channelOffset); + } + else + { + // scroll down to the next page + ChannelScroll(m_channelsPerPage); + } + } + else + { + if (m_blockOffset == m_gridModel->GridItemsSize() - m_blocksPerPage || + m_gridModel->GridItemsSize() < m_blocksPerPage) + { + // already at the last page, so move to the last item. + SetBlock(m_gridModel->GetLastBlock() - m_blockOffset); + } + else + { + // scroll down to the next page + ProgrammesScroll(m_blocksPerPage); + } + } + + return true; + + // smooth scrolling (for analog controls) + case ACTION_TELETEXT_RED: + case ACTION_TELETEXT_GREEN: + case ACTION_SCROLL_UP: // left horizontal scrolling + if (m_orientation == VERTICAL) + { + int blocksToJump = action.GetID() == ACTION_TELETEXT_RED ? m_blocksPerPage / 2 : m_blocksPerPage / 4; + + m_analogScrollCount += action.GetAmount() * action.GetAmount(); + bool handled = false; + + while (m_analogScrollCount > 0.4f) + { + handled = true; + m_analogScrollCount -= 0.4f; + + if (m_blockOffset > 0 && m_blockCursor <= m_blocksPerPage / 2) + ProgrammesScroll(-blocksToJump); + else if (m_blockCursor > blocksToJump) + SetBlock(m_blockCursor - blocksToJump); + } + return handled; + } + else + { + int channelsToJump = action.GetID() == ACTION_TELETEXT_RED ? m_channelsPerPage / 2 : m_channelsPerPage / 4; + + m_analogScrollCount += action.GetAmount() * action.GetAmount(); + bool handled = false; + + while (m_analogScrollCount > 0.4f) + { + handled = true; + m_analogScrollCount -= 0.4f; + + if (m_channelOffset > 0 && m_channelCursor <= m_channelsPerPage / 2) + ChannelScroll(-channelsToJump); + else if (m_channelCursor > channelsToJump) + SetChannel(m_channelCursor - channelsToJump); + } + return handled; + } + break; + + case ACTION_TELETEXT_BLUE: + case ACTION_TELETEXT_YELLOW: + case ACTION_SCROLL_DOWN: // right horizontal scrolling + if (m_orientation == VERTICAL) + { + int blocksToJump = action.GetID() == ACTION_TELETEXT_BLUE ? m_blocksPerPage / 2 : m_blocksPerPage / 4; + + m_analogScrollCount += action.GetAmount() * action.GetAmount(); + bool handled = false; + + while (m_analogScrollCount > 0.4f) + { + handled = true; + m_analogScrollCount -= 0.4f; + + if (m_blockOffset + m_blocksPerPage < m_gridModel->GridItemsSize() && + m_blockCursor >= m_blocksPerPage / 2) + ProgrammesScroll(blocksToJump); + else if (m_blockCursor < m_blocksPerPage - blocksToJump && + m_blockOffset + m_blockCursor < m_gridModel->GridItemsSize() - blocksToJump) + SetBlock(m_blockCursor + blocksToJump); + } + return handled; + } + else + { + int channelsToJump = action.GetID() == ACTION_TELETEXT_BLUE ? m_channelsPerPage / 2 : m_channelsPerPage / 4; + + m_analogScrollCount += action.GetAmount() * action.GetAmount(); + bool handled = false; + + while (m_analogScrollCount > 0.4f) + { + handled = true; + m_analogScrollCount -= 0.4f; + + if (m_channelOffset + m_channelsPerPage < m_gridModel->ChannelItemsSize() && m_channelCursor >= m_channelsPerPage / 2) + ChannelScroll(channelsToJump); + else if (m_channelCursor < m_channelsPerPage - channelsToJump && m_channelOffset + m_channelCursor < m_gridModel->ChannelItemsSize() - channelsToJump) + SetChannel(m_channelCursor + channelsToJump); + } + return handled; + } + break; + + default: + if (action.GetID()) + return OnClick(action.GetID()); + + break; + } + + return false; +} + +bool CGUIEPGGridContainer::OnMessage(CGUIMessage& message) +{ + if (message.GetControlId() == GetID()) + { + switch (message.GetMessage()) + { + case GUI_MSG_PAGE_CHANGE: + if (message.GetSenderId() == m_pageControl && IsVisible()) + { + if (m_orientation == VERTICAL) + { + ScrollToChannelOffset(message.GetParam1()); + SetChannel(m_channelCursor); + } + else + { + ScrollToBlockOffset(message.GetParam1()); + SetBlock(m_blockCursor); + } + return true; + } + break; + + case GUI_MSG_LABEL_BIND: + UpdateItems(); + return true; + + case GUI_MSG_REFRESH_LIST: + // update our list contents + m_gridModel->SetInvalid(); + break; + } + } + + return CGUIControl::OnMessage(message); +} + +void CGUIEPGGridContainer::UpdateItems() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (!m_updatedGridModel) + return; + + // Save currently selected epg tag and grid coordinates. Selection shall be restored after update. + std::shared_ptr<CPVREpgInfoTag> prevSelectedEpgTag; + if (HasData()) + prevSelectedEpgTag = + m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset) + ->GetEPGInfoTag(); + + const int oldChannelIndex = m_channelOffset + m_channelCursor; + const int oldBlockIndex = m_blockOffset + m_blockCursor; + const CDateTime oldGridStart(m_gridModel->GetGridStart()); + int eventOffset = oldBlockIndex; + int newChannelIndex = oldChannelIndex; + int newBlockIndex = oldBlockIndex; + int channelUid = -1; + unsigned int broadcastUid = 0; + + if (prevSelectedEpgTag) + { + // get the block offset relative to the first block of the selected event + eventOffset = + oldBlockIndex - m_gridModel->GetGridItemStartBlock(oldChannelIndex, oldBlockIndex); + + if (!prevSelectedEpgTag->IsGapTag()) // "normal" tag selected + { + if (oldGridStart >= prevSelectedEpgTag->StartAsUTC()) + { + // start of previously selected event is before grid start + newBlockIndex = eventOffset; + } + else + { + newBlockIndex = m_gridModel->GetFirstEventBlock(prevSelectedEpgTag) + eventOffset; + } + + channelUid = prevSelectedEpgTag->UniqueChannelID(); + broadcastUid = prevSelectedEpgTag->UniqueBroadcastID(); + } + else // "gap" tag selected + { + channelUid = prevSelectedEpgTag->UniqueChannelID(); + + // As gap tags do not have a unique broadcast id, we will look for the real tag preceding + // the gap tag and add the respective offset to restore the gap tag selection, assuming that + // the real tag is still the predecessor of the gap tag after the grid model update. + + const std::shared_ptr<CFileItem> prevItem = GetPrevItem().first; + if (prevItem) + { + const std::shared_ptr<CPVREpgInfoTag> tag = prevItem->GetEPGInfoTag(); + if (tag && !tag->IsGapTag()) + { + if (oldGridStart >= tag->StartAsUTC()) + { + // start of previously selected event is before grid start + newBlockIndex = eventOffset; + } + else + { + newBlockIndex = m_gridModel->GetFirstEventBlock(tag); + eventOffset += m_gridModel->GetFirstEventBlock(prevSelectedEpgTag) - newBlockIndex; + } + + broadcastUid = tag->UniqueBroadcastID(); + } + } + } + } + + m_lastItem = nullptr; + m_lastChannel = nullptr; + + // always use asynchronously precalculated grid data. + m_gridModel = std::move(m_updatedGridModel); + + if (prevSelectedEpgTag) + { + if (oldGridStart != m_gridModel->GetGridStart()) + { + // grid start changed. block offset for selected event might have changed. + newBlockIndex += m_gridModel->GetBlock(oldGridStart); + if (newBlockIndex < 0 || newBlockIndex > m_gridModel->GetLastBlock()) + { + // previous selection is no longer in grid. + SetInvalid(); + m_bEnableChannelScrolling = false; + GoToChannel(newChannelIndex); + m_bEnableProgrammeScrolling = false; + GoToNow(); + return; + } + } + + if (newChannelIndex >= m_gridModel->ChannelItemsSize() || + newBlockIndex >= m_gridModel->GridItemsSize() || + m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag() != + prevSelectedEpgTag) + { + int iChannelIndex = CGUIEPGGridContainerModel::INVALID_INDEX; + int iBlockIndex = CGUIEPGGridContainerModel::INVALID_INDEX; + m_gridModel->FindChannelAndBlockIndex(channelUid, broadcastUid, eventOffset, iChannelIndex, iBlockIndex); + + if (iBlockIndex != CGUIEPGGridContainerModel::INVALID_INDEX) + { + newBlockIndex = iBlockIndex; + } + else if (newBlockIndex > m_gridModel->GetLastBlock()) + { + // default to now + newBlockIndex = m_gridModel->GetNowBlock(); + + if (newBlockIndex > m_gridModel->GetLastBlock()) + { + // last block is in the past. default to last block + newBlockIndex = m_gridModel->GetLastBlock(); + } + } + + if (iChannelIndex != CGUIEPGGridContainerModel::INVALID_INDEX) + { + newChannelIndex = iChannelIndex; + } + else if (newChannelIndex >= m_gridModel->ChannelItemsSize() || + (m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag()->UniqueChannelID() != prevSelectedEpgTag->UniqueChannelID() && + m_gridModel->GetGridItem(newChannelIndex, newBlockIndex)->GetEPGInfoTag()->ClientID() != prevSelectedEpgTag->ClientID())) + { + // default to first channel + newChannelIndex = 0; + } + } + + // restore previous selection. + if (newChannelIndex == oldChannelIndex && newBlockIndex == oldBlockIndex) + { + // same coordinates, keep current grid view port + UpdateItem(); + } + else + { + // new coordinates, move grid view port accordingly + SetInvalid(); + + if (newBlockIndex != oldBlockIndex) + { + m_bEnableProgrammeScrolling = false; + GoToBlock(newBlockIndex); + } + + if (newChannelIndex != oldChannelIndex) + { + m_bEnableChannelScrolling = false; + GoToChannel(newChannelIndex); + } + } + } + else + { + // no previous selection, goto now + SetInvalid(); + m_bEnableProgrammeScrolling = false; + GoToNow(); + } +} + +float CGUIEPGGridContainer::GetChannelScrollOffsetPos() const +{ + if (m_bEnableChannelScrolling) + return m_channelScrollOffset; + else + return m_channelOffset * m_channelLayout->Size(m_orientation); +} + +float CGUIEPGGridContainer::GetProgrammeScrollOffsetPos() const +{ + if (m_bEnableProgrammeScrolling) + return m_programmeScrollOffset; + else + return m_blockOffset * m_blockSize; +} + +int CGUIEPGGridContainer::GetChannelScrollOffset(CGUIListItemLayout* layout) const +{ + if (m_bEnableChannelScrolling) + return MathUtils::round_int( + static_cast<double>(m_channelScrollOffset / layout->Size(m_orientation))); + else + return m_channelOffset; +} + +int CGUIEPGGridContainer::GetProgrammeScrollOffset() const +{ + if (m_bEnableProgrammeScrolling) + return MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize)); + else + return m_blockOffset; +} + +void CGUIEPGGridContainer::ChannelScroll(int amount) +{ + // increase or decrease the vertical offset + int offset = m_channelOffset + amount; + + if (offset > m_gridModel->ChannelItemsSize() - m_channelsPerPage) + offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage; + + if (offset < 0) + offset = 0; + + ScrollToChannelOffset(offset); + SetChannel(m_channelCursor); +} + +void CGUIEPGGridContainer::ProgrammesScroll(int amount) +{ + // increase or decrease the horizontal offset + ScrollToBlockOffset(m_blockOffset + amount); + SetBlock(m_blockCursor); +} + +void CGUIEPGGridContainer::OnUp() +{ + if (!HasData()) + return CGUIControl::OnUp(); + + if (m_orientation == VERTICAL) + { + CGUIAction action = GetAction(ACTION_MOVE_UP); + if (m_channelCursor > 0) + { + SetChannel(m_channelCursor - 1); + } + else if (m_channelCursor == 0 && m_channelOffset) + { + ScrollToChannelOffset(m_channelOffset - 1); + SetChannel(0); + } + else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around + { + int offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage; + + if (offset < 0) + offset = 0; + + SetChannel(m_gridModel->GetLastChannel() - offset); + ScrollToChannelOffset(offset); + } + else + CGUIControl::OnUp(); + } + else + { + if (m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset) > m_blockOffset) + { + // this is not first item on page + SetItem(GetPrevItem()); + UpdateBlock(); + return; + } + else if (m_blockCursor <= 0 && m_blockOffset && m_blockOffset - BLOCK_SCROLL_OFFSET >= 0) + { + // this is the first item on page + ScrollToBlockOffset(m_blockOffset - BLOCK_SCROLL_OFFSET); + UpdateBlock(); + return; + } + + CGUIControl::OnUp(); + } +} + +void CGUIEPGGridContainer::OnDown() +{ + if (!HasData()) + return CGUIControl::OnDown(); + + if (m_orientation == VERTICAL) + { + CGUIAction action = GetAction(ACTION_MOVE_DOWN); + if (m_channelOffset + m_channelCursor < m_gridModel->GetLastChannel()) + { + if (m_channelCursor + 1 < m_channelsPerPage) + { + SetChannel(m_channelCursor + 1); + } + else + { + ScrollToChannelOffset(m_channelOffset + 1); + SetChannel(m_channelsPerPage - 1); + } + } + else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around + { + ScrollToChannelOffset(0); + SetChannel(0); + } + else + CGUIControl::OnDown(); + } + else + { + if (m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset) < + (m_blockOffset + m_blocksPerPage - 1)) + { + // this is not last item on page + SetItem(GetNextItem()); + UpdateBlock(); + return; + } + else if ((m_blockOffset != m_gridModel->GridItemsSize() - m_blocksPerPage) && + m_gridModel->GridItemsSize() > m_blocksPerPage && + m_blockOffset + BLOCK_SCROLL_OFFSET < m_gridModel->GetLastBlock()) + { + // this is the last item on page + ScrollToBlockOffset(m_blockOffset + BLOCK_SCROLL_OFFSET); + UpdateBlock(); + return; + } + + CGUIControl::OnDown(); + } +} + +void CGUIEPGGridContainer::OnLeft() +{ + if (!HasData()) + return CGUIControl::OnLeft(); + + if (m_orientation == VERTICAL) + { + if (m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset) > m_blockOffset) + { + // this is not first item on page + SetItem(GetPrevItem()); + UpdateBlock(); + return; + } + else if (m_blockCursor <= 0 && m_blockOffset && m_blockOffset - BLOCK_SCROLL_OFFSET >= 0) + { + // this is the first item on page + ScrollToBlockOffset(m_blockOffset - BLOCK_SCROLL_OFFSET); + UpdateBlock(); + return; + } + + CGUIControl::OnLeft(); + } + else + { + CGUIAction action = GetAction(ACTION_MOVE_LEFT); + if (m_channelCursor > 0) + { + SetChannel(m_channelCursor - 1); + } + else if (m_channelCursor == 0 && m_channelOffset) + { + ScrollToChannelOffset(m_channelOffset - 1); + SetChannel(0); + } + else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around + { + int offset = m_gridModel->ChannelItemsSize() - m_channelsPerPage; + + if (offset < 0) + offset = 0; + + SetChannel(m_gridModel->GetLastChannel() - offset); + ScrollToChannelOffset(offset); + } + else + CGUIControl::OnLeft(); + } +} + +void CGUIEPGGridContainer::OnRight() +{ + if (!HasData()) + return CGUIControl::OnRight(); + + if (m_orientation == VERTICAL) + { + if (m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset) < + (m_blockOffset + m_blocksPerPage - 1)) + { + // this is not last item on page + SetItem(GetNextItem()); + UpdateBlock(); + return; + } + else if ((m_blockOffset != m_gridModel->GridItemsSize() - m_blocksPerPage) && + m_gridModel->GridItemsSize() > m_blocksPerPage && + m_blockOffset + BLOCK_SCROLL_OFFSET < m_gridModel->GetLastBlock()) + { + // this is the last item on page + ScrollToBlockOffset(m_blockOffset + BLOCK_SCROLL_OFFSET); + UpdateBlock(); + return; + } + + CGUIControl::OnRight(); + } + else + { + CGUIAction action = GetAction(ACTION_MOVE_RIGHT); + if (m_channelOffset + m_channelCursor < m_gridModel->GetLastChannel()) + { + if (m_channelCursor + 1 < m_channelsPerPage) + { + SetChannel(m_channelCursor + 1); + } + else + { + ScrollToChannelOffset(m_channelOffset + 1); + SetChannel(m_channelsPerPage - 1); + } + } + else if (action.GetNavigation() == GetID() || !action.HasActionsMeetingCondition()) // wrap around + { + SetChannel(0); + ScrollToChannelOffset(0); + } + else + CGUIControl::OnRight(); + } +} + +bool CGUIEPGGridContainer::SetChannel(const std::string& channel) +{ + for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++) + { + std::string strPath = m_gridModel->GetChannelItem(iIndex)->GetProperty("path").asString(); + if (strPath == channel) + { + GoToChannel(iIndex); + return true; + } + } + return false; +} + +bool CGUIEPGGridContainer::SetChannel(const std::shared_ptr<CPVRChannel>& channel) +{ + for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++) + { + int iChannelId = static_cast<int>(m_gridModel->GetChannelItem(iIndex)->GetProperty("channelid").asInteger(-1)); + if (iChannelId == channel->ChannelID()) + { + GoToChannel(iIndex); + return true; + } + } + return false; +} + +bool CGUIEPGGridContainer::SetChannel(const CPVRChannelNumber& channelNumber) +{ + for (int iIndex = 0; iIndex < m_gridModel->ChannelItemsSize(); iIndex++) + { + const CPVRChannelNumber& number = + m_gridModel->GetChannelItem(iIndex)->GetPVRChannelGroupMemberInfoTag()->ChannelNumber(); + if (number == channelNumber) + { + GoToChannel(iIndex); + return true; + } + } + return false; +} + +void CGUIEPGGridContainer::SetChannel(int channel) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + int channelIndex = channel + m_channelOffset; + int blockIndex = m_blockCursor + m_blockOffset; + if (channelIndex < m_gridModel->ChannelItemsSize() && blockIndex < m_gridModel->GridItemsSize()) + { + if (SetItem(m_gridModel->GetGridItem(channelIndex, m_blockTravelAxis), channelIndex, + m_blockTravelAxis)) + { + m_channelCursor = channel; + MarkDirtyRegion(); + UpdateBlock(false); + } + } +} + +void CGUIEPGGridContainer::SetBlock(int block, bool bUpdateBlockTravelAxis /* = true */) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (block < 0) + m_blockCursor = 0; + else if (block > m_blocksPerPage - 1) + m_blockCursor = m_blocksPerPage - 1; + else + m_blockCursor = block; + + if (bUpdateBlockTravelAxis) + m_blockTravelAxis = m_blockOffset + m_blockCursor; + + UpdateItem(); + MarkDirtyRegion(); +} + +void CGUIEPGGridContainer::UpdateBlock(bool bUpdateBlockTravelAxis /* = true */) +{ + SetBlock(m_itemStartBlock > 0 ? m_itemStartBlock - m_blockOffset : 0, bUpdateBlockTravelAxis); +} + +CGUIListItemLayout* CGUIEPGGridContainer::GetFocusedLayout() const +{ + CGUIListItemPtr item = GetListItem(0); + + if (item) + return item->GetFocusedLayout(); + + return nullptr; +} + +bool CGUIEPGGridContainer::SelectItemFromPoint(const CPoint& point, bool justGrid /* = false */) +{ + /* point has already had origin set to m_posX, m_posY */ + if (!m_focusedProgrammeLayout || !m_programmeLayout || (justGrid && point.x < 0)) + return false; + + int channel; + int block; + + if (m_orientation == VERTICAL) + { + channel = point.y / m_channelHeight; + block = point.x / m_blockSize; + } + else + { + channel = point.x / m_channelWidth; + block = point.y / m_blockSize; + } + + if (channel > m_channelsPerPage) + channel = m_channelsPerPage - 1; + + if (channel >= m_gridModel->ChannelItemsSize()) + channel = m_gridModel->GetLastChannel(); + + if (channel < 0) + channel = 0; + + if (block > m_blocksPerPage) + block = m_blocksPerPage - 1; + + if (block < 0) + block = 0; + + int channelIndex = channel + m_channelOffset; + int blockIndex = block + m_blockOffset; + + // bail if out of range + if (channelIndex >= m_gridModel->ChannelItemsSize() || blockIndex >= m_gridModel->GridItemsSize()) + return false; + + // bail if block isn't occupied + if (!m_gridModel->GetGridItem(channelIndex, blockIndex)) + return false; + + SetChannel(channel); + SetBlock(block); + return true; +} + +EVENT_RESULT CGUIEPGGridContainer::OnMouseEvent(const CPoint& point, const CMouseEvent& event) +{ + switch (event.m_id) + { + case ACTION_MOUSE_LEFT_CLICK: + OnMouseClick(0, point); + return EVENT_RESULT_HANDLED; + case ACTION_MOUSE_RIGHT_CLICK: + OnMouseClick(1, point); + return EVENT_RESULT_HANDLED; + case ACTION_MOUSE_DOUBLE_CLICK: + OnMouseDoubleClick(0, point); + return EVENT_RESULT_HANDLED; + case ACTION_MOUSE_WHEEL_UP: + OnMouseWheel(-1, point); + return EVENT_RESULT_HANDLED; + case ACTION_MOUSE_WHEEL_DOWN: + OnMouseWheel(1, point); + return EVENT_RESULT_HANDLED; + case ACTION_GESTURE_BEGIN: + { + // we want exclusive access + CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, GetID(), GetParentID()); + SendWindowMessage(msg); + return EVENT_RESULT_HANDLED; + } + case ACTION_GESTURE_END: + case ACTION_GESTURE_ABORT: + { + // we're done with exclusive access + CGUIMessage msg(GUI_MSG_EXCLUSIVE_MOUSE, 0, GetParentID()); + SendWindowMessage(msg); + ScrollToChannelOffset(MathUtils::round_int( + static_cast<double>(m_channelScrollOffset / m_channelLayout->Size(m_orientation)))); + SetChannel(m_channelCursor); + ScrollToBlockOffset( + MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize))); + SetBlock(m_blockCursor); + return EVENT_RESULT_HANDLED; + } + case ACTION_GESTURE_PAN: + { + m_programmeScrollOffset -= event.m_offsetX; + m_channelScrollOffset -= event.m_offsetY; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_channelOffset = MathUtils::round_int( + static_cast<double>(m_channelScrollOffset / m_channelLayout->Size(m_orientation))); + m_blockOffset = + MathUtils::round_int(static_cast<double>(m_programmeScrollOffset / m_blockSize)); + ValidateOffset(); + } + return EVENT_RESULT_HANDLED; + } + default: + return EVENT_RESULT_UNHANDLED; + } +} + +bool CGUIEPGGridContainer::OnMouseOver(const CPoint& point) +{ + // select the item under the pointer + SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY), false); + return CGUIControl::OnMouseOver(point); +} + +bool CGUIEPGGridContainer::OnMouseClick(int dwButton, const CPoint& point) +{ + if (SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY))) + { + // send click message to window + OnClick(ACTION_MOUSE_LEFT_CLICK + dwButton); + return true; + } + return false; +} + +bool CGUIEPGGridContainer::OnMouseDoubleClick(int dwButton, const CPoint& point) +{ + if (SelectItemFromPoint(point - CPoint(m_gridPosX, m_gridPosY))) + { + // send double click message to window + OnClick(ACTION_MOUSE_DOUBLE_CLICK + dwButton); + return true; + } + return false; +} + +bool CGUIEPGGridContainer::OnClick(int actionID) +{ + int subItem = 0; + + if (actionID == ACTION_SELECT_ITEM || actionID == ACTION_MOUSE_LEFT_CLICK) + { + // grab the currently focused subitem (if applicable) + CGUIListItemLayout* focusedLayout = GetFocusedLayout(); + + if (focusedLayout) + subItem = focusedLayout->GetFocusedItem(); + } + + // Don't know what to do, so send to our parent window. + CGUIMessage msg(GUI_MSG_CLICKED, GetID(), GetParentID(), actionID, subItem); + return SendWindowMessage(msg); +} + +bool CGUIEPGGridContainer::OnMouseWheel(char wheel, const CPoint& point) +{ + // doesn't work while an item is selected? + ProgrammesScroll(-wheel); + return true; +} + +std::shared_ptr<CPVRChannelGroupMember> CGUIEPGGridContainer::GetSelectedChannelGroupMember() const +{ + CFileItemPtr fileItem; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_channelCursor + m_channelOffset < m_gridModel->ChannelItemsSize()) + fileItem = m_gridModel->GetChannelItem(m_channelCursor + m_channelOffset); + } + + if (fileItem) + return fileItem->GetPVRChannelGroupMemberInfoTag(); + + return {}; +} + +CDateTime CGUIEPGGridContainer::GetSelectedDate() const +{ + return m_gridModel->GetStartTimeForBlock(m_blockOffset + m_blockCursor); +} + +CFileItemPtr CGUIEPGGridContainer::GetSelectedGridItem(int offset /*= 0*/) const +{ + CFileItemPtr item; + + if (m_channelCursor + m_channelOffset + offset < m_gridModel->ChannelItemsSize() && + m_blockCursor + m_blockOffset < m_gridModel->GridItemsSize()) + item = m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset); + + return item; +} + + +CGUIListItemPtr CGUIEPGGridContainer::GetListItem(int offset, unsigned int flag) const +{ + if (!m_gridModel->HasChannelItems()) + return CGUIListItemPtr(); + + int item = m_channelCursor + m_channelOffset + offset; + if (flag & INFOFLAG_LISTITEM_POSITION) + item = GetChannelScrollOffset(m_channelLayout); + + if (flag & INFOFLAG_LISTITEM_WRAP) + { + item %= m_gridModel->ChannelItemsSize(); + if (item < 0) + item += m_gridModel->ChannelItemsSize(); + + return m_gridModel->GetChannelItem(item); + } + else + { + if (item >= 0 && item < m_gridModel->ChannelItemsSize()) + return m_gridModel->GetChannelItem(item); + } + return CGUIListItemPtr(); +} + +std::string CGUIEPGGridContainer::GetLabel(int info) const +{ + std::string label; + switch (info) + { + case CONTAINER_NUM_PAGES: + if (m_channelsPerPage > 0) + label = std::to_string((m_gridModel->ChannelItemsSize() + m_channelsPerPage - 1) / + m_channelsPerPage); + else + label = std::to_string(0); + break; + case CONTAINER_CURRENT_PAGE: + if (m_channelsPerPage > 0) + label = std::to_string(1 + (m_channelCursor + m_channelOffset) / m_channelsPerPage); + else + label = std::to_string(1); + break; + case CONTAINER_POSITION: + label = std::to_string(1 + m_channelCursor + m_channelOffset); + break; + case CONTAINER_NUM_ITEMS: + label = std::to_string(m_gridModel->ChannelItemsSize()); + break; + default: + break; + } + return label; +} + +void CGUIEPGGridContainer::SetItem(const std::pair<std::shared_ptr<CFileItem>, int>& itemInfo) +{ + SetItem(itemInfo.first, m_channelCursor + m_channelOffset, itemInfo.second); +} + +bool CGUIEPGGridContainer::SetItem(const std::shared_ptr<CFileItem>& item, + int channelIndex, + int blockIndex) +{ + if (item && channelIndex < m_gridModel->ChannelItemsSize() && + blockIndex < m_gridModel->GridItemsSize()) + { + m_itemStartBlock = m_gridModel->GetGridItemStartBlock(channelIndex, blockIndex); + return true; + } + else + { + m_itemStartBlock = 0; + return false; + } +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainer::GetItem() const +{ + const int channelIndex = m_channelCursor + m_channelOffset; + const int blockIndex = m_blockCursor + m_blockOffset; + + if (channelIndex >= m_gridModel->ChannelItemsSize() || blockIndex >= m_gridModel->GridItemsSize()) + return {}; + + return m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset); +} + +std::pair<std::shared_ptr<CFileItem>, int> CGUIEPGGridContainer::GetNextItem() const +{ + int block = m_gridModel->GetGridItemEndBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset); + if (block < m_gridModel->GridItemsSize()) + { + // first block of next event is one block after end block of selected event + block += 1; + } + + return {m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, block), block}; +} + +std::pair<std::shared_ptr<CFileItem>, int> CGUIEPGGridContainer::GetPrevItem() const +{ + int block = m_gridModel->GetGridItemStartBlock(m_channelCursor + m_channelOffset, + m_blockCursor + m_blockOffset); + if (block > 0) + { + // last block of previous event is one block before start block of selected event + block -= 1; + } + + return {m_gridModel->GetGridItem(m_channelCursor + m_channelOffset, block), block}; +} + +void CGUIEPGGridContainer::UpdateItem() +{ + SetItem(GetItem(), m_channelCursor + m_channelOffset, m_blockCursor + m_blockOffset); +} + +void CGUIEPGGridContainer::SetFocus(bool focus) +{ + if (focus != HasFocus()) + SetInvalid(); + + CGUIControl::SetFocus(focus); +} + +void CGUIEPGGridContainer::ScrollToChannelOffset(int offset) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + float size = m_programmeLayout->Size(m_orientation); + int range = m_channelsPerPage / 4; + + if (range <= 0) + range = 1; + + if (offset * size < m_channelScrollOffset && m_channelScrollOffset - offset * size > size * range) + { + // scrolling up, and we're jumping more than 0.5 of a screen + m_channelScrollOffset = (offset + range) * size; + } + + if (offset * size > m_channelScrollOffset && offset * size - m_channelScrollOffset > size * range) + { + // scrolling down, and we're jumping more than 0.5 of a screen + m_channelScrollOffset = (offset - range) * size; + } + + m_channelScrollSpeed = (offset * size - m_channelScrollOffset) / m_scrollTime; + m_channelOffset = offset; + MarkDirtyRegion(); +} + +void CGUIEPGGridContainer::ScrollToBlockOffset(int offset) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // make sure offset is in valid range + offset = std::max(0, std::min(offset, m_gridModel->GridItemsSize() - m_blocksPerPage)); + + float size = m_blockSize; + int range = m_blocksPerPage / 1; + + if (range <= 0) + range = 1; + + if (offset * size < m_programmeScrollOffset && m_programmeScrollOffset - offset * size > size * range) + { + // scrolling left, and we're jumping more than 0.5 of a screen + m_programmeScrollOffset = (offset + range) * size; + } + + if (offset * size > m_programmeScrollOffset && offset * size - m_programmeScrollOffset > size * range) + { + // scrolling right, and we're jumping more than 0.5 of a screen + m_programmeScrollOffset = (offset - range) * size; + } + + m_programmeScrollSpeed = (offset * size - m_programmeScrollOffset) / m_scrollTime; + m_blockOffset = offset; + MarkDirtyRegion(); +} + +void CGUIEPGGridContainer::ValidateOffset() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (!m_programmeLayout) + return; + + float pos = (m_orientation == VERTICAL) ? m_channelHeight : m_channelWidth; + + if (m_gridModel->ChannelItemsSize() && + (m_channelOffset > m_gridModel->ChannelItemsSize() - m_channelsPerPage || + m_channelScrollOffset > (m_gridModel->ChannelItemsSize() - m_channelsPerPage) * pos)) + { + m_channelOffset = m_gridModel->ChannelItemsSize() - m_channelsPerPage; + m_channelScrollOffset = m_channelOffset * pos; + } + + if (m_channelOffset < 0 || m_channelScrollOffset < 0) + { + m_channelOffset = 0; + m_channelScrollOffset = 0; + } + + if (m_gridModel->GridItemsSize() && + (m_blockOffset > m_gridModel->GridItemsSize() - m_blocksPerPage || + m_programmeScrollOffset > (m_gridModel->GridItemsSize() - m_blocksPerPage) * m_blockSize)) + { + m_blockOffset = m_gridModel->GridItemsSize() - m_blocksPerPage; + m_programmeScrollOffset = m_blockOffset * m_blockSize; + } + + if (m_blockOffset < 0 || m_programmeScrollOffset < 0) + { + m_blockOffset = 0; + m_programmeScrollOffset = 0; + } +} + +void CGUIEPGGridContainer::LoadLayout(TiXmlElement* layout) +{ + /* layouts for the channel column */ + TiXmlElement* itemElement = layout->FirstChildElement("channellayout"); + while (itemElement) + { + m_channelLayouts.emplace_back(); + m_channelLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height); + itemElement = itemElement->NextSiblingElement("channellayout"); + } + itemElement = layout->FirstChildElement("focusedchannellayout"); + while (itemElement) + { + m_focusedChannelLayouts.emplace_back(); + m_focusedChannelLayouts.back().LoadLayout(itemElement, GetParentID(), true, m_width, m_height); + itemElement = itemElement->NextSiblingElement("focusedchannellayout"); + } + + /* layouts for the grid items */ + itemElement = layout->FirstChildElement("focusedlayout"); + while (itemElement) + { + m_focusedProgrammeLayouts.emplace_back(); + m_focusedProgrammeLayouts.back().LoadLayout(itemElement, GetParentID(), true, m_width, m_height); + itemElement = itemElement->NextSiblingElement("focusedlayout"); + } + itemElement = layout->FirstChildElement("itemlayout"); + while (itemElement) + { + m_programmeLayouts.emplace_back(); + m_programmeLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height); + itemElement = itemElement->NextSiblingElement("itemlayout"); + } + + /* layout for the date label for the grid */ + itemElement = layout->FirstChildElement("rulerdatelayout"); + while (itemElement) + { + m_rulerDateLayouts.emplace_back(); + m_rulerDateLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height); + itemElement = itemElement->NextSiblingElement("rulerdatelayout"); + } + + /* layout for the timeline for the grid */ + itemElement = layout->FirstChildElement("rulerlayout"); + while (itemElement) + { + m_rulerLayouts.emplace_back(); + m_rulerLayouts.back().LoadLayout(itemElement, GetParentID(), false, m_width, m_height); + itemElement = itemElement->NextSiblingElement("rulerlayout"); + } + + UpdateLayout(); +} + +std::string CGUIEPGGridContainer::GetDescription() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + const int channelIndex = m_channelCursor + m_channelOffset; + const int blockIndex = m_blockCursor + m_blockOffset; + + if (channelIndex < m_gridModel->ChannelItemsSize() && blockIndex < m_gridModel->GridItemsSize()) + { + const std::shared_ptr<CFileItem> item = m_gridModel->GetGridItem(channelIndex, blockIndex); + if (item) + return item->GetLabel(); + } + + return {}; +} + +void CGUIEPGGridContainer::JumpToNow() +{ + m_bEnableProgrammeScrolling = false; + GoToNow(); +} + +void CGUIEPGGridContainer::JumpToDate(const CDateTime& date) +{ + m_bEnableProgrammeScrolling = false; + GoToDate(date); +} + +void CGUIEPGGridContainer::GoToBegin() +{ + ScrollToBlockOffset(0); + SetBlock(0); +} + +void CGUIEPGGridContainer::GoToEnd() +{ + ScrollToBlockOffset(m_gridModel->GetLastBlock() - m_blocksPerPage + 1); + SetBlock(m_blocksPerPage - 1); +} + +void CGUIEPGGridContainer::GoToNow() +{ + GoToDate(CDateTime::GetUTCDateTime()); +} + +void CGUIEPGGridContainer::GoToDate(const CDateTime& date) +{ + unsigned int offset = m_gridModel->GetPageNowOffset(); + ScrollToBlockOffset(m_gridModel->GetBlock(date) - offset); + + // ensure we're selecting the active event, not its predecessor. + const int iChannel = m_channelOffset + m_channelCursor; + const int iBlock = m_blockOffset + offset; + if (iChannel >= m_gridModel->ChannelItemsSize() || iBlock >= m_gridModel->GridItemsSize() || + m_gridModel->GetGridItemEndTime(iChannel, iBlock) > date) + { + SetBlock(offset); + } + else + { + SetBlock(offset + 1); + } +} + +void CGUIEPGGridContainer::GoToFirstChannel() +{ + GoToChannel(0); +} + +void CGUIEPGGridContainer::GoToLastChannel() +{ + if (m_gridModel->ChannelItemsSize()) + GoToChannel(m_gridModel->GetLastChannel()); + else + GoToChannel(0); +} + +void CGUIEPGGridContainer::GoToTop() +{ + if (m_orientation == VERTICAL) + { + GoToChannel(0); + } + else + { + GoToBlock(0); + } +} + +void CGUIEPGGridContainer::GoToBottom() +{ + if (m_orientation == VERTICAL) + { + if (m_gridModel->HasChannelItems()) + GoToChannel(m_gridModel->GetLastChannel()); + else + GoToChannel(0); + } + else + { + if (m_gridModel->GridItemsSize()) + GoToBlock(m_gridModel->GetLastBlock()); + else + GoToBlock(0); + } +} + +void CGUIEPGGridContainer::GoToMostLeft() +{ + if (m_orientation == VERTICAL) + { + GoToBlock(0); + } + else + { + GoToChannel(0); + } +} + +void CGUIEPGGridContainer::GoToMostRight() +{ + if (m_orientation == VERTICAL) + { + if (m_gridModel->GridItemsSize()) + GoToBlock(m_gridModel->GetLastBlock()); + else + GoToBlock(0); + } + else + { + if (m_gridModel->HasChannelItems()) + GoToChannel(m_gridModel->GetLastChannel()); + else + GoToChannel(0); + } +} + +void CGUIEPGGridContainer::SetTimelineItems(const std::unique_ptr<CFileItemList>& items, + const CDateTime& gridStart, + const CDateTime& gridEnd) +{ + int iRulerUnit; + int iFirstChannel; + int iChannelsPerPage; + int iBlocksPerPage; + int iFirstBlock; + float fBlockSize; + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + iRulerUnit = m_rulerUnit; + iFirstChannel = m_channelOffset; + iChannelsPerPage = m_channelsPerPage; + iFirstBlock = m_blockOffset; + iBlocksPerPage = m_blocksPerPage; + fBlockSize = m_blockSize; + } + + std::unique_ptr<CGUIEPGGridContainerModel> oldUpdatedGridModel; + std::unique_ptr<CGUIEPGGridContainerModel> newUpdatedGridModel(new CGUIEPGGridContainerModel); + + newUpdatedGridModel->Initialize(items, gridStart, gridEnd, iFirstChannel, iChannelsPerPage, + iFirstBlock, iBlocksPerPage, iRulerUnit, fBlockSize); + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + // grid contains CFileItem instances. CFileItem dtor locks global graphics mutex. + // by increasing its refcount make sure, old data are not deleted while we're holding own mutex. + oldUpdatedGridModel = std::move(m_updatedGridModel); + + m_updatedGridModel = std::move(newUpdatedGridModel); + } +} + +std::unique_ptr<CFileItemList> CGUIEPGGridContainer::GetCurrentTimeLineItems() const +{ + return m_gridModel->GetCurrentTimeLineItems(m_channelOffset, m_channelsPerPage); +} + +void CGUIEPGGridContainer::GoToChannel(int channelIndex) +{ + if (channelIndex < m_channelsPerPage) + { + // first page + ScrollToChannelOffset(0); + SetChannel(channelIndex); + } + else if (channelIndex > m_gridModel->ChannelItemsSize() - m_channelsPerPage) + { + // last page + ScrollToChannelOffset(m_gridModel->ChannelItemsSize() - m_channelsPerPage); + SetChannel(channelIndex - (m_gridModel->ChannelItemsSize() - m_channelsPerPage)); + } + else + { + ScrollToChannelOffset(channelIndex - m_channelCursor); + SetChannel(m_channelCursor); + } +} + +void CGUIEPGGridContainer::GoToBlock(int blockIndex) +{ + int lastPage = m_gridModel->GridItemsSize() - m_blocksPerPage; + if (blockIndex > lastPage) + { + // last page + ScrollToBlockOffset(lastPage); + SetBlock(blockIndex - lastPage); + } + else + { + ScrollToBlockOffset(blockIndex - m_blockCursor); + SetBlock(m_blockCursor); + } +} + +void CGUIEPGGridContainer::UpdateLayout() +{ + CGUIListItemLayout* oldFocusedChannelLayout = m_focusedChannelLayout; + CGUIListItemLayout* oldChannelLayout = m_channelLayout; + CGUIListItemLayout* oldFocusedProgrammeLayout = m_focusedProgrammeLayout; + CGUIListItemLayout* oldProgrammeLayout = m_programmeLayout; + CGUIListItemLayout* oldRulerLayout = m_rulerLayout; + CGUIListItemLayout* oldRulerDateLayout = m_rulerDateLayout; + + GetCurrentLayouts(); + + // Note: m_rulerDateLayout is optional + if (!m_focusedProgrammeLayout || !m_programmeLayout || !m_focusedChannelLayout || !m_channelLayout || !m_rulerLayout) + return; + + if (oldChannelLayout == m_channelLayout && oldFocusedChannelLayout == m_focusedChannelLayout && + oldProgrammeLayout == m_programmeLayout && oldFocusedProgrammeLayout == m_focusedProgrammeLayout && + oldRulerLayout == m_rulerLayout && oldRulerDateLayout == m_rulerDateLayout) + return; // nothing has changed, so don't update stuff + + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_channelHeight = m_channelLayout->Size(VERTICAL); + m_channelWidth = m_channelLayout->Size(HORIZONTAL); + + m_rulerDateHeight = m_rulerDateLayout ? m_rulerDateLayout->Size(VERTICAL) : 0; + m_rulerDateWidth = m_rulerDateLayout ? m_rulerDateLayout->Size(HORIZONTAL) : 0; + + if (m_orientation == VERTICAL) + { + m_rulerHeight = m_rulerLayout->Size(VERTICAL); + m_gridPosX = m_posX + m_channelWidth; + m_gridPosY = m_posY + m_rulerHeight + m_rulerDateHeight; + m_gridWidth = m_width - m_channelWidth; + m_gridHeight = m_height - m_rulerHeight - m_rulerDateHeight; + m_blockSize = m_gridWidth / m_blocksPerPage; + m_rulerWidth = m_rulerUnit * m_blockSize; + m_channelPosX = m_posX; + m_channelPosY = m_posY + m_rulerHeight + m_rulerDateHeight; + m_rulerPosX = m_posX + m_channelWidth; + m_rulerPosY = m_posY + m_rulerDateHeight; + m_channelsPerPage = m_gridHeight / m_channelHeight; + m_programmesPerPage = (m_gridWidth / m_blockSize) + 1; + + m_programmeLayout->SetHeight(m_channelHeight); + m_focusedProgrammeLayout->SetHeight(m_channelHeight); + } + else + { + m_rulerWidth = m_rulerLayout->Size(HORIZONTAL); + m_gridPosX = m_posX + m_rulerWidth; + m_gridPosY = m_posY + m_channelHeight + m_rulerDateHeight; + m_gridWidth = m_width - m_rulerWidth; + m_gridHeight = m_height - m_channelHeight - m_rulerDateHeight; + m_blockSize = m_gridHeight / m_blocksPerPage; + m_rulerHeight = m_rulerUnit * m_blockSize; + m_channelPosX = m_posX + m_rulerWidth; + m_channelPosY = m_posY + m_rulerDateHeight; + m_rulerPosX = m_posX; + m_rulerPosY = m_posY + m_channelHeight + m_rulerDateHeight; + m_channelsPerPage = m_gridWidth / m_channelWidth; + m_programmesPerPage = (m_gridHeight / m_blockSize) + 1; + + m_programmeLayout->SetWidth(m_channelWidth); + m_focusedProgrammeLayout->SetWidth(m_channelWidth); + } + + // ensure that the scroll offsets are a multiple of our sizes + m_channelScrollOffset = m_channelOffset * m_programmeLayout->Size(m_orientation); + m_programmeScrollOffset = m_blockOffset * m_blockSize; +} + +void CGUIEPGGridContainer::UpdateScrollOffset(unsigned int currentTime) +{ + if (!m_programmeLayout) + return; + + m_channelScrollOffset += m_channelScrollSpeed * (currentTime - m_channelScrollLastTime); + if ((m_channelScrollSpeed < 0 && m_channelScrollOffset < m_channelOffset * m_programmeLayout->Size(m_orientation)) || + (m_channelScrollSpeed > 0 && m_channelScrollOffset > m_channelOffset * m_programmeLayout->Size(m_orientation))) + { + m_channelScrollOffset = m_channelOffset * m_programmeLayout->Size(m_orientation); + m_channelScrollSpeed = 0; + m_bEnableChannelScrolling = true; + } + + m_channelScrollLastTime = currentTime; + m_programmeScrollOffset += m_programmeScrollSpeed * (currentTime - m_programmeScrollLastTime); + + if ((m_programmeScrollSpeed < 0 && m_programmeScrollOffset < m_blockOffset * m_blockSize) || + (m_programmeScrollSpeed > 0 && m_programmeScrollOffset > m_blockOffset * m_blockSize)) + { + m_programmeScrollOffset = m_blockOffset * m_blockSize; + m_programmeScrollSpeed = 0; + m_bEnableProgrammeScrolling = true; + } + + m_programmeScrollLastTime = currentTime; + + if (m_channelScrollSpeed || m_programmeScrollSpeed) + MarkDirtyRegion(); +} + +void CGUIEPGGridContainer::GetCurrentLayouts() +{ + m_channelLayout = nullptr; + + for (unsigned int i = 0; i < m_channelLayouts.size(); i++) + { + if (m_channelLayouts[i].CheckCondition()) + { + m_channelLayout = &m_channelLayouts[i]; + break; + } + } + + if (!m_channelLayout && !m_channelLayouts.empty()) + m_channelLayout = &m_channelLayouts[0]; // failsafe + + m_focusedChannelLayout = nullptr; + + for (unsigned int i = 0; i < m_focusedChannelLayouts.size(); i++) + { + if (m_focusedChannelLayouts[i].CheckCondition()) + { + m_focusedChannelLayout = &m_focusedChannelLayouts[i]; + break; + } + } + + if (!m_focusedChannelLayout && !m_focusedChannelLayouts.empty()) + m_focusedChannelLayout = &m_focusedChannelLayouts[0]; // failsafe + + m_programmeLayout = nullptr; + + for (unsigned int i = 0; i < m_programmeLayouts.size(); i++) + { + if (m_programmeLayouts[i].CheckCondition()) + { + m_programmeLayout = &m_programmeLayouts[i]; + break; + } + } + + if (!m_programmeLayout && !m_programmeLayouts.empty()) + m_programmeLayout = &m_programmeLayouts[0]; // failsafe + + m_focusedProgrammeLayout = nullptr; + + for (unsigned int i = 0; i < m_focusedProgrammeLayouts.size(); i++) + { + if (m_focusedProgrammeLayouts[i].CheckCondition()) + { + m_focusedProgrammeLayout = &m_focusedProgrammeLayouts[i]; + break; + } + } + + if (!m_focusedProgrammeLayout && !m_focusedProgrammeLayouts.empty()) + m_focusedProgrammeLayout = &m_focusedProgrammeLayouts[0]; // failsafe + + m_rulerLayout = nullptr; + + for (unsigned int i = 0; i < m_rulerLayouts.size(); i++) + { + if (m_rulerLayouts[i].CheckCondition()) + { + m_rulerLayout = &m_rulerLayouts[i]; + break; + } + } + + if (!m_rulerLayout && !m_rulerLayouts.empty()) + m_rulerLayout = &m_rulerLayouts[0]; // failsafe + + m_rulerDateLayout = nullptr; + + for (unsigned int i = 0; i < m_rulerDateLayouts.size(); i++) + { + if (m_rulerDateLayouts[i].CheckCondition()) + { + m_rulerDateLayout = &m_rulerDateLayouts[i]; + break; + } + } + + // Note: m_rulerDateLayout is optional; so no "failsafe" logic here (see above) +} + +void CGUIEPGGridContainer::SetRenderOffset(const CPoint& offset) +{ + m_renderOffset = offset; +} + +void CGUIEPGGridContainer::GetChannelCacheOffsets(int& cacheBefore, int& cacheAfter) +{ + if (m_channelScrollSpeed > 0) + { + cacheBefore = 0; + cacheAfter = m_cacheChannelItems; + } + else if (m_channelScrollSpeed < 0) + { + cacheBefore = m_cacheChannelItems; + cacheAfter = 0; + } + else + { + cacheBefore = m_cacheChannelItems / 2; + cacheAfter = m_cacheChannelItems / 2; + } +} + +void CGUIEPGGridContainer::GetProgrammeCacheOffsets(int& cacheBefore, int& cacheAfter) +{ + if (m_programmeScrollSpeed > 0) + { + cacheBefore = 0; + cacheAfter = m_cacheProgrammeItems; + } + else if (m_programmeScrollSpeed < 0) + { + cacheBefore = m_cacheProgrammeItems; + cacheAfter = 0; + } + else + { + cacheBefore = m_cacheProgrammeItems / 2; + cacheAfter = m_cacheProgrammeItems / 2; + } +} + +void CGUIEPGGridContainer::HandleChannels(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + if (!m_focusedChannelLayout || !m_channelLayout) + return; + + const int chanOffset = GetChannelScrollOffset(m_programmeLayout); + + int cacheBeforeChannel, cacheAfterChannel; + GetChannelCacheOffsets(cacheBeforeChannel, cacheAfterChannel); + + if (bRender) + { + if (m_orientation == VERTICAL) + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_channelPosX, m_channelPosY, m_channelWidth, m_gridHeight); + else + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_channelPosX, m_channelPosY, m_gridWidth, m_channelHeight); + } + else + { + // Free memory not used on screen + if (m_gridModel->ChannelItemsSize() > m_channelsPerPage + cacheBeforeChannel + cacheAfterChannel) + m_gridModel->FreeChannelMemory(chanOffset - cacheBeforeChannel, + chanOffset + m_channelsPerPage - 1 + cacheAfterChannel); + } + + CPoint originChannel = CPoint(m_channelPosX, m_channelPosY) + m_renderOffset; + float pos; + float end; + + if (m_orientation == VERTICAL) + { + pos = originChannel.y; + end = m_posY + m_height; + } + else + { + pos = originChannel.x; + end = m_posX + m_width; + } + + // we offset our draw position to take into account scrolling and whether or not our focused + // item is offscreen "above" the list. + float drawOffset = (chanOffset - cacheBeforeChannel) * m_channelLayout->Size(m_orientation) - + GetChannelScrollOffsetPos(); + if (m_channelOffset + m_channelCursor < chanOffset) + drawOffset += m_focusedChannelLayout->Size(m_orientation) - m_channelLayout->Size(m_orientation); + + pos += drawOffset; + end += cacheAfterChannel * m_channelLayout->Size(m_orientation); + + float focusedPos = 0; + CGUIListItemPtr focusedItem; + + CFileItemPtr item; + int current = chanOffset - cacheBeforeChannel; + while (pos < end && m_gridModel->HasChannelItems()) + { + int itemNo = current; + if (itemNo >= m_gridModel->ChannelItemsSize()) + break; + + bool focused = (current == m_channelOffset + m_channelCursor); + if (itemNo >= 0) + { + item = m_gridModel->GetChannelItem(itemNo); + if (bRender) + { + // render our item + if (focused) + { + focusedPos = pos; + focusedItem = item; + } + else + { + if (m_orientation == VERTICAL) + RenderItem(originChannel.x, pos, item.get(), false); + else + RenderItem(pos, originChannel.y, item.get(), false); + } + } + else + { + // process our item + if (m_orientation == VERTICAL) + ProcessItem(originChannel.x, pos, item, m_lastItem, focused, m_channelLayout, m_focusedChannelLayout, currentTime, dirtyregions); + else + ProcessItem(pos, originChannel.y, item, m_lastItem, focused, m_channelLayout, m_focusedChannelLayout, currentTime, dirtyregions); + } + } + // increment our position + pos += focused ? m_focusedChannelLayout->Size(m_orientation) : m_channelLayout->Size(m_orientation); + current++; + } + + if (bRender) + { + // render focused item last so it can overlap other items + if (focusedItem) + { + if (m_orientation == VERTICAL) + RenderItem(originChannel.x, focusedPos, focusedItem.get(), true); + else + RenderItem(focusedPos, originChannel.y, focusedItem.get(), true); + } + + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } +} + +void CGUIEPGGridContainer::HandleRulerDate(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + if (!m_rulerDateLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration()) + return; + + CFileItemPtr item(m_gridModel->GetRulerItem(0)); + + if (bRender) + { + // Render single ruler item with date of selected programme + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_rulerDateWidth, m_rulerDateHeight); + RenderItem(m_posX, m_posY, item.get(), false); + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } + else + { + const int rulerOffset = GetProgrammeScrollOffset(); + item->SetLabel(m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1)->GetLabel2()); + + CFileItemPtr lastitem; + ProcessItem(m_posX, m_posY, item, lastitem, false, m_rulerDateLayout, m_rulerDateLayout, currentTime, dirtyregions); + } +} + +void CGUIEPGGridContainer::HandleRuler(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + if (!m_rulerLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration()) + return; + + int rulerOffset = GetProgrammeScrollOffset(); + + CFileItemPtr item(m_gridModel->GetRulerItem(0)); + CFileItemPtr lastitem; + int cacheBeforeRuler, cacheAfterRuler; + + if (bRender) + { + if (!m_rulerDateLayout) + { + // Render single ruler item with date of selected programme + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_posX, m_posY, m_width, m_height); + RenderItem(m_posX, m_posY, item.get(), false); + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } + + // render ruler items + GetProgrammeCacheOffsets(cacheBeforeRuler, cacheAfterRuler); + + if (m_orientation == VERTICAL) + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, m_gridWidth, m_rulerHeight); + else + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_rulerPosX, m_rulerPosY, m_rulerWidth, m_gridHeight); + } + else + { + if (!m_rulerDateLayout) + { + item->SetLabel(m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1)->GetLabel2()); + ProcessItem(m_posX, m_posY, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_channelWidth); + } + + GetProgrammeCacheOffsets(cacheBeforeRuler, cacheAfterRuler); + + // Free memory not used on screen + if (m_gridModel->RulerItemsSize() > m_blocksPerPage + cacheBeforeRuler + cacheAfterRuler) + m_gridModel->FreeRulerMemory(rulerOffset / m_rulerUnit + 1 - cacheBeforeRuler, + rulerOffset / m_rulerUnit + 1 + m_blocksPerPage - 1 + + cacheAfterRuler); + } + + CPoint originRuler = CPoint(m_rulerPosX, m_rulerPosY) + m_renderOffset; + float pos; + float end; + + if (m_orientation == VERTICAL) + { + pos = originRuler.x; + end = m_posX + m_width; + } + else + { + pos = originRuler.y; + end = m_posY + m_height; + } + + const float drawOffset = + (rulerOffset - cacheBeforeRuler) * m_blockSize - GetProgrammeScrollOffsetPos(); + pos += drawOffset; + end += cacheAfterRuler * m_rulerLayout->Size(m_orientation == VERTICAL ? HORIZONTAL : VERTICAL); + + if (rulerOffset % m_rulerUnit != 0) + { + /* first ruler marker starts before current view */ + int startBlock = rulerOffset - 1; + + while (startBlock % m_rulerUnit != 0) + startBlock--; + + int missingSection = rulerOffset - startBlock; + + pos -= missingSection * m_blockSize; + } + + while (pos < end && (rulerOffset / m_rulerUnit + 1) < m_gridModel->RulerItemsSize()) + { + item = m_gridModel->GetRulerItem(rulerOffset / m_rulerUnit + 1); + + if (m_orientation == VERTICAL) + { + if (bRender) + RenderItem(pos, originRuler.y, item.get(), false); + else + ProcessItem(pos, originRuler.y, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_rulerWidth); + + pos += m_rulerWidth; + } + else + { + if (bRender) + RenderItem(originRuler.x, pos, item.get(), false); + else + ProcessItem(originRuler.x, pos, item, lastitem, false, m_rulerLayout, m_rulerLayout, currentTime, dirtyregions, m_rulerHeight); + + pos += m_rulerHeight; + } + + rulerOffset += m_rulerUnit; + } + + if (bRender) + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); +} + +void CGUIEPGGridContainer::HandleProgrammeGrid(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions) +{ + if (!m_focusedProgrammeLayout || !m_programmeLayout || m_gridModel->RulerItemsSize() <= 1 || m_gridModel->IsZeroGridDuration()) + return; + + const int blockOffset = GetProgrammeScrollOffset(); + const int chanOffset = GetChannelScrollOffset(m_programmeLayout); + + int cacheBeforeProgramme, cacheAfterProgramme; + GetProgrammeCacheOffsets(cacheBeforeProgramme, cacheAfterProgramme); + + if (bRender) + { + CServiceBroker::GetWinSystem()->GetGfxContext().SetClipRegion(m_gridPosX, m_gridPosY, m_gridWidth, m_gridHeight); + } + else + { + int cacheBeforeChannel, cacheAfterChannel; + GetChannelCacheOffsets(cacheBeforeChannel, cacheAfterChannel); + + // Free memory not used on screen + int firstChannel = chanOffset - cacheBeforeChannel; + if (firstChannel < 0) + firstChannel = 0; + int lastChannel = chanOffset + m_channelsPerPage - 1 + cacheAfterChannel; + if (lastChannel > m_gridModel->GetLastChannel()) + lastChannel = m_gridModel->GetLastChannel(); + int firstBlock = blockOffset - cacheBeforeProgramme; + if (firstBlock < 0) + firstBlock = 0; + int lastBlock = blockOffset + m_programmesPerPage - 1 + cacheAfterProgramme; + if (lastBlock > m_gridModel->GetLastBlock()) + lastBlock = m_gridModel->GetLastBlock(); + + if (m_gridModel->FreeProgrammeMemory(firstChannel, lastChannel, firstBlock, lastBlock)) + { + // announce changed viewport + const CGUIMessage msg( + GUI_MSG_REFRESH_LIST, GetParentID(), GetID(), static_cast<int>(PVREvent::Epg)); + CServiceBroker::GetAppMessenger()->SendGUIMessage(msg); + } + } + + CPoint originProgramme = CPoint(m_gridPosX, m_gridPosY) + m_renderOffset; + float posA; + float endA; + float posB; + float endB; + + if (m_orientation == VERTICAL) + { + posA = originProgramme.x; + endA = m_posX + m_width; + posB = originProgramme.y; + endB = m_gridPosY + m_gridHeight; + } + else + { + posA = originProgramme.y; + endA = m_posY + m_height; + posB = originProgramme.x; + endB = m_gridPosX + m_gridWidth; + } + + endA += cacheAfterProgramme * m_blockSize; + + const float drawOffsetA = blockOffset * m_blockSize - GetProgrammeScrollOffsetPos(); + posA += drawOffsetA; + const float drawOffsetB = + (chanOffset - cacheBeforeProgramme) * m_channelLayout->Size(m_orientation) - + GetChannelScrollOffsetPos(); + posB += drawOffsetB; + + int channel = chanOffset - cacheBeforeProgramme; + + float focusedPosX = 0; + float focusedPosY = 0; + CFileItemPtr focusedItem; + CFileItemPtr item; + + const int lastChannel = m_gridModel->GetLastChannel(); + while (posB < endB && HasData() && channel <= lastChannel) + { + if (channel >= 0) + { + int block = blockOffset; + float posA2 = posA; + + const int startBlock = blockOffset == 0 ? 0 : blockOffset - 1; + if (startBlock == 0 || m_gridModel->IsSameGridItem(channel, block, startBlock)) + { + // First program starts before current view + block = m_gridModel->GetGridItemStartBlock(channel, startBlock); + const int missingSection = blockOffset - block; + posA2 -= missingSection * m_blockSize; + } + + const int lastBlock = m_gridModel->GetLastBlock(); + while (posA2 < endA && HasData() && block <= lastBlock) + { + item = m_gridModel->GetGridItem(channel, block); + + bool focused = (channel == m_channelOffset + m_channelCursor) && + m_gridModel->IsSameGridItem(m_channelOffset + m_channelCursor, + m_blockOffset + m_blockCursor, block); + + if (bRender) + { + // reset to grid start position if first item is out of grid view + if (posA2 < posA) + posA2 = posA; + + // render our item + if (focused) + { + focusedPosX = posA2; + focusedPosY = posB; + focusedItem = item; + } + else + { + if (m_orientation == VERTICAL) + RenderItem(posA2, posB, item.get(), focused); + else + RenderItem(posB, posA2, item.get(), focused); + } + } + else + { + // calculate the size to truncate if item is out of grid view + float truncateSize = 0; + if (posA2 < posA) + { + truncateSize = posA - posA2; + posA2 = posA; // reset to grid start position + } + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + // truncate item's width + m_gridModel->DecreaseGridItemWidth(channel, block, truncateSize); + } + + if (m_orientation == VERTICAL) + ProcessItem(posA2, posB, item, m_lastChannel, focused, m_programmeLayout, + m_focusedProgrammeLayout, currentTime, dirtyregions, + m_gridModel->GetGridItemWidth(channel, block)); + else + ProcessItem(posB, posA2, item, m_lastChannel, focused, m_programmeLayout, + m_focusedProgrammeLayout, currentTime, dirtyregions, + m_gridModel->GetGridItemWidth(channel, block)); + } + + // increment our X position + posA2 += m_gridModel->GetGridItemWidth( + channel, block); // assumes focused & unfocused layouts have equal length + block += MathUtils::round_int( + static_cast<double>(m_gridModel->GetGridItemOriginWidth(channel, block) / m_blockSize)); + } + } + + // increment our Y position + channel++; + posB += (m_orientation == VERTICAL) ? m_channelHeight : m_channelWidth; + } + + if (bRender) + { + // and render the focused item last (for overlapping purposes) + if (focusedItem) + { + if (m_orientation == VERTICAL) + RenderItem(focusedPosX, focusedPosY, focusedItem.get(), true); + else + RenderItem(focusedPosY, focusedPosX, focusedItem.get(), true); + } + + CServiceBroker::GetWinSystem()->GetGfxContext().RestoreClipRegion(); + } +} diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.dox b/xbmc/pvr/guilib/GUIEPGGridContainer.dox new file mode 100644 index 0000000..1916cde --- /dev/null +++ b/xbmc/pvr/guilib/GUIEPGGridContainer.dox @@ -0,0 +1,245 @@ +/*! + +\page EPGGrid_control EPGGrid control +\brief **Used to display the EPG guide in the PVR section.** + +\tableofcontents + +The epggrid control is used for creating an epg timeline in Kodi. You can choose +the position, size, and look of the grid and it's contents. + + +-------------------------------------------------------------------------------- +\section EPGGrid_control_sect1 Example + +~~~~~~~~~~~~~ +<control type="epggrid" id="10"> + <description>EPG Grid</description> + <posx>80</posx> + <posy>81</posy> + <width>1120</width> + <height>555</height> + <pagecontrol>10</pagecontrol> + <scrolltime>350</scrolltime> + <timeblocks>40</timeblocks> + <rulerunit>6</rulerunit> + <progresstexture border="5">PVR-EpgProgressIndicator.png</progresstexture> + <orienttation>vertical</orientation> + <onleft>31</onleft> + <onright>31</onright> + <onup>10</onup> + <ondown>10</ondown> + <rulerlayout height="35" width="40"> + <control type="image" id="1"> + <width>40</width> + <height>29</height> + <posx>0</posx> + <posy>0</posy> + <texture border="5">button-nofocus.png</texture> + </control> + <control type="label" id="2"> + <posx>10</posx> + <posy>0</posy> + <width>34</width> + <height>29</height> + <font>font12</font> + <aligny>center</aligny> + <selectedcolor>selected</selectedcolor> + <align>left</align> + <label>$INFO[ListItem.Label]</label> + </control> + </rulerlayout> + <channellayout height="52" width="280"> + <animation effect="fade" start="110" time="200">UnFocus</animation> + <control type="image" id="1"> + <posx>0</posx> + <posy>0</posy> + <width>270</width> + <height>52</height> + <texture border="5">button-nofocus.png</texture> + </control> + <control type="label"> + <posx>5</posx> + <posy>5</posy> + <width>40</width> + <height>35</height> + <font>font12</font> + <align>left</align> + <aligny>center</aligny> + <textcolor>grey</textcolor> + <selectedcolor>grey</selectedcolor> + <info>ListItem.ChannelNumber</info> + </control> + <control type="image"> + <posx>45</posx> + <posy>4</posy> + <width>45</width> + <height>44</height> + <texture>$INFO[ListItem.Icon]</texture> + </control> + <control type="label" id="1"> + <posx>94</posx> + <posy>0</posy> + <width>160</width> + <height>52</height> + <font>special12</font> + <aligny>center</aligny> + <selectedcolor>selected</selectedcolor> + <align>left</align> + <label>$INFO[ListItem.ChannelName]</label> + </control> + </channellayout> + <focusedchannellayout height="52" width="280"> + <animation effect="fade" start="110" time="200">OnFocus</animation> + <control type="image" id="1"> + <posx>0</posx> + <posy>0</posy> + <width>270</width> + <height>52</height> + <texture border="5">button-focus.png</texture> + </control> + <control type="label"> + <posx>5</posx> + <posy>5</posy> + <width>40</width> + <height>35</height> + <font>font12</font> + <align>left</align> + <aligny>center</aligny> + <textcolor>grey</textcolor> + <selectedcolor>grey</selectedcolor> + <info>ListItem.ChannelNumber</info> + </control> + <control type="image"> + <posx>45</posx> + <posy>4</posy> + <width>45</width> + <height>44</height> + <texture>$INFO[ListItem.Icon]</texture> + </control> + <control type="label" id="1"> + <posx>94</posx> + <posy>0</posy> + <width>160</width> + <height>52</height> + <font>special12</font> + <aligny>center</aligny> + <selectedcolor>selected</selectedcolor> + <align>left</align> + <label>$INFO[ListItem.ChannelName]</label> + </control> + </focusedchannellayout> + <itemlayout height="52" width="40"> + <control type="image" id="2"> + <width>40</width> + <height>52</height> + <posx>0</posx> + <posy>0</posy> + <aspectratio>stretch</aspectratio> + <texture border="3">epg-genres/$INFO[ListItem.Property(GenreType)].png</texture> + </control> + <control type="label" id="1"> + <posx>6</posx> + <posy>3</posy> + <width>30</width> + <height>25</height> + <font>font12</font> + <aligny>center</aligny> + <selectedcolor>selected</selectedcolor> + <align>left</align> + <info>ListItem.Label</info> + </control> + <control type="image"> + <posx>5</posx> + <posy>28</posy> + <width>30</width> + <height>20</height> + <texture>PVR-IsRecording.png</texture> + <visible>ListItem.IsRecording</visible> + </control> + <control type="image"> + <posx>5</posx> + <posy>28</posy> + <width>20</width> + <height>20</height> + <texture>PVR-HasTimer.png</texture> + <visible>ListItem.HasTimer + !ListItem.IsRecording</visible> + </control> + </itemlayout> + <focusedlayout height="52" width="40"> + <control type="image" id="14"> + <width>40</width> + <height>52</height> + <posx>0</posx> + <posy>0</posy> + <texture border="5">folder-focus.png</texture> + </control> + <control type="image" id="2"> + <width>40</width> + <height>52</height> + <posx>0</posx> + <posy>0</posy> + <aspectratio>stretch</aspectratio> + <texture border="3">epg-genres/$INFO[ListItem.Property(GenreType)].png</texture> + </control> + <control type="label" id="1"> + <posx>6</posx> + <posy>3</posy> + <width>30</width> + <height>25</height> + <font>font12</font> + <aligny>center</aligny> + <selectedcolor>selected</selectedcolor> + <align>left</align> + <info>ListItem.Label</info> + </control> + <control type="image"> + <posx>5</posx> + <posy>28</posy> + <width>30</width> + <height>20</height> + <texture>PVR-IsRecording.png</texture> + <visible>ListItem.IsRecording</visible> + </control> + <control type="image"> + <posx>5</posx> + <posy>28</posy> + <width>20</width> + <height>20</height> + <texture>PVR-HasTimer.png</texture> + <visible>ListItem.HasTimer + !ListItem.IsRecording</visible> + </control> + </focusedlayout> +</control> +~~~~~~~~~~~~~ + + +-------------------------------------------------------------------------------- +\section EPGGrid_control_sect2 Available tags + +In addition to the [Default Control Tags](http://kodi.wiki/view/Default_Control_Tags) +the following tags are available. Note that each tag is **lower case** only. This is +important, as `xml` tags are case-sensitive. + +| Tag | Description | +|----------------------:|:--------------------------------------------------------------| +| timeblocks | The number of timeframes on the ruler. +| rulerunit | Timeframe of each unit on ruler. 1 unit equals 5 minutes. +| rulerdatelayout | The layout of a ruler date item (usually used to display the start date of current epg page). +| rulerlayout | The layout of a ruler item. +| progresstexture | A texture which indicates the current progress time +| channellayout | The layout of a channel item. +| focusedchannellayout | The focused layout of a channel item. +| itemlayout | The layout of the grid +| focusedlayout | The focused layout of the grid + + +-------------------------------------------------------------------------------- +\section EPGGrid_control_sect3 See also + +#### Development: + +- [Add-on development](http://kodi.wiki/view/Add-on_development) +- [Skinning](http://kodi.wiki/view/Skinning) + +*/ diff --git a/xbmc/pvr/guilib/GUIEPGGridContainer.h b/xbmc/pvr/guilib/GUIEPGGridContainer.h new file mode 100644 index 0000000..360ade9 --- /dev/null +++ b/xbmc/pvr/guilib/GUIEPGGridContainer.h @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "guilib/DirtyRegion.h" +#include "guilib/GUIControl.h" +#include "guilib/GUIListItemLayout.h" +#include "guilib/GUITexture.h" +#include "guilib/IGUIContainer.h" +#include "threads/CriticalSection.h" +#include "utils/Geometry.h" + +#include <memory> +#include <string> +#include <utility> +#include <vector> + +class CDateTime; +class CFileItem; +class CFileItemList; +class CGUIListItem; +class CGUIListItemLayout; + +namespace PVR +{ + class CPVRChannel; + class CPVRChannelGroupMember; + class CPVRChannelNumber; + + class CGUIEPGGridContainerModel; + + class CGUIEPGGridContainer : public IGUIContainer + { + public: + CGUIEPGGridContainer(int parentID, int controlID, float posX, float posY, float width, float height, + ORIENTATION orientation, int scrollTime, int preloadItems, int minutesPerPage, + int rulerUnit, const CTextureInfo& progressIndicatorTexture); + CGUIEPGGridContainer(const CGUIEPGGridContainer& other); + + CGUIEPGGridContainer* Clone() const override { return new CGUIEPGGridContainer(*this); } + + /*! + * @brief Check whether the control currently holds data. + * @return true if the control has data, false otherwise. + */ + bool HasData() const; + + void AllocResources() override; + void FreeResources(bool immediately) override; + + bool OnAction(const CAction& action) override; + void OnDown() override; + void OnUp() override; + void OnLeft() override; + void OnRight() override; + bool OnMouseOver(const CPoint& point) override; + bool OnMessage(CGUIMessage& message) override; + void SetFocus(bool focus) override; + std::string GetDescription() const override; + EVENT_RESULT OnMouseEvent(const CPoint& point, const CMouseEvent& event) override; + + void Process(unsigned int currentTime, CDirtyRegionList& dirtyregions) override; + void Render() override; + + CGUIListItemPtr GetListItem(int offset, unsigned int flag = 0) const override; + std::string GetLabel(int info) const override; + + std::shared_ptr<CFileItem> GetSelectedGridItem(int offset = 0) const; + std::shared_ptr<CPVRChannelGroupMember> GetSelectedChannelGroupMember() const; + CDateTime GetSelectedDate() const; + + void LoadLayout(TiXmlElement* layout); + void SetPageControl(int id); + + /*! \brief Set the offset of the first item in the container from the container's position + Useful for lists/panels where the focused item may be larger than the non-focused items and thus + normally cut off from the clipping window defined by the container's position + size. + \param offset CPoint holding the offset in skin coordinates. + */ + void SetRenderOffset(const CPoint& offset); + + void JumpToNow(); + void JumpToDate(const CDateTime& date); + + void GoToBegin(); + void GoToEnd(); + void GoToNow(); + void GoToDate(const CDateTime& date); + + void GoToFirstChannel(); + void GoToLastChannel(); + + void GoToTop(); + void GoToBottom(); + void GoToMostLeft(); + void GoToMostRight(); + + void SetTimelineItems(const std::unique_ptr<CFileItemList>& items, + const CDateTime& gridStart, + const CDateTime& gridEnd); + + std::unique_ptr<CFileItemList> GetCurrentTimeLineItems() const; + + /*! + * @brief Set the control's selection to the given channel and set the control's view port to show the channel. + * @param channel the channel. + * @return true if the selection was set to the given channel, false otherwise. + */ + bool SetChannel(const std::shared_ptr<CPVRChannel>& channel); + + /*! + * @brief Set the control's selection to the given channel and set the control's view port to show the channel. + * @param channel the channel's path. + * @return true if the selection was set to the given channel, false otherwise. + */ + bool SetChannel(const std::string& channel); + + /*! + * @brief Set the control's selection to the given channel and set the control's view port to show the channel. + * @param channelNumber the channel's number. + * @return true if the selection was set to the given channel, false otherwise. + */ + bool SetChannel(const CPVRChannelNumber& channelNumber); + + private: + bool OnClick(int actionID); + bool SelectItemFromPoint(const CPoint& point, bool justGrid = true); + + void SetChannel(int channel); + + void SetBlock(int block, bool bUpdateBlockTravelAxis = true); + void UpdateBlock(bool bUpdateBlockTravelAxis = true); + + void ChannelScroll(int amount); + void ProgrammesScroll(int amount); + void ValidateOffset(); + void UpdateLayout(); + + void SetItem(const std::pair<std::shared_ptr<CFileItem>, int>& itemInfo); + bool SetItem(const std::shared_ptr<CFileItem>& item, int channelIndex, int blockIndex); + std::shared_ptr<CFileItem> GetItem() const; + std::pair<std::shared_ptr<CFileItem>, int> GetNextItem() const; + std::pair<std::shared_ptr<CFileItem>, int> GetPrevItem() const; + void UpdateItem(); + + void MoveToRow(int row); + + CGUIListItemLayout* GetFocusedLayout() const; + + void ScrollToBlockOffset(int offset); + void ScrollToChannelOffset(int offset); + void GoToBlock(int blockIndex); + void GoToChannel(int channelIndex); + void UpdateScrollOffset(unsigned int currentTime); + void ProcessItem(float posX, float posY, const std::shared_ptr<CFileItem>& item, std::shared_ptr<CFileItem>& lastitem, bool focused, CGUIListItemLayout* normallayout, CGUIListItemLayout* focusedlayout, unsigned int currentTime, CDirtyRegionList& dirtyregions, float resize = -1.0f); + void RenderItem(float posX, float posY, CGUIListItem* item, bool focused); + void GetCurrentLayouts(); + + void ProcessChannels(unsigned int currentTime, CDirtyRegionList& dirtyregions); + void ProcessRuler(unsigned int currentTime, CDirtyRegionList& dirtyregions); + void ProcessRulerDate(unsigned int currentTime, CDirtyRegionList& dirtyregions); + void ProcessProgrammeGrid(unsigned int currentTime, CDirtyRegionList& dirtyregions); + void ProcessProgressIndicator(unsigned int currentTime, CDirtyRegionList& dirtyregions); + void RenderChannels(); + void RenderRulerDate(); + void RenderRuler(); + void RenderProgrammeGrid(); + void RenderProgressIndicator(); + + CPoint m_renderOffset; ///< \brief render offset of the first item in the list \sa SetRenderOffset + + ORIENTATION m_orientation; + + std::vector<CGUIListItemLayout> m_channelLayouts; + std::vector<CGUIListItemLayout> m_focusedChannelLayouts; + std::vector<CGUIListItemLayout> m_focusedProgrammeLayouts; + std::vector<CGUIListItemLayout> m_programmeLayouts; + std::vector<CGUIListItemLayout> m_rulerLayouts; + std::vector<CGUIListItemLayout> m_rulerDateLayouts; + + CGUIListItemLayout* m_channelLayout; + CGUIListItemLayout* m_focusedChannelLayout; + CGUIListItemLayout* m_programmeLayout; + CGUIListItemLayout* m_focusedProgrammeLayout; + CGUIListItemLayout* m_rulerLayout; + CGUIListItemLayout* m_rulerDateLayout; + + int m_pageControl; + + void GetChannelCacheOffsets(int& cacheBefore, int& cacheAfter); + void GetProgrammeCacheOffsets(int& cacheBefore, int& cacheAfter); + + private: + bool OnMouseClick(int dwButton, const CPoint& point); + bool OnMouseDoubleClick(int dwButton, const CPoint& point); + bool OnMouseWheel(char wheel, const CPoint& point); + + void HandleChannels(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions); + void HandleRuler(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions); + void HandleRulerDate(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions); + void HandleProgrammeGrid(bool bRender, unsigned int currentTime, CDirtyRegionList& dirtyregions); + + float GetCurrentTimePositionOnPage() const; + float GetProgressIndicatorWidth() const; + float GetProgressIndicatorHeight() const; + + void UpdateItems(); + + float GetChannelScrollOffsetPos() const; + float GetProgrammeScrollOffsetPos() const; + int GetChannelScrollOffset(CGUIListItemLayout* layout) const; + int GetProgrammeScrollOffset() const; + + int m_rulerUnit; //! number of blocks that makes up one element of the ruler + int m_channelsPerPage; + int m_programmesPerPage; + int m_channelCursor; + int m_channelOffset; + int m_blocksPerPage; + int m_blockCursor; + int m_blockOffset; + int m_blockTravelAxis; + int m_cacheChannelItems; + int m_cacheProgrammeItems; + int m_cacheRulerItems; + + float m_rulerDateHeight; //! height of ruler date item + float m_rulerDateWidth; //! width of ruler date item + float m_rulerPosX; //! X position of first ruler item + float m_rulerPosY; //! Y position of first ruler item + float m_rulerHeight; //! height of the scrolling timeline above the ruler items + float m_rulerWidth; //! width of each element of the ruler + float m_channelPosX; //! X position of first channel row + float m_channelPosY; //! Y position of first channel row + float m_channelHeight; //! height of the channel item + float m_channelWidth; //! width of the channel item + float m_gridPosX; //! X position of first grid item + float m_gridPosY; //! Y position of first grid item + float m_gridWidth; //! width of the epg grid control + float m_gridHeight; //! height of the epg grid control + float m_blockSize; //! a block's width in pixels + float m_analogScrollCount; + + std::unique_ptr<CGUITexture> m_guiProgressIndicatorTexture; + + std::shared_ptr<CFileItem> m_lastItem; + std::shared_ptr<CFileItem> m_lastChannel; + + bool m_bEnableProgrammeScrolling = true; + bool m_bEnableChannelScrolling = true; + + int m_scrollTime; + + int m_programmeScrollLastTime; + float m_programmeScrollSpeed; + float m_programmeScrollOffset; + + int m_channelScrollLastTime; + float m_channelScrollSpeed; + float m_channelScrollOffset; + + mutable CCriticalSection m_critSection; + std::unique_ptr<CGUIEPGGridContainerModel> m_gridModel; + std::unique_ptr<CGUIEPGGridContainerModel> m_updatedGridModel; + + int m_itemStartBlock = 0; + }; +} diff --git a/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp b/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp new file mode 100644 index 0000000..e8fac01 --- /dev/null +++ b/xbmc/pvr/guilib/GUIEPGGridContainerModel.cpp @@ -0,0 +1,723 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "GUIEPGGridContainerModel.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "pvr/PVRManager.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/epg/Epg.h" +#include "pvr/epg/EpgChannelData.h" +#include "pvr/epg/EpgInfoTag.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <cmath> +#include <iterator> +#include <memory> +#include <vector> + +using namespace PVR; + +static const unsigned int GRID_START_PADDING = 30; // minutes + +void CGUIEPGGridContainerModel::SetInvalid() +{ + for (const auto& gridItem : m_gridIndex) + gridItem.second.item->SetInvalid(); + for (const auto& channel : m_channelItems) + channel->SetInvalid(); + for (const auto& ruler : m_rulerItems) + ruler->SetInvalid(); +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::CreateGapItem(int iChannel) const +{ + const std::shared_ptr<CPVRChannel> channel = m_channelItems[iChannel]->GetPVRChannelInfoTag(); + const std::shared_ptr<CPVREpgInfoTag> gapTag = channel->CreateEPGGapTag(m_gridStart, m_gridEnd); + return std::make_shared<CFileItem>(gapTag); +} + +std::vector<std::shared_ptr<CPVREpgInfoTag>> CGUIEPGGridContainerModel::GetEPGTimeline( + int iChannel, const CDateTime& minEventEnd, const CDateTime& maxEventStart) const +{ + CDateTime min = minEventEnd - CDateTimeSpan(0, 0, MINSPERBLOCK, 0) + CDateTimeSpan(0, 0, 0, 1); + CDateTime max = maxEventStart + CDateTimeSpan(0, 0, MINSPERBLOCK, 0); + + if (min < m_gridStart) + min = m_gridStart; + + if (max > m_gridEnd) + max = m_gridEnd; + + return m_channelItems[iChannel]->GetPVRChannelInfoTag()->GetEPGTimeline(m_gridStart, m_gridEnd, + min, max); +} + +void CGUIEPGGridContainerModel::Initialize(const std::unique_ptr<CFileItemList>& items, + const CDateTime& gridStart, + const CDateTime& gridEnd, + int iFirstChannel, + int iChannelsPerPage, + int iFirstBlock, + int iBlocksPerPage, + int iRulerUnit, + float fBlockSize) +{ + if (!m_channelItems.empty()) + { + CLog::LogF(LOGERROR, "Already initialized!"); + return; + } + + m_fBlockSize = fBlockSize; + + //////////////////////////////////////////////////////////////////////// + // Create channel items + std::copy(items->cbegin(), items->cend(), std::back_inserter(m_channelItems)); + + /* check for invalid start and end time */ + if (gridStart >= gridEnd) + { + // default to start "now minus GRID_START_PADDING minutes" and end "start plus one page". + m_gridStart = CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0); + m_gridEnd = m_gridStart + CDateTimeSpan(0, 0, iBlocksPerPage * MINSPERBLOCK, 0); + } + else if (gridStart > + (CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0))) + { + // adjust to start "now minus GRID_START_PADDING minutes". + m_gridStart = CDateTime::GetUTCDateTime() - CDateTimeSpan(0, 0, GetGridStartPadding(), 0); + m_gridEnd = gridEnd; + } + else + { + m_gridStart = gridStart; + m_gridEnd = gridEnd; + } + + // roundup + m_gridStart = CDateTime(m_gridStart.GetYear(), m_gridStart.GetMonth(), m_gridStart.GetDay(), + m_gridStart.GetHour(), m_gridStart.GetMinute() >= 30 ? 30 : 0, 0); + m_gridEnd = CDateTime(m_gridEnd.GetYear(), m_gridEnd.GetMonth(), m_gridEnd.GetDay(), + m_gridEnd.GetHour(), m_gridEnd.GetMinute() >= 30 ? 30 : 0, 0); + + m_blocks = GetBlock(m_gridEnd) + 1; + + const int iBlocksLastPage = m_blocks % iBlocksPerPage; + if (iBlocksLastPage > 0) + { + m_gridEnd += CDateTimeSpan(0, 0, (iBlocksPerPage - iBlocksLastPage) * MINSPERBLOCK, 0); + m_blocks += (iBlocksPerPage - iBlocksLastPage); + } + + //////////////////////////////////////////////////////////////////////// + // Create ruler items + CDateTime ruler; + ruler.SetFromUTCDateTime(m_gridStart); + CDateTime rulerEnd; + rulerEnd.SetFromUTCDateTime(m_gridEnd); + CFileItemPtr rulerItem(new CFileItem(ruler.GetAsLocalizedDate(true))); + rulerItem->SetProperty("DateLabel", true); + m_rulerItems.emplace_back(rulerItem); + + const CDateTimeSpan unit(0, 0, iRulerUnit * MINSPERBLOCK, 0); + for (; ruler < rulerEnd; ruler += unit) + { + rulerItem.reset(new CFileItem(ruler.GetAsLocalizedTime("", false))); + rulerItem->SetLabel2(ruler.GetAsLocalizedDate(true)); + m_rulerItems.emplace_back(rulerItem); + } + + m_firstActiveChannel = iFirstChannel; + m_lastActiveChannel = iFirstChannel + iChannelsPerPage - 1; + m_firstActiveBlock = iFirstBlock; + m_lastActiveBlock = iFirstBlock + iBlocksPerPage - 1; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::CreateEpgTags(int iChannel, int iBlock) const +{ + std::shared_ptr<CFileItem> result; + + const int firstBlock = iBlock < m_firstActiveBlock ? iBlock : m_firstActiveBlock; + const int lastBlock = iBlock > m_lastActiveBlock ? iBlock : m_lastActiveBlock; + + const auto tags = + GetEPGTimeline(iChannel, GetStartTimeForBlock(firstBlock), GetStartTimeForBlock(lastBlock)); + + const int firstResultBlock = GetFirstEventBlock(tags.front()); + const int lastResultBlock = GetLastEventBlock(tags.back()); + if (firstResultBlock > lastResultBlock) + return result; + + auto it = m_epgItems.insert({iChannel, EpgTags()}).first; + EpgTags& epgTags = (*it).second; + + epgTags.firstBlock = firstResultBlock; + epgTags.lastBlock = lastResultBlock; + + for (const auto& tag : tags) + { + if (GetFirstEventBlock(tag) > GetLastEventBlock(tag)) + continue; + + const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(tag); + if (!result && IsEventMemberOfBlock(tag, iBlock)) + result = item; + + epgTags.tags.emplace_back(item); + } + + return result; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTags(EpgTagsMap::iterator& itEpg, + int iChannel, + int iBlock) const +{ + std::shared_ptr<CFileItem> result; + + EpgTags& epgTags = (*itEpg).second; + + if (iBlock < epgTags.firstBlock) + { + result = GetEpgTagsBefore(epgTags, iChannel, iBlock); + } + else if (iBlock > epgTags.lastBlock) + { + result = GetEpgTagsAfter(epgTags, iChannel, iBlock); + } + else + { + const auto it = + std::find_if(epgTags.tags.cbegin(), epgTags.tags.cend(), [this, iBlock](const auto& item) { + return IsEventMemberOfBlock(item->GetEPGInfoTag(), iBlock); + }); + if (it != epgTags.tags.cend()) + result = (*it); + } + + return result; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTagsBefore(EpgTags& epgTags, + int iChannel, + int iBlock) const +{ + std::shared_ptr<CFileItem> result; + + int lastBlock = epgTags.firstBlock - 1; + if (lastBlock < 0) + lastBlock = 0; + + const auto tags = + GetEPGTimeline(iChannel, GetStartTimeForBlock(iBlock), GetStartTimeForBlock(lastBlock)); + + if (epgTags.lastBlock == -1) + epgTags.lastBlock = lastBlock; + + if (tags.empty()) + { + epgTags.firstBlock = iBlock; + } + else + { + const int firstResultBlock = GetFirstEventBlock(tags.front()); + const int lastResultBlock = GetLastEventBlock(tags.back()); + if (firstResultBlock > lastResultBlock) + return result; + + // insert before the existing tags + epgTags.firstBlock = firstResultBlock; + + auto it = tags.crbegin(); + if (!epgTags.tags.empty()) + { + // ptr comp does not work for gap tags! + // if ((*it) == epgTags.tags.front()->GetEPGInfoTag()) + + const std::shared_ptr<CPVREpgInfoTag> t = epgTags.tags.front()->GetEPGInfoTag(); + if ((*it)->StartAsUTC() == t->StartAsUTC() && (*it)->EndAsUTC() == t->EndAsUTC()) + { + if (!result && IsEventMemberOfBlock(*it, iBlock)) + result = epgTags.tags.front(); + + ++it; // skip, because we already have that epg tag + } + } + + for (; it != tags.crend(); ++it) + { + if (GetFirstEventBlock(*it) > GetLastEventBlock(*it)) + continue; + + const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(*it); + if (!result && IsEventMemberOfBlock(*it, iBlock)) + result = item; + + epgTags.tags.insert(epgTags.tags.begin(), item); + } + } + + return result; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetEpgTagsAfter(EpgTags& epgTags, + int iChannel, + int iBlock) const +{ + std::shared_ptr<CFileItem> result; + + int firstBlock = epgTags.lastBlock + 1; + if (firstBlock >= GetLastBlock()) + firstBlock = GetLastBlock(); + + const auto tags = + GetEPGTimeline(iChannel, GetStartTimeForBlock(firstBlock), GetStartTimeForBlock(iBlock)); + + if (epgTags.firstBlock == -1) + epgTags.firstBlock = firstBlock; + + if (tags.empty()) + { + epgTags.lastBlock = iBlock; + } + else + { + const int firstResultBlock = GetFirstEventBlock(tags.front()); + const int lastResultBlock = GetLastEventBlock(tags.back()); + if (firstResultBlock > lastResultBlock) + return result; + + // append to the existing tags + epgTags.lastBlock = lastResultBlock; + + auto it = tags.cbegin(); + if (!epgTags.tags.empty()) + { + // ptr comp does not work for gap tags! + // if ((*it) == epgTags.tags.back()->GetEPGInfoTag()) + + const std::shared_ptr<CPVREpgInfoTag> t = epgTags.tags.back()->GetEPGInfoTag(); + if ((*it)->StartAsUTC() == t->StartAsUTC() && (*it)->EndAsUTC() == t->EndAsUTC()) + { + if (!result && IsEventMemberOfBlock(*it, iBlock)) + result = epgTags.tags.back(); + + ++it; // skip, because we already have that epg tag + } + } + + for (; it != tags.cend(); ++it) + { + if (GetFirstEventBlock(*it) > GetLastEventBlock(*it)) + continue; + + const std::shared_ptr<CFileItem> item = std::make_shared<CFileItem>(*it); + if (!result && IsEventMemberOfBlock(*it, iBlock)) + result = item; + + epgTags.tags.emplace_back(item); + } + } + + return result; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetItem(int iChannel, int iBlock) const +{ + std::shared_ptr<CFileItem> result; + + auto itEpg = m_epgItems.find(iChannel); + if (itEpg == m_epgItems.end()) + { + result = CreateEpgTags(iChannel, iBlock); + } + else + { + result = GetEpgTags(itEpg, iChannel, iBlock); + } + + if (!result) + { + // Must never happen. if it does, fix the root cause, don't tolerate nullptr! + CLog::LogF(LOGERROR, "EPG tag ({}, {}) not found!", iChannel, iBlock); + } + + return result; +} + +void CGUIEPGGridContainerModel::FindChannelAndBlockIndex(int channelUid, + unsigned int broadcastUid, + int eventOffset, + int& newChannelIndex, + int& newBlockIndex) const +{ + newChannelIndex = INVALID_INDEX; + newBlockIndex = INVALID_INDEX; + + // find the new channel index + int iCurrentChannel = 0; + for (const auto& channel : m_channelItems) + { + if (channel->GetPVRChannelInfoTag()->UniqueID() == channelUid) + { + newChannelIndex = iCurrentChannel; + + // find the new block index + const std::shared_ptr<CPVREpg> epg = channel->GetPVRChannelInfoTag()->GetEPG(); + if (epg) + { + const std::shared_ptr<CPVREpgInfoTag> tag = epg->GetTagByBroadcastId(broadcastUid); + if (tag) + newBlockIndex = GetFirstEventBlock(tag) + eventOffset; + } + break; // done + } + iCurrentChannel++; + } +} + +GridItem* CGUIEPGGridContainerModel::GetGridItemPtr(int iChannel, int iBlock) const +{ + auto it = m_gridIndex.find({iChannel, iBlock}); + if (it == m_gridIndex.end()) + { + const CDateTime startTime = GetStartTimeForBlock(iBlock); + if (startTime < m_gridStart || m_gridEnd < startTime) + { + CLog::LogF(LOGERROR, "Requested EPG tag ({}, {}) outside grid boundaries!", iChannel, iBlock); + return nullptr; + } + + const std::shared_ptr<CFileItem> item = GetItem(iChannel, iBlock); + if (!item) + { + CLog::LogF(LOGERROR, "Got no EPG tag ({}, {})!", iChannel, iBlock); + return nullptr; + } + + const std::shared_ptr<CPVREpgInfoTag> epgTag = item->GetEPGInfoTag(); + + const int startBlock = GetFirstEventBlock(epgTag); + const int endBlock = GetLastEventBlock(epgTag); + + //! @todo it seems that this should be done somewhere else. CFileItem ctor maybe. + item->SetProperty("GenreType", epgTag->GenreType()); + + const float fItemWidth = (endBlock - startBlock + 1) * m_fBlockSize; + it = m_gridIndex.insert({{iChannel, iBlock}, {item, fItemWidth, startBlock, endBlock}}).first; + } + + return &(*it).second; +} + +bool CGUIEPGGridContainerModel::IsSameGridItem(int iChannel, int iBlock1, int iBlock2) const +{ + if (iBlock1 == iBlock2) + return true; + + const GridItem* item1 = GetGridItemPtr(iChannel, iBlock1); + const GridItem* item2 = GetGridItemPtr(iChannel, iBlock2); + + // compare the instances, not instance pointers, pointers are not unique. + return *item1 == *item2; +} + +std::shared_ptr<CFileItem> CGUIEPGGridContainerModel::GetGridItem(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->item; +} + +int CGUIEPGGridContainerModel::GetGridItemStartBlock(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->startBlock; +} + +int CGUIEPGGridContainerModel::GetGridItemEndBlock(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->endBlock; +} + +CDateTime CGUIEPGGridContainerModel::GetGridItemEndTime(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->item->GetEPGInfoTag()->EndAsUTC(); +} + +float CGUIEPGGridContainerModel::GetGridItemWidth(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->width; +} + +float CGUIEPGGridContainerModel::GetGridItemOriginWidth(int iChannel, int iBlock) const +{ + return GetGridItemPtr(iChannel, iBlock)->originWidth; +} + +void CGUIEPGGridContainerModel::DecreaseGridItemWidth(int iChannel, int iBlock, float fSize) +{ + auto it = m_gridIndex.find({iChannel, iBlock}); + if (it != m_gridIndex.end() && (*it).second.width != ((*it).second.originWidth - fSize)) + (*it).second.width = (*it).second.originWidth - fSize; +} + +unsigned int CGUIEPGGridContainerModel::GetGridStartPadding() const +{ + unsigned int iPastMinutes = + CServiceBroker::GetPVRManager().EpgContainer().GetPastDaysToDisplay() * 24 * 60; + + if (iPastMinutes < GRID_START_PADDING) + return iPastMinutes; + + return GRID_START_PADDING; // minutes +} + +void CGUIEPGGridContainerModel::FreeChannelMemory(int keepStart, int keepEnd) +{ + if (keepStart < keepEnd) + { + // remove before keepStart and after keepEnd + for (int i = 0; i < keepStart && i < ChannelItemsSize(); ++i) + m_channelItems[i]->FreeMemory(); + for (int i = keepEnd + 1; i < ChannelItemsSize(); ++i) + m_channelItems[i]->FreeMemory(); + } + else + { + // wrapping + for (int i = keepEnd + 1; i < keepStart && i < ChannelItemsSize(); ++i) + m_channelItems[i]->FreeMemory(); + } +} + +bool CGUIEPGGridContainerModel::FreeProgrammeMemory(int firstChannel, + int lastChannel, + int firstBlock, + int lastBlock) +{ + const bool channelsChanged = + (firstChannel != m_firstActiveChannel || lastChannel != m_lastActiveChannel); + const bool blocksChanged = (firstBlock != m_firstActiveBlock || lastBlock != m_lastActiveBlock); + if (!channelsChanged && !blocksChanged) + return false; + + // clear the grid. it will be recreated on-demand. + m_gridIndex.clear(); + + bool newChannels = false; + + if (channelsChanged) + { + // purge epg tags for inactive channels + for (auto it = m_epgItems.begin(); it != m_epgItems.end();) + { + if ((*it).first < firstChannel || (*it).first > lastChannel) + { + it = m_epgItems.erase(it); + continue; // next channel + } + ++it; + } + + newChannels = (firstChannel < m_firstActiveChannel) || (lastChannel > m_lastActiveChannel); + } + + if (blocksChanged || newChannels) + { + // clear and refetch epg tags for active channels + const CDateTime maxEnd = GetStartTimeForBlock(firstBlock); + const CDateTime minStart = GetStartTimeForBlock(lastBlock); + std::vector<std::shared_ptr<CPVREpgInfoTag>> tags; + for (int i = firstChannel; i <= lastChannel; ++i) + { + auto it = m_epgItems.find(i); + if (it == m_epgItems.end()) + it = m_epgItems.insert({i, EpgTags()}).first; + + if (blocksChanged || i < m_firstActiveChannel || i > m_lastActiveChannel) + { + EpgTags& epgTags = (*it).second; + + (*it).second.tags.clear(); + + tags = GetEPGTimeline(i, maxEnd, minStart); + const int firstResultBlock = GetFirstEventBlock(tags.front()); + const int lastResultBlock = GetLastEventBlock(tags.back()); + if (firstResultBlock > lastResultBlock) + continue; + + epgTags.firstBlock = firstResultBlock; + epgTags.lastBlock = lastResultBlock; + + for (const auto& tag : tags) + { + if (GetFirstEventBlock(tag) > GetLastEventBlock(tag)) + continue; + + epgTags.tags.emplace_back(std::make_shared<CFileItem>(tag)); + } + } + } + } + + m_firstActiveChannel = firstChannel; + m_lastActiveChannel = lastChannel; + m_firstActiveBlock = firstBlock; + m_lastActiveBlock = lastBlock; + + return true; +} + +void CGUIEPGGridContainerModel::FreeRulerMemory(int keepStart, int keepEnd) +{ + if (keepStart < keepEnd) + { + // remove before keepStart and after keepEnd + for (int i = 1; i < keepStart && i < RulerItemsSize(); ++i) + m_rulerItems[i]->FreeMemory(); + for (int i = keepEnd + 1; i < RulerItemsSize(); ++i) + m_rulerItems[i]->FreeMemory(); + } + else + { + // wrapping + for (int i = keepEnd + 1; i < keepStart && i < RulerItemsSize(); ++i) + { + if (i == 0) + continue; + + m_rulerItems[i]->FreeMemory(); + } + } +} + +unsigned int CGUIEPGGridContainerModel::GetPageNowOffset() const +{ + return GetGridStartPadding() / MINSPERBLOCK; // this is the 'now' block relative to page start +} + +CDateTime CGUIEPGGridContainerModel::GetStartTimeForBlock(int block) const +{ + if (block < 0) + block = 0; + else if (block >= GridItemsSize()) + block = GetLastBlock(); + + return m_gridStart + CDateTimeSpan(0, 0, block * MINSPERBLOCK, 0); +} + +int CGUIEPGGridContainerModel::GetBlock(const CDateTime& datetime) const +{ + int diff; + + if (m_gridStart == datetime) + return 0; // block is at grid start + else if (m_gridStart > datetime) + diff = -1 * (m_gridStart - datetime).GetSecondsTotal(); // block is before grid start + else + diff = (datetime - m_gridStart).GetSecondsTotal(); // block is after grid start + + // Note: Subtract 1 second from diff to ensure that events ending exactly at block boundary + // are unambiguous. Example: An event ending at 5:00:00 shall be mapped to block 9 and + // an event starting at 5:00:00 shall be mapped to block 10, not both at block 10. + // Only exception is grid end, because there is no successor. + if (datetime >= m_gridEnd) + return diff / 60 / MINSPERBLOCK; // block is equal or after grid end + else + return (diff - 1) / 60 / MINSPERBLOCK; +} + +int CGUIEPGGridContainerModel::GetNowBlock() const +{ + return GetBlock(CDateTime::GetUTCDateTime()) - GetPageNowOffset(); +} + +int CGUIEPGGridContainerModel::GetFirstEventBlock( + const std::shared_ptr<CPVREpgInfoTag>& event) const +{ + const CDateTime eventStart = event->StartAsUTC(); + int diff; + + if (m_gridStart == eventStart) + return 0; // block is at grid start + else if (m_gridStart > eventStart) + diff = -1 * (m_gridStart - eventStart).GetSecondsTotal(); + else + diff = (eventStart - m_gridStart).GetSecondsTotal(); + + // First block of a tag is always the block calculated using event's start time, rounded up. + float fBlockIndex = diff / 60.0f / MINSPERBLOCK; + return static_cast<int>(std::ceil(fBlockIndex)); +} + +int CGUIEPGGridContainerModel::GetLastEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const +{ + // Last block of a tag is always the block calculated using event's end time, not rounded up. + return GetBlock(event->EndAsUTC()); +} + +bool CGUIEPGGridContainerModel::IsEventMemberOfBlock(const std::shared_ptr<CPVREpgInfoTag>& event, + int iBlock) const +{ + const int iFirstBlock = GetFirstEventBlock(event); + const int iLastBlock = GetLastEventBlock(event); + + if (iFirstBlock > iLastBlock) + { + return false; + } + else if (iFirstBlock == iBlock) + { + return true; + } + else if (iFirstBlock < iBlock) + { + return (iBlock <= iLastBlock); + } + return false; +} + +std::unique_ptr<CFileItemList> CGUIEPGGridContainerModel::GetCurrentTimeLineItems( + int firstChannel, int numChannels) const +{ + // Note: No need to keep this in a member. Gets generally not called multiple times for the + // same timeline, but content must be synced with m_epgItems, which changes quite often. + + std::unique_ptr<CFileItemList> items(new CFileItemList); + + if (numChannels > ChannelItemsSize()) + numChannels = ChannelItemsSize(); + + int i = 0; + for (int channel = firstChannel; channel < (firstChannel + numChannels); ++channel) + { + // m_epgItems is not sorted, fileitemlist must be sorted, so we have to 'find' the channel + const auto itEpg = m_epgItems.find(channel); + if (itEpg != m_epgItems.end()) + { + // tags are sorted, so we can iterate and append + for (const auto& tag : (*itEpg).second.tags) + { + tag->SetProperty("TimelineIndex", i); + items->Add(tag); + ++i; + } + } + else + { + // fake empty EPG + const std::shared_ptr<CFileItem> tag = CreateGapItem(channel); + tag->SetProperty("TimelineIndex", i); + items->Add(tag); + ++i; + } + } + return items; +} diff --git a/xbmc/pvr/guilib/GUIEPGGridContainerModel.h b/xbmc/pvr/guilib/GUIEPGGridContainerModel.h new file mode 100644 index 0000000..2e0a6d6 --- /dev/null +++ b/xbmc/pvr/guilib/GUIEPGGridContainerModel.h @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "XBDateTime.h" + +#include <functional> +#include <map> +#include <memory> +#include <unordered_map> +#include <utility> +#include <vector> + +class CFileItem; +class CFileItemList; + +namespace PVR +{ +struct GridItem +{ + GridItem(const std::shared_ptr<CFileItem>& _item, float _width, int _startBlock, int _endBlock) + : item(_item), originWidth(_width), width(_width), startBlock(_startBlock), endBlock(_endBlock) + { + } + + bool operator==(const GridItem& other) const + { + return (startBlock == other.startBlock && endBlock == other.endBlock); + } + + std::shared_ptr<CFileItem> item; + float originWidth = 0.0f; + float width = 0.0f; + int startBlock = 0; + int endBlock = 0; +}; + +class CPVREpgInfoTag; + +class CGUIEPGGridContainerModel +{ +public: + static constexpr int MINSPERBLOCK = 5; // minutes + + CGUIEPGGridContainerModel() = default; + virtual ~CGUIEPGGridContainerModel() = default; + + void Initialize(const std::unique_ptr<CFileItemList>& items, + const CDateTime& gridStart, + const CDateTime& gridEnd, + int iFirstChannel, + int iChannelsPerPage, + int iFirstBlock, + int iBlocksPerPage, + int iRulerUnit, + float fBlockSize); + void SetInvalid(); + + static const int INVALID_INDEX = -1; + void FindChannelAndBlockIndex(int channelUid, + unsigned int broadcastUid, + int eventOffset, + int& newChannelIndex, + int& newBlockIndex) const; + + void FreeChannelMemory(int keepStart, int keepEnd); + bool FreeProgrammeMemory(int firstChannel, int lastChannel, int firstBlock, int lastBlock); + void FreeRulerMemory(int keepStart, int keepEnd); + + std::shared_ptr<CFileItem> GetChannelItem(int iIndex) const { return m_channelItems[iIndex]; } + bool HasChannelItems() const { return !m_channelItems.empty(); } + int ChannelItemsSize() const { return static_cast<int>(m_channelItems.size()); } + int GetLastChannel() const + { + return m_channelItems.empty() ? -1 : static_cast<int>(m_channelItems.size()) - 1; + } + + std::shared_ptr<CFileItem> GetRulerItem(int iIndex) const { return m_rulerItems[iIndex]; } + int RulerItemsSize() const { return static_cast<int>(m_rulerItems.size()); } + + int GridItemsSize() const { return m_blocks; } + bool IsSameGridItem(int iChannel, int iBlock1, int iBlock2) const; + std::shared_ptr<CFileItem> GetGridItem(int iChannel, int iBlock) const; + int GetGridItemStartBlock(int iChannel, int iBlock) const; + int GetGridItemEndBlock(int iChannel, int iBlock) const; + CDateTime GetGridItemEndTime(int iChannel, int iBlock) const; + float GetGridItemWidth(int iChannel, int iBlock) const; + float GetGridItemOriginWidth(int iChannel, int iBlock) const; + void DecreaseGridItemWidth(int iChannel, int iBlock, float fSize); + + bool IsZeroGridDuration() const { return (m_gridEnd - m_gridStart) == CDateTimeSpan(0, 0, 0, 0); } + const CDateTime& GetGridStart() const { return m_gridStart; } + const CDateTime& GetGridEnd() const { return m_gridEnd; } + unsigned int GetGridStartPadding() const; + + unsigned int GetPageNowOffset() const; + int GetNowBlock() const; + int GetLastBlock() const { return m_blocks - 1; } + + CDateTime GetStartTimeForBlock(int block) const; + int GetBlock(const CDateTime& datetime) const; + int GetFirstEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const; + int GetLastEventBlock(const std::shared_ptr<CPVREpgInfoTag>& event) const; + bool IsEventMemberOfBlock(const std::shared_ptr<CPVREpgInfoTag>& event, int iBlock) const; + + std::unique_ptr<CFileItemList> GetCurrentTimeLineItems(int firstChannel, int numChannels) const; + +private: + GridItem* GetGridItemPtr(int iChannel, int iBlock) const; + std::shared_ptr<CFileItem> CreateGapItem(int iChannel) const; + std::shared_ptr<CFileItem> GetItem(int iChannel, int iBlock) const; + + std::vector<std::shared_ptr<CPVREpgInfoTag>> GetEPGTimeline(int iChannel, + const CDateTime& minEventEnd, + const CDateTime& maxEventStart) const; + + struct EpgTags + { + std::vector<std::shared_ptr<CFileItem>> tags; + int firstBlock = -1; + int lastBlock = -1; + }; + + using EpgTagsMap = std::unordered_map<int, EpgTags>; + + std::shared_ptr<CFileItem> CreateEpgTags(int iChannel, int iBlock) const; + std::shared_ptr<CFileItem> GetEpgTags(EpgTagsMap::iterator& itEpg, + int iChannel, + int iBlock) const; + std::shared_ptr<CFileItem> GetEpgTagsBefore(EpgTags& epgTags, int iChannel, int iBlock) const; + std::shared_ptr<CFileItem> GetEpgTagsAfter(EpgTags& epgTags, int iChannel, int iBlock) const; + + mutable EpgTagsMap m_epgItems; + + CDateTime m_gridStart; + CDateTime m_gridEnd; + + std::vector<std::shared_ptr<CFileItem>> m_channelItems; + std::vector<std::shared_ptr<CFileItem>> m_rulerItems; + + struct GridCoordinates + { + GridCoordinates(int _channel, int _block) : channel(_channel), block(_block) {} + + bool operator==(const GridCoordinates& other) const + { + return (channel == other.channel && block == other.block); + } + + int channel = 0; + int block = 0; + }; + + struct GridCoordinatesHash + { + std::size_t operator()(const GridCoordinates& coordinates) const + { + return std::hash<int>()(coordinates.channel) ^ std::hash<int>()(coordinates.block); + } + }; + + mutable std::unordered_map<GridCoordinates, GridItem, GridCoordinatesHash> m_gridIndex; + + int m_blocks = 0; + float m_fBlockSize = 0.0f; + + int m_firstActiveChannel = 0; + int m_lastActiveChannel = 0; + int m_firstActiveBlock = 0; + int m_lastActiveBlock = 0; +}; +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionListener.cpp b/xbmc/pvr/guilib/PVRGUIActionListener.cpp new file mode 100644 index 0000000..3e68386 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionListener.cpp @@ -0,0 +1,420 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionListener.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "application/Application.h" +#include "application/ApplicationActionListeners.h" +#include "application/ApplicationComponents.h" +#include "dialogs/GUIDialogNumeric.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsClients.h" +#include "pvr/guilib/PVRGUIActionsDatabase.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "pvr/guilib/PVRGUIActionsTimers.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "settings/lib/Setting.h" + +#include <memory> +#include <string> + +namespace PVR +{ + +CPVRGUIActionListener::CPVRGUIActionListener() +{ + auto& components = CServiceBroker::GetAppComponents(); + const auto appListener = components.GetComponent<CApplicationActionListeners>(); + appListener->RegisterActionListener(this); + CServiceBroker::GetSettingsComponent()->GetSettings()->RegisterCallback( + this, + {CSettings::SETTING_PVRPARENTAL_ENABLED, CSettings::SETTING_PVRMANAGER_RESETDB, + CSettings::SETTING_EPG_RESETEPG, CSettings::SETTING_PVRMANAGER_ADDONS, + CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES, CSettings::SETTING_PVRMANAGER_CHANNELMANAGER, + CSettings::SETTING_PVRMANAGER_GROUPMANAGER, CSettings::SETTING_PVRMANAGER_CHANNELSCAN, + CSettings::SETTING_PVRMENU_SEARCHICONS, CSettings::SETTING_PVRCLIENT_MENUHOOK, + CSettings::SETTING_EPG_PAST_DAYSTODISPLAY, CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY}); +} + +CPVRGUIActionListener::~CPVRGUIActionListener() +{ + CServiceBroker::GetSettingsComponent()->GetSettings()->UnregisterCallback(this); + auto& components = CServiceBroker::GetAppComponents(); + const auto appListener = components.GetComponent<CApplicationActionListeners>(); + appListener->UnregisterActionListener(this); +} + +void CPVRGUIActionListener::Init(CPVRManager& mgr) +{ + mgr.Events().Subscribe(this, &CPVRGUIActionListener::OnPVRManagerEvent); +} + +void CPVRGUIActionListener::Deinit(CPVRManager& mgr) +{ + mgr.Events().Unsubscribe(this); +} + +void CPVRGUIActionListener::OnPVRManagerEvent(const PVREvent& event) +{ + if (event == PVREvent::AnnounceReminder) + { + if (g_application.IsInitialized()) + { + // if GUI is ready, dispatch to GUI thread and handle the action there + CServiceBroker::GetAppMessenger()->PostMsg( + TMSG_GUI_ACTION, WINDOW_INVALID, -1, + static_cast<void*>(new CAction(ACTION_PVR_ANNOUNCE_REMINDERS))); + } + } +} + +ChannelSwitchMode CPVRGUIActionListener::GetChannelSwitchMode(int iAction) +{ + if ((iAction == ACTION_MOVE_UP || iAction == ACTION_MOVE_DOWN) && + CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRPLAYBACK_CONFIRMCHANNELSWITCH)) + return ChannelSwitchMode::NO_SWITCH; + + return ChannelSwitchMode::INSTANT_OR_DELAYED_SWITCH; +} + +bool CPVRGUIActionListener::OnAction(const CAction& action) +{ + bool bIsJumpSMS = false; + bool bIsPlayingPVR = CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying() && + g_application.CurrentFileItem().HasPVRChannelInfoTag(); + + switch (action.GetID()) + { + case ACTION_PVR_PLAY: + case ACTION_PVR_PLAY_TV: + case ACTION_PVR_PLAY_RADIO: + { + // see if we're already playing a PVR stream and if not or the stream type + // doesn't match the demanded type, start playback of according type + switch (action.GetID()) + { + case ACTION_PVR_PLAY: + if (!bIsPlayingPVR) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + PlaybackTypeAny); + break; + case ACTION_PVR_PLAY_TV: + if (!bIsPlayingPVR || g_application.CurrentFileItem().GetPVRChannelInfoTag()->IsRadio()) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + PlaybackTypeTV); + break; + case ACTION_PVR_PLAY_RADIO: + if (!bIsPlayingPVR || !g_application.CurrentFileItem().GetPVRChannelInfoTag()->IsRadio()) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + PlaybackTypeRadio); + break; + } + return true; + } + + case ACTION_JUMP_SMS2: + case ACTION_JUMP_SMS3: + case ACTION_JUMP_SMS4: + case ACTION_JUMP_SMS5: + case ACTION_JUMP_SMS6: + case ACTION_JUMP_SMS7: + case ACTION_JUMP_SMS8: + case ACTION_JUMP_SMS9: + bIsJumpSMS = true; + // fallthru is intended + [[fallthrough]]; + case REMOTE_0: + case REMOTE_1: + case REMOTE_2: + case REMOTE_3: + case REMOTE_4: + case REMOTE_5: + case REMOTE_6: + case REMOTE_7: + case REMOTE_8: + case REMOTE_9: + case ACTION_CHANNEL_NUMBER_SEP: + { + if (!bIsPlayingPVR) + return false; + + if (CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_FULLSCREEN_VIDEO) || + CServiceBroker::GetGUI()->GetWindowManager().IsWindowActive(WINDOW_VISUALISATION)) + { + // do not consume action if a python modal is the top most dialog + // as a python modal can't return that it consumed the action. + if (CServiceBroker::GetGUI()->GetWindowManager().IsPythonWindow( + CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog())) + return false; + + char cCharacter; + if (action.GetID() == ACTION_CHANNEL_NUMBER_SEP) + { + cCharacter = CPVRChannelNumber::SEPARATOR; + } + else + { + int iRemote = + bIsJumpSMS ? action.GetID() - (ACTION_JUMP_SMS2 - REMOTE_2) : action.GetID(); + cCharacter = static_cast<char>(iRemote - REMOTE_0) + '0'; + } + CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNumberInputHandler() + .AppendChannelNumberCharacter(cCharacter); + return true; + } + return false; + } + + case ACTION_SHOW_INFO: + { + if (!bIsPlayingPVR) + return false; + + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelNavigator().ToggleInfo(); + return true; + } + + case ACTION_SELECT_ITEM: + { + if (!bIsPlayingPVR) + return false; + + // If the button that caused this action matches action "Select" ... + if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRPLAYBACK_CONFIRMCHANNELSWITCH) && + CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNavigator() + .IsPreview()) + { + // ... and if "confirm channel switch" setting is active and a channel + // preview is currently shown, switch to the currently previewed channel. + CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNavigator() + .SwitchToCurrentChannel(); + return true; + } + else if (CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNumberInputHandler() + .CheckInputAndExecuteAction()) + { + // ... or if the action was processed by direct channel number input, we're done. + return true; + } + return false; + } + + case ACTION_NEXT_ITEM: + { + if (!bIsPlayingPVR) + return false; + + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SeekForward(); + return true; + } + + case ACTION_PREV_ITEM: + { + if (!bIsPlayingPVR) + return false; + + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SeekBackward( + CApplication::ACTION_PREV_ITEM_THRESHOLD); + return true; + } + + case ACTION_MOVE_UP: + case ACTION_CHANNEL_UP: + { + if (!bIsPlayingPVR) + return false; + + CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNavigator() + .SelectNextChannel(GetChannelSwitchMode(action.GetID())); + return true; + } + + case ACTION_MOVE_DOWN: + case ACTION_CHANNEL_DOWN: + { + if (!bIsPlayingPVR) + return false; + + CServiceBroker::GetPVRManager() + .Get<PVR::GUI::Channels>() + .GetChannelNavigator() + .SelectPreviousChannel(GetChannelSwitchMode(action.GetID())); + return true; + } + + case ACTION_CHANNEL_SWITCH: + { + if (!bIsPlayingPVR) + return false; + + int iChannelNumber = static_cast<int>(action.GetAmount(0)); + int iSubChannelNumber = static_cast<int>(action.GetAmount(1)); + + const std::shared_ptr<CPVRPlaybackState> playbackState = + CServiceBroker::GetPVRManager().PlaybackState(); + const std::shared_ptr<CPVRChannelGroup> activeGroup = + playbackState->GetActiveChannelGroup(playbackState->IsPlayingRadio()); + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + activeGroup->GetByChannelNumber(CPVRChannelNumber(iChannelNumber, iSubChannelNumber)); + + if (!groupMember) + return false; + + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + CFileItem(groupMember), false); + return true; + } + + case ACTION_RECORD: + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().ToggleRecordingOnPlayingChannel(); + return true; + } + + case ACTION_PVR_ANNOUNCE_REMINDERS: + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Timers>().AnnounceReminders(); + return true; + } + } + return false; +} + +void CPVRGUIActionListener::OnSettingChanged(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + return; + + const std::string& settingId = setting->GetId(); + if (settingId == CSettings::SETTING_PVRPARENTAL_ENABLED) + { + if (std::static_pointer_cast<const CSettingBool>(setting)->GetValue() && + CServiceBroker::GetSettingsComponent() + ->GetSettings() + ->GetString(CSettings::SETTING_PVRPARENTAL_PIN) + .empty()) + { + std::string newPassword = ""; + // password set... save it + if (CGUIDialogNumeric::ShowAndVerifyNewPassword(newPassword)) + CServiceBroker::GetSettingsComponent()->GetSettings()->SetString( + CSettings::SETTING_PVRPARENTAL_PIN, newPassword); + // password not set... disable parental + else + std::static_pointer_cast<CSettingBool>(std::const_pointer_cast<CSetting>(setting)) + ->SetValue(false); + } + } + else if (settingId == CSettings::SETTING_EPG_PAST_DAYSTODISPLAY) + { + CServiceBroker::GetPVRManager().Clients()->SetEPGMaxPastDays( + std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + } + else if (settingId == CSettings::SETTING_EPG_FUTURE_DAYSTODISPLAY) + { + CServiceBroker::GetPVRManager().Clients()->SetEPGMaxFutureDays( + std::static_pointer_cast<const CSettingInt>(setting)->GetValue()); + } +} + +void CPVRGUIActionListener::OnSettingAction(const std::shared_ptr<const CSetting>& setting) +{ + if (setting == nullptr) + return; + + const std::string& settingId = setting->GetId(); + if (settingId == CSettings::SETTING_PVRMANAGER_RESETDB) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Database>().ResetDatabase(false); + } + else if (settingId == CSettings::SETTING_EPG_RESETEPG) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Database>().ResetDatabase(true); + } + else if (settingId == CSettings::SETTING_PVRMANAGER_CLIENTPRIORITIES) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + CGUIDialog* dialog = CServiceBroker::GetGUI()->GetWindowManager().GetDialog( + WINDOW_DIALOG_PVR_CLIENT_PRIORITIES); + if (dialog) + { + dialog->Open(); + CServiceBroker::GetPVRManager().ChannelGroups()->UpdateFromClients({}); + } + } + } + else if (settingId == CSettings::SETTING_PVRMANAGER_CHANNELMANAGER) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + CGUIDialog* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_CHANNEL_MANAGER); + if (dialog) + dialog->Open(); + } + } + else if (settingId == CSettings::SETTING_PVRMANAGER_GROUPMANAGER) + { + if (CServiceBroker::GetPVRManager().IsStarted()) + { + CGUIDialog* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetDialog(WINDOW_DIALOG_PVR_GROUP_MANAGER); + if (dialog) + dialog->Open(); + } + } + else if (settingId == CSettings::SETTING_PVRMANAGER_CHANNELSCAN) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().StartChannelScan(); + } + else if (settingId == CSettings::SETTING_PVRMENU_SEARCHICONS) + { + CServiceBroker::GetPVRManager().TriggerSearchMissingChannelIcons(); + } + else if (settingId == CSettings::SETTING_PVRCLIENT_MENUHOOK) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Clients>().ProcessSettingsMenuHooks(); + } + else if (settingId == CSettings::SETTING_PVRMANAGER_ADDONS) + { + const std::vector<std::string> params{"addons://default_binary_addons_source/kodi.pvrclient", + "return"}; + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(WINDOW_ADDON_BROWSER, params); + } +} + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionListener.h b/xbmc/pvr/guilib/PVRGUIActionListener.h new file mode 100644 index 0000000..d24818b --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionListener.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2005-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "interfaces/IActionListener.h" +#include "settings/lib/ISettingCallback.h" + +namespace PVR +{ + +class CPVRManager; +enum class ChannelSwitchMode; +enum class PVREvent; + +class CPVRGUIActionListener : public IActionListener, public ISettingCallback +{ +public: + CPVRGUIActionListener(); + ~CPVRGUIActionListener() override; + + void Init(CPVRManager& mgr); + void Deinit(CPVRManager& mgr); + + // IActionListener implementation + bool OnAction(const CAction& action) override; + + // ISettingCallback implementation + void OnSettingChanged(const std::shared_ptr<const CSetting>& setting) override; + void OnSettingAction(const std::shared_ptr<const CSetting>& setting) override; + + void OnPVRManagerEvent(const PVREvent& event); + +private: + CPVRGUIActionListener(const CPVRGUIActionListener&) = delete; + CPVRGUIActionListener& operator=(const CPVRGUIActionListener&) = delete; + + static ChannelSwitchMode GetChannelSwitchMode(int iAction); +}; + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp b/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp new file mode 100644 index 0000000..80fb90e --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsChannels.cpp @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionsChannels.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" +#include "input/actions/Action.h" +#include "input/actions/ActionIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/windows/GUIWindowPVRBase.h" +#include "settings/Settings.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <chrono> +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +using namespace PVR; +using namespace KODI::MESSAGING; + +void CPVRChannelSwitchingInputHandler::AppendChannelNumberCharacter(char cCharacter) +{ + // special case. if only a single zero was typed in, switch to previously played channel. + if (GetCurrentDigitCount() == 0 && cCharacter == '0') + { + SwitchToPreviousChannel(); + return; + } + + CPVRChannelNumberInputHandler::AppendChannelNumberCharacter(cCharacter); +} + +void CPVRChannelSwitchingInputHandler::GetChannelNumbers(std::vector<std::string>& channelNumbers) +{ + const CPVRManager& pvrMgr = CServiceBroker::GetPVRManager(); + const std::shared_ptr<CPVRChannel> playingChannel = pvrMgr.PlaybackState()->GetPlayingChannel(); + if (playingChannel) + { + const std::shared_ptr<CPVRChannelGroup> group = + pvrMgr.ChannelGroups()->GetGroupAll(playingChannel->IsRadio()); + if (group) + group->GetChannelNumbers(channelNumbers); + } +} + +void CPVRChannelSwitchingInputHandler::OnInputDone() +{ + CPVRChannelNumber channelNumber = GetChannelNumber(); + if (channelNumber.GetChannelNumber()) + SwitchToChannel(channelNumber); +} + +void CPVRChannelSwitchingInputHandler::SwitchToChannel(const CPVRChannelNumber& channelNumber) +{ + if (channelNumber.IsValid() && CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying()) + { + const std::shared_ptr<CPVRChannel> playingChannel = + CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (playingChannel) + { + bool bRadio = playingChannel->IsRadio(); + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bRadio); + + if (channelNumber != group->GetChannelNumber(playingChannel)) + { + // channel number present in active group? + std::shared_ptr<CPVRChannelGroupMember> groupMember = + group->GetByChannelNumber(channelNumber); + + if (!groupMember) + { + // channel number present in any group? + const CPVRChannelGroups* groupAccess = + CServiceBroker::GetPVRManager().ChannelGroups()->Get(bRadio); + const std::vector<std::shared_ptr<CPVRChannelGroup>> groups = + groupAccess->GetMembers(true); + for (const auto& currentGroup : groups) + { + if (currentGroup == group) // we have already checked this group + continue; + + groupMember = currentGroup->GetByChannelNumber(channelNumber); + if (groupMember) + break; + } + } + + if (groupMember) + { + CServiceBroker::GetAppMessenger()->PostMsg( + TMSG_GUI_ACTION, WINDOW_INVALID, -1, + static_cast<void*>(new CAction( + ACTION_CHANNEL_SWITCH, static_cast<float>(channelNumber.GetChannelNumber()), + static_cast<float>(channelNumber.GetSubChannelNumber())))); + } + } + } + } +} + +void CPVRChannelSwitchingInputHandler::SwitchToPreviousChannel() +{ + const std::shared_ptr<CPVRPlaybackState> playbackState = + CServiceBroker::GetPVRManager().PlaybackState(); + if (playbackState->IsPlaying()) + { + const std::shared_ptr<CPVRChannel> playingChannel = playbackState->GetPlayingChannel(); + if (playingChannel) + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + playbackState->GetPreviousToLastPlayedChannelGroupMember(playingChannel->IsRadio()); + if (groupMember) + { + const CPVRChannelNumber channelNumber = groupMember->ChannelNumber(); + CServiceBroker::GetAppMessenger()->SendMsg( + TMSG_GUI_ACTION, WINDOW_INVALID, -1, + static_cast<void*>(new CAction( + ACTION_CHANNEL_SWITCH, static_cast<float>(channelNumber.GetChannelNumber()), + static_cast<float>(channelNumber.GetSubChannelNumber())))); + } + } + } +} + +CPVRGUIActionsChannels::CPVRGUIActionsChannels() + : m_settings({CSettings::SETTING_PVRMANAGER_PRESELECTPLAYINGCHANNEL}) +{ + RegisterChannelNumberInputHandler(&m_channelNumberInputHandler); +} + +CPVRGUIActionsChannels::~CPVRGUIActionsChannels() +{ + DeregisterChannelNumberInputHandler(&m_channelNumberInputHandler); +} + +void CPVRGUIActionsChannels::RegisterChannelNumberInputHandler( + CPVRChannelNumberInputHandler* handler) +{ + if (handler) + handler->Events().Subscribe(this, &CPVRGUIActionsChannels::Notify); +} + +void CPVRGUIActionsChannels::DeregisterChannelNumberInputHandler( + CPVRChannelNumberInputHandler* handler) +{ + if (handler) + handler->Events().Unsubscribe(this); +} + +void CPVRGUIActionsChannels::Notify(const PVRChannelNumberInputChangedEvent& event) +{ + m_events.Publish(event); +} + +bool CPVRGUIActionsChannels::HideChannel(const CFileItem& item) const +{ + const std::shared_ptr<CPVRChannel> channel = item.GetPVRChannelInfoTag(); + + if (!channel) + return false; + + if (!CGUIDialogYesNo::ShowAndGetInput( + CVariant{19054}, // "Hide channel" + CVariant{19039}, // "Are you sure you want to hide this channel?" + CVariant{""}, CVariant{channel->ChannelName()})) + return false; + + if (!CServiceBroker::GetPVRManager() + .ChannelGroups() + ->GetGroupAll(channel->IsRadio()) + ->RemoveFromGroup(channel)) + return false; + + CGUIWindowPVRBase* pvrWindow = + dynamic_cast<CGUIWindowPVRBase*>(CServiceBroker::GetGUI()->GetWindowManager().GetWindow( + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow())); + if (pvrWindow) + pvrWindow->DoRefresh(); + else + CLog::LogF(LOGERROR, "Called on non-pvr window. No refresh possible."); + + return true; +} + +bool CPVRGUIActionsChannels::StartChannelScan() +{ + return StartChannelScan(PVR_INVALID_CLIENT_ID); +} + +bool CPVRGUIActionsChannels::StartChannelScan(int clientId) +{ + if (!CServiceBroker::GetPVRManager().IsStarted() || IsRunningChannelScan()) + return false; + + std::shared_ptr<CPVRClient> scanClient; + std::vector<std::shared_ptr<CPVRClient>> possibleScanClients = + CServiceBroker::GetPVRManager().Clients()->GetClientsSupportingChannelScan(); + m_bChannelScanRunning = true; + + if (clientId != PVR_INVALID_CLIENT_ID) + { + const auto it = + std::find_if(possibleScanClients.cbegin(), possibleScanClients.cend(), + [clientId](const auto& client) { return client->GetID() == clientId; }); + + if (it != possibleScanClients.cend()) + scanClient = (*it); + + if (!scanClient) + { + CLog::LogF(LOGERROR, + "Provided client id '{}' could not be found in list of possible scan clients!", + clientId); + m_bChannelScanRunning = false; + return false; + } + } + /* multiple clients found */ + else if (possibleScanClients.size() > 1) + { + CGUIDialogSelect* pDialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (!pDialog) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!"); + m_bChannelScanRunning = false; + return false; + } + + pDialog->Reset(); + pDialog->SetHeading(CVariant{19119}); // "On which backend do you want to search?" + + for (const auto& client : possibleScanClients) + pDialog->Add(client->GetFriendlyName()); + + pDialog->Open(); + + int selection = pDialog->GetSelectedItem(); + if (selection >= 0) + scanClient = possibleScanClients[selection]; + } + /* one client found */ + else if (possibleScanClients.size() == 1) + { + scanClient = possibleScanClients[0]; + } + /* no clients found */ + else if (!scanClient) + { + HELPERS::ShowOKDialogText( + CVariant{19033}, // "Information" + CVariant{19192}); // "None of the connected PVR backends supports scanning for channels." + m_bChannelScanRunning = false; + return false; + } + + /* start the channel scan */ + CLog::LogFC(LOGDEBUG, LOGPVR, "Starting to scan for channels on client {}", + scanClient->GetFriendlyName()); + auto start = std::chrono::steady_clock::now(); + + /* do the scan */ + if (scanClient->StartChannelScan() != PVR_ERROR_NO_ERROR) + HELPERS::ShowOKDialogText( + CVariant{257}, // "Error" + CVariant{ + 19193}); // "The channel scan can't be started. Check the log for more information about this message." + + auto end = std::chrono::steady_clock::now(); + auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); + + CLog::LogFC(LOGDEBUG, LOGPVR, "Channel scan finished after {} ms", duration.count()); + + m_bChannelScanRunning = false; + return true; +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRGUIActionsChannels::GetChannelGroupMember( + const std::shared_ptr<CPVRChannel>& channel) const +{ + if (!channel) + return {}; + + std::shared_ptr<CPVRChannelGroupMember> groupMember; + + // first, try whether the channel is contained in the active channel group, except + // if a window is active which never uses the active channel group, e.g. Timers window + const int activeWindowID = CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(); + + static std::vector<int> windowIDs = { + WINDOW_TV_RECORDINGS, WINDOW_TV_TIMERS, WINDOW_TV_TIMER_RULES, WINDOW_TV_SEARCH, + WINDOW_RADIO_RECORDINGS, WINDOW_RADIO_TIMERS, WINDOW_RADIO_TIMER_RULES, WINDOW_RADIO_SEARCH, + }; + + if (std::find(windowIDs.cbegin(), windowIDs.cend(), activeWindowID) == windowIDs.cend()) + { + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(channel->IsRadio()); + if (group) + groupMember = group->GetByUniqueID(channel->StorageId()); + } + + // as fallback, obtain the member from the 'all channels' group + if (!groupMember) + { + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAll(channel->IsRadio()); + if (group) + groupMember = group->GetByUniqueID(channel->StorageId()); + } + + return groupMember; +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRGUIActionsChannels::GetChannelGroupMember( + const CFileItem& item) const +{ + std::shared_ptr<CPVRChannelGroupMember> groupMember = item.GetPVRChannelGroupMemberInfoTag(); + + if (!groupMember) + groupMember = GetChannelGroupMember(CPVRItem(std::make_shared<CFileItem>(item)).GetChannel()); + + return groupMember; +} + +CPVRChannelNumberInputHandler& CPVRGUIActionsChannels::GetChannelNumberInputHandler() +{ + // window/dialog specific input handler + CPVRChannelNumberInputHandler* windowInputHandler = dynamic_cast<CPVRChannelNumberInputHandler*>( + CServiceBroker::GetGUI()->GetWindowManager().GetWindow( + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindowOrDialog())); + if (windowInputHandler) + return *windowInputHandler; + + // default + return m_channelNumberInputHandler; +} + +CPVRGUIChannelNavigator& CPVRGUIActionsChannels::GetChannelNavigator() +{ + return m_channelNavigator; +} + +void CPVRGUIActionsChannels::OnPlaybackStarted(const CFileItem& item) +{ + const std::shared_ptr<CPVRChannelGroupMember> groupMember = GetChannelGroupMember(item); + if (groupMember) + { + m_channelNavigator.SetPlayingChannel(groupMember); + SetSelectedChannelPath(groupMember->Channel()->IsRadio(), groupMember->Path()); + } +} + +void CPVRGUIActionsChannels::OnPlaybackStopped(const CFileItem& item) +{ + if (item.HasPVRChannelInfoTag() || item.HasEPGInfoTag()) + { + m_channelNavigator.ClearPlayingChannel(); + } +} + +void CPVRGUIActionsChannels::SetSelectedChannelPath(bool bRadio, const std::string& path) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (bRadio) + m_selectedChannelPathRadio = path; + else + m_selectedChannelPathTV = path; +} + +std::string CPVRGUIActionsChannels::GetSelectedChannelPath(bool bRadio) const +{ + if (m_settings.GetBoolValue(CSettings::SETTING_PVRMANAGER_PRESELECTPLAYINGCHANNEL)) + { + CPVRManager& mgr = CServiceBroker::GetPVRManager(); + + // if preselect playing channel is activated, return the path of the playing channel, if any. + const std::shared_ptr<CPVRChannelGroupMember> playingChannel = + mgr.PlaybackState()->GetPlayingChannelGroupMember(); + if (playingChannel && playingChannel->IsRadio() == bRadio) + return playingChannel->Path(); + + const std::shared_ptr<CPVREpgInfoTag> playingTag = mgr.PlaybackState()->GetPlayingEpgTag(); + if (playingTag && playingTag->IsRadio() == bRadio) + { + const std::shared_ptr<CPVRChannel> channel = + mgr.ChannelGroups()->GetChannelForEpgTag(playingTag); + if (channel) + return GetChannelGroupMember(channel)->Path(); + } + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + return bRadio ? m_selectedChannelPathRadio : m_selectedChannelPathTV; +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsChannels.h b/xbmc/pvr/guilib/PVRGUIActionsChannels.h new file mode 100644 index 0000000..a7656fe --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsChannels.h @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "pvr/PVRChannelNumberInputHandler.h" +#include "pvr/guilib/PVRGUIChannelNavigator.h" +#include "pvr/settings/PVRSettings.h" +#include "threads/CriticalSection.h" + +#include <memory> +#include <string> +#include <vector> + +class CFileItem; + +namespace PVR +{ +class CPVRChannel; +class CPVRChannelGroupMember; + +class CPVRChannelSwitchingInputHandler : public CPVRChannelNumberInputHandler +{ +public: + // CPVRChannelNumberInputHandler implementation + void GetChannelNumbers(std::vector<std::string>& channelNumbers) override; + void AppendChannelNumberCharacter(char cCharacter) override; + void OnInputDone() override; + +private: + /*! + * @brief Switch to the channel with the given number. + * @param channelNumber the channel number + */ + void SwitchToChannel(const CPVRChannelNumber& channelNumber); + + /*! + * @brief Switch to the previously played channel. + */ + void SwitchToPreviousChannel(); +}; + +class CPVRGUIActionsChannels : public IPVRComponent +{ +public: + CPVRGUIActionsChannels(); + ~CPVRGUIActionsChannels() override; + + /*! + * @brief Get the events available for CEventStream. + * @return The events. + */ + CEventStream<PVRChannelNumberInputChangedEvent>& Events() { return m_events; } + + /*! + * @brief Register a handler for channel number input. + * @param handler The handler to register. + */ + void RegisterChannelNumberInputHandler(CPVRChannelNumberInputHandler* handler); + + /*! + * @brief Deregister a handler for channel number input. + * @param handler The handler to deregister. + */ + void DeregisterChannelNumberInputHandler(CPVRChannelNumberInputHandler* handler); + + /*! + * @brief CEventStream callback for channel number input changes. + * @param event The event. + */ + void Notify(const PVRChannelNumberInputChangedEvent& event); + + /*! + * @brief Hide a channel, always showing a confirmation dialog. + * @param item containing a channel or an epg tag. + * @return true on success, false otherwise. + */ + bool HideChannel(const CFileItem& item) const; + + /*! + * @brief Open a selection dialog and start a channel scan on the selected client. + * @return true on success, false otherwise. + */ + bool StartChannelScan(); + + /*! + * @brief Start a channel scan on the specified client or open a dialog to select a client + * @param clientId the id of client to scan or PVR_INVALID_CLIENT_ID if a dialog will be opened + * @return true on success, false otherwise. + */ + bool StartChannelScan(int clientId); + + /*! + * @return True when a channel scan is currently running, false otherwise. + */ + bool IsRunningChannelScan() const { return m_bChannelScanRunning; } + + /*! + * @brief Get a channel group member for the given channel, either from the currently active + * group or if not found there, from the 'all channels' group. + * @param channel the channel. + * @return the group member or nullptr if not found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMember( + const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @brief Get a channel group member for the given item, either from the currently active group + * or if not found there, from the 'all channels' group. + * @param item the item containing a channel, channel group, recording, timer or epg tag. + * @return the group member or nullptr if not found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetChannelGroupMember(const CFileItem& item) const; + + /*! + * @brief Get the currently active channel number input handler. + * @return the handler. + */ + CPVRChannelNumberInputHandler& GetChannelNumberInputHandler(); + + /*! + * @brief Get the channel navigator. + * @return the navigator. + */ + CPVRGUIChannelNavigator& GetChannelNavigator(); + + /*! + * @brief Inform GUI actions that playback of an item just started. + * @param item The item that started to play. + */ + void OnPlaybackStarted(const CFileItem& item); + + /*! + * @brief Inform GUI actions that playback of an item was stopped due to user interaction. + * @param item The item that stopped to play. + */ + void OnPlaybackStopped(const CFileItem& item); + + /*! + * @brief Get the currently selected channel item path; used across several windows/dialogs to + * share item selection. + * @param bRadio True to query the selected path for PVR radio, false for Live TV. + * @return the path. + */ + std::string GetSelectedChannelPath(bool bRadio) const; + + /*! + * @brief Set the currently selected channel item path; used across several windows/dialogs to + * share item selection. + * @param bRadio True to set the selected path for PVR radio, false for Live TV. + * @param path The new path to set. + */ + void SetSelectedChannelPath(bool bRadio, const std::string& path); + +private: + CPVRGUIActionsChannels(const CPVRGUIActionsChannels&) = delete; + CPVRGUIActionsChannels const& operator=(CPVRGUIActionsChannels const&) = delete; + + CPVRChannelSwitchingInputHandler m_channelNumberInputHandler; + bool m_bChannelScanRunning{false}; + CPVRGUIChannelNavigator m_channelNavigator; + CEventSource<PVRChannelNumberInputChangedEvent> m_events; + + mutable CCriticalSection m_critSection; + CPVRSettings m_settings; + std::string m_selectedChannelPathTV; + std::string m_selectedChannelPathRadio; +}; + +namespace GUI +{ +// pretty scope and name +using Channels = CPVRGUIActionsChannels; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsClients.cpp b/xbmc/pvr/guilib/PVRGUIActionsClients.cpp new file mode 100644 index 0000000..b9c598c --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsClients.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionsClients.h" + +#include "ServiceBroker.h" +#include "dialogs/GUIDialogSelect.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClientMenuHooks.h" +#include "pvr/addons/PVRClients.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <iterator> +#include <memory> +#include <utility> +#include <vector> + +using namespace KODI::MESSAGING; + +using namespace PVR; + +bool CPVRGUIActionsClients::ProcessSettingsMenuHooks() +{ + const CPVRClientMap clients = CServiceBroker::GetPVRManager().Clients()->GetCreatedClients(); + + std::vector<std::pair<std::shared_ptr<CPVRClient>, CPVRClientMenuHook>> settingsHooks; + for (const auto& client : clients) + { + const auto hooks = client.second->GetMenuHooks()->GetSettingsHooks(); + std::transform(hooks.cbegin(), hooks.cend(), std::back_inserter(settingsHooks), + [&client](const auto& hook) { return std::make_pair(client.second, hook); }); + } + + if (settingsHooks.empty()) + { + HELPERS::ShowOKDialogText( + CVariant{19033}, // "Information" + CVariant{19347}); // "None of the active PVR clients does provide client-specific settings." + return true; // no settings hooks, no error + } + + auto selectedHook = settingsHooks.begin(); + + // if there is only one settings hook, execute it directly, otherwise let the user select + if (settingsHooks.size() > 1) + { + CGUIDialogSelect* pDialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (!pDialog) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!"); + return false; + } + + pDialog->Reset(); + pDialog->SetHeading(CVariant{19196}); // "PVR client specific actions" + + for (const auto& hook : settingsHooks) + { + if (clients.size() == 1) + pDialog->Add(hook.second.GetLabel()); + else + pDialog->Add(hook.first->GetFriendlyName() + ": " + hook.second.GetLabel()); + } + + pDialog->Open(); + + int selection = pDialog->GetSelectedItem(); + if (selection < 0) + return true; // cancelled + + std::advance(selectedHook, selection); + } + return selectedHook->first->CallSettingsMenuHook(selectedHook->second) == PVR_ERROR_NO_ERROR; +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsClients.h b/xbmc/pvr/guilib/PVRGUIActionsClients.h new file mode 100644 index 0000000..7c48274 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsClients.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" + +namespace PVR +{ +class CPVRGUIActionsClients : public IPVRComponent +{ +public: + CPVRGUIActionsClients() = default; + ~CPVRGUIActionsClients() override = default; + + /*! + * @brief Select and invoke client-specific settings actions + * @return true on success, false otherwise. + */ + bool ProcessSettingsMenuHooks(); + +private: + CPVRGUIActionsClients(const CPVRGUIActionsClients&) = delete; + CPVRGUIActionsClients const& operator=(CPVRGUIActionsClients const&) = delete; +}; + +namespace GUI +{ +// pretty scope and name +using Clients = CPVRGUIActionsClients; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp b/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp new file mode 100644 index 0000000..53bbf81 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsDatabase.cpp @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionsDatabase.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVRDatabase.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgDatabase.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/recordings/PVRRecordingsPath.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" +#include "video/VideoDatabase.h" + +#include <memory> +#include <string> + +using namespace PVR; + +namespace +{ +class CPVRGUIDatabaseResetComponentsSelector +{ +public: + CPVRGUIDatabaseResetComponentsSelector() = default; + virtual ~CPVRGUIDatabaseResetComponentsSelector() = default; + + bool Select() + { + CGUIDialogSelect* pDlgSelect = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT); + if (!pDlgSelect) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_SELECT!"); + return false; + } + + CFileItemList options; + + const std::shared_ptr<CFileItem> itemAll = + std::make_shared<CFileItem>(StringUtils::Format(g_localizeStrings.Get(593))); // All + itemAll->SetPath("all"); + options.Add(itemAll); + + // if channels are cleared, groups, EPG data and providers must also be cleared + const std::shared_ptr<CFileItem> itemChannels = + std::make_shared<CFileItem>(StringUtils::Format("{}, {}, {}, {}", + g_localizeStrings.Get(19019), // Channels + g_localizeStrings.Get(19146), // Groups + g_localizeStrings.Get(19069), // Guide + g_localizeStrings.Get(19334))); // Providers + itemChannels->SetPath("channels"); + itemChannels->Select(true); // preselect this item in dialog + options.Add(itemChannels); + + const std::shared_ptr<CFileItem> itemGroups = + std::make_shared<CFileItem>(g_localizeStrings.Get(19146)); // Groups + itemGroups->SetPath("groups"); + options.Add(itemGroups); + + const std::shared_ptr<CFileItem> itemGuide = + std::make_shared<CFileItem>(g_localizeStrings.Get(19069)); // Guide + itemGuide->SetPath("guide"); + options.Add(itemGuide); + + const std::shared_ptr<CFileItem> itemProviders = + std::make_shared<CFileItem>(g_localizeStrings.Get(19334)); // Providers + itemProviders->SetPath("providers"); + options.Add(itemProviders); + + const std::shared_ptr<CFileItem> itemReminders = + std::make_shared<CFileItem>(g_localizeStrings.Get(19215)); // Reminders + itemReminders->SetPath("reminders"); + options.Add(itemReminders); + + const std::shared_ptr<CFileItem> itemRecordings = + std::make_shared<CFileItem>(g_localizeStrings.Get(19017)); // Recordings + itemRecordings->SetPath("recordings"); + options.Add(itemRecordings); + + const std::shared_ptr<CFileItem> itemClients = + std::make_shared<CFileItem>(g_localizeStrings.Get(24019)); // PVR clients + itemClients->SetPath("clients"); + options.Add(itemClients); + + pDlgSelect->Reset(); + pDlgSelect->SetHeading(CVariant{g_localizeStrings.Get(19185)}); // "Clear data" + pDlgSelect->SetItems(options); + pDlgSelect->SetMultiSelection(true); + pDlgSelect->Open(); + + if (!pDlgSelect->IsConfirmed()) + return false; + + for (int i : pDlgSelect->GetSelectedItems()) + { + const std::string path = options.Get(i)->GetPath(); + + m_bResetChannels |= (path == "channels" || path == "all"); + m_bResetGroups |= (path == "groups" || path == "all"); + m_bResetGuide |= (path == "guide" || path == "all"); + m_bResetProviders |= (path == "providers" || path == "all"); + m_bResetReminders |= (path == "reminders" || path == "all"); + m_bResetRecordings |= (path == "recordings" || path == "all"); + m_bResetClients |= (path == "clients" || path == "all"); + } + + m_bResetGroups |= m_bResetChannels; + m_bResetGuide |= m_bResetChannels; + m_bResetProviders |= m_bResetChannels; + + return (m_bResetChannels || m_bResetGroups || m_bResetGuide || m_bResetProviders || + m_bResetReminders || m_bResetRecordings || m_bResetClients); + } + + bool IsResetChannelsSelected() const { return m_bResetChannels; } + bool IsResetGroupsSelected() const { return m_bResetGroups; } + bool IsResetGuideSelected() const { return m_bResetGuide; } + bool IsResetProvidersSelected() const { return m_bResetProviders; } + bool IsResetRemindersSelected() const { return m_bResetReminders; } + bool IsResetRecordingsSelected() const { return m_bResetRecordings; } + bool IsResetClientsSelected() const { return m_bResetClients; } + +private: + bool m_bResetChannels = false; + bool m_bResetGroups = false; + bool m_bResetGuide = false; + bool m_bResetProviders = false; + bool m_bResetReminders = false; + bool m_bResetRecordings = false; + bool m_bResetClients = false; +}; + +} // unnamed namespace + +bool CPVRGUIActionsDatabase::ResetDatabase(bool bResetEPGOnly) +{ + CGUIDialogProgress* pDlgProgress = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>( + WINDOW_DIALOG_PROGRESS); + if (!pDlgProgress) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PROGRESS!"); + return false; + } + + bool bResetChannels = false; + bool bResetGroups = false; + bool bResetGuide = false; + bool bResetProviders = false; + bool bResetReminders = false; + bool bResetRecordings = false; + bool bResetClients = false; + + if (bResetEPGOnly) + { + if (!CGUIDialogYesNo::ShowAndGetInput( + CVariant{19098}, // "Warning!" + CVariant{19188})) // "All guide data will be cleared. Are you sure?" + return false; + + bResetGuide = true; + } + else + { + if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalPIN() != + ParentalCheckResult::SUCCESS) + return false; + + CPVRGUIDatabaseResetComponentsSelector selector; + if (!selector.Select()) + return false; + + if (!CGUIDialogYesNo::ShowAndGetInput( + CVariant{19098}, // "Warning!" + CVariant{19186})) // "All selected data will be cleared. ... Are you sure?" + return false; + + bResetChannels = selector.IsResetChannelsSelected(); + bResetGroups = selector.IsResetGroupsSelected(); + bResetGuide = selector.IsResetGuideSelected(); + bResetProviders = selector.IsResetProvidersSelected(); + bResetReminders = selector.IsResetRemindersSelected(); + bResetRecordings = selector.IsResetRecordingsSelected(); + bResetClients = selector.IsResetClientsSelected(); + } + + CDateTime::ResetTimezoneBias(); + + CLog::LogFC(LOGDEBUG, LOGPVR, "PVR clearing {} database", bResetEPGOnly ? "EPG" : "PVR and EPG"); + + pDlgProgress->SetHeading(CVariant{313}); // "Cleaning database" + pDlgProgress->SetLine(0, CVariant{g_localizeStrings.Get(19187)}); // "Clearing all related data." + pDlgProgress->SetLine(1, CVariant{""}); + pDlgProgress->SetLine(2, CVariant{""}); + + pDlgProgress->Open(); + pDlgProgress->Progress(); + + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying()) + { + CLog::Log(LOGINFO, "PVR is stopping playback for {} database reset", + bResetEPGOnly ? "EPG" : "PVR and EPG"); + CServiceBroker::GetAppMessenger()->SendMsg(TMSG_MEDIA_STOP); + } + + const std::shared_ptr<CPVRDatabase> pvrDatabase(CServiceBroker::GetPVRManager().GetTVDatabase()); + const std::shared_ptr<CPVREpgDatabase> epgDatabase( + CServiceBroker::GetPVRManager().EpgContainer().GetEpgDatabase()); + + // increase db open refcounts, so they don't get closed during following pvr manager shutdown + pvrDatabase->Open(); + epgDatabase->Open(); + + // stop pvr manager; close both pvr and epg databases + CServiceBroker::GetPVRManager().Stop(); + + const int iProgressStepPercentage = + 100 / ((2 * bResetChannels) + bResetGroups + bResetGuide + bResetProviders + bResetReminders + + bResetRecordings + bResetClients + 1); + int iProgressStepsDone = 0; + + if (bResetProviders) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all providers + pvrDatabase->DeleteProviders(); + } + + if (bResetGuide) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // reset channel's EPG pointers + pvrDatabase->ResetEPG(); + + // delete all entries from the EPG database + epgDatabase->DeleteEpg(); + } + + if (bResetGroups) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all channel groups (including data only available locally, like user defined groups) + pvrDatabase->DeleteChannelGroups(); + } + + if (bResetChannels) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all channels (including data only available locally, like user set icons) + pvrDatabase->DeleteChannels(); + } + + if (bResetReminders) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all timers data (e.g. all reminders, which are only stored locally) + pvrDatabase->DeleteTimers(); + } + + if (bResetClients) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all clients data (e.g priorities, which are only stored locally) + pvrDatabase->DeleteClients(); + } + + if (bResetChannels || bResetRecordings) + { + CVideoDatabase videoDatabase; + + if (videoDatabase.Open()) + { + if (bResetChannels) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all channel's entries (e.g. settings, bookmarks, stream details) + videoDatabase.EraseAllForPath("pvr://channels/"); + } + + if (bResetRecordings) + { + pDlgProgress->SetPercentage(iProgressStepPercentage * ++iProgressStepsDone); + pDlgProgress->Progress(); + + // delete all recording's entries (e.g. settings, bookmarks, stream details) + videoDatabase.EraseAllForPath(CPVRRecordingsPath::PATH_RECORDINGS); + } + + videoDatabase.Close(); + } + } + + // decrease db open refcounts; this actually closes dbs because refcounts drops to zero + pvrDatabase->Close(); + epgDatabase->Close(); + + CLog::LogFC(LOGDEBUG, LOGPVR, "{} database cleared", bResetEPGOnly ? "EPG" : "PVR and EPG"); + + CLog::Log(LOGINFO, "Restarting the PVR Manager after {} database reset", + bResetEPGOnly ? "EPG" : "PVR and EPG"); + CServiceBroker::GetPVRManager().Start(); + + pDlgProgress->SetPercentage(100); + pDlgProgress->Close(); + return true; +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsDatabase.h b/xbmc/pvr/guilib/PVRGUIActionsDatabase.h new file mode 100644 index 0000000..62eb484 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsDatabase.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" + +namespace PVR +{ +class CPVRGUIActionsDatabase : public IPVRComponent +{ +public: + CPVRGUIActionsDatabase() = default; + ~CPVRGUIActionsDatabase() override = default; + + /*! + * @brief Reset the TV database to it's initial state and delete all the data. + * @param bResetEPGOnly True to only reset the EPG database, false to reset both PVR and EPG + * database. + * @return true on success, false otherwise. + */ + bool ResetDatabase(bool bResetEPGOnly); + +private: + CPVRGUIActionsDatabase(const CPVRGUIActionsDatabase&) = delete; + CPVRGUIActionsDatabase const& operator=(CPVRGUIActionsDatabase const&) = delete; +}; + +namespace GUI +{ +// pretty scope and name +using Database = CPVRGUIActionsDatabase; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp b/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp new file mode 100644 index 0000000..8b5cc9b --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsEPG.cpp @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionsEPG.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIKeyboardFactory.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/dialogs/GUIDialogPVRChannelGuide.h" +#include "pvr/dialogs/GUIDialogPVRGuideInfo.h" +#include "pvr/epg/EpgContainer.h" +#include "pvr/epg/EpgSearchFilter.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "pvr/windows/GUIWindowPVRSearch.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <memory> +#include <string> + +using namespace PVR; + +namespace +{ +PVR::CGUIWindowPVRSearchBase* GetSearchWindow(bool bRadio) +{ + const int windowSearchId = bRadio ? WINDOW_RADIO_SEARCH : WINDOW_TV_SEARCH; + + PVR::CGUIWindowPVRSearchBase* windowSearch; + + CGUIWindowManager& windowMgr = CServiceBroker::GetGUI()->GetWindowManager(); + if (bRadio) + windowSearch = windowMgr.GetWindow<PVR::CGUIWindowPVRRadioSearch>(windowSearchId); + else + windowSearch = windowMgr.GetWindow<PVR::CGUIWindowPVRTVSearch>(windowSearchId); + + if (!windowSearch) + CLog::LogF(LOGERROR, "Unable to get {}!", bRadio ? "WINDOW_RADIO_SEARCH" : "WINDOW_TV_SEARCH"); + + return windowSearch; +} +} // unnamed namespace + +bool CPVRGUIActionsEPG::ShowEPGInfo(const CFileItem& item) const +{ + const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel()); + if (channel && CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock( + channel) != ParentalCheckResult::SUCCESS) + return false; + + const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag()); + if (!epgTag) + { + CLog::LogF(LOGERROR, "No epg tag!"); + return false; + } + + CGUIDialogPVRGuideInfo* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRGuideInfo>( + WINDOW_DIALOG_PVR_GUIDE_INFO); + if (!pDlgInfo) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_GUIDE_INFO!"); + return false; + } + + pDlgInfo->SetProgInfo(std::make_shared<CFileItem>(epgTag)); + pDlgInfo->Open(); + return true; +} + +bool CPVRGUIActionsEPG::ShowChannelEPG(const CFileItem& item) const +{ + const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel()); + if (channel && CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock( + channel) != ParentalCheckResult::SUCCESS) + return false; + + CGUIDialogPVRChannelGuide* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRChannelGuide>( + WINDOW_DIALOG_PVR_CHANNEL_GUIDE); + if (!pDlgInfo) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_CHANNEL_GUIDE!"); + return false; + } + + pDlgInfo->Open(channel); + return true; +} + +bool CPVRGUIActionsEPG::FindSimilar(const CFileItem& item) const +{ + CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(CPVRItem(item).IsRadio()); + if (!windowSearch) + return false; + + //! @todo If we want dialogs to spawn program search in a clean way - without having to force-close any + // other dialogs - we must introduce a search dialog with functionality similar to the search window. + + for (int iId = CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog( + true /* ignoreClosing */); + iId != WINDOW_INVALID; + iId = CServiceBroker::GetGUI()->GetWindowManager().GetTopmostModalDialog( + true /* ignoreClosing */)) + { + CLog::LogF(LOGWARNING, + "Have to close modal dialog with id {} before search window can be opened.", iId); + + CGUIWindow* window = CServiceBroker::GetGUI()->GetWindowManager().GetWindow(iId); + if (window) + { + window->Close(); + } + else + { + CLog::LogF(LOGERROR, "Unable to get window instance {}! Cannot open search window.", iId); + return false; // return, otherwise we run into an endless loop + } + } + + windowSearch->SetItemToSearch(item); + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID()); + return true; +}; + +bool CPVRGUIActionsEPG::ExecuteSavedSearch(const CFileItem& item) +{ + const auto searchFilter = item.GetEPGSearchFilter(); + + if (!searchFilter) + { + CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present."); + return false; + } + + CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(searchFilter->IsRadio()); + if (!windowSearch) + return false; + + windowSearch->SetItemToSearch(item); + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID()); + return true; +} + +bool CPVRGUIActionsEPG::EditSavedSearch(const CFileItem& item) +{ + const auto searchFilter = item.GetEPGSearchFilter(); + + if (!searchFilter) + { + CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present."); + return false; + } + + CGUIWindowPVRSearchBase* windowSearch = GetSearchWindow(searchFilter->IsRadio()); + if (!windowSearch) + return false; + + if (windowSearch->OpenDialogSearch(item) == CGUIDialogPVRGuideSearch::Result::SEARCH) + CServiceBroker::GetGUI()->GetWindowManager().ActivateWindow(windowSearch->GetID()); + + return true; +} + +bool CPVRGUIActionsEPG::RenameSavedSearch(const CFileItem& item) +{ + const auto searchFilter = item.GetEPGSearchFilter(); + + if (!searchFilter) + { + CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present."); + return false; + } + + std::string title = searchFilter->GetTitle(); + if (CGUIKeyboardFactory::ShowAndGetInput(title, + CVariant{g_localizeStrings.Get(528)}, // "Enter title" + false)) + { + searchFilter->SetTitle(title); + CServiceBroker::GetPVRManager().EpgContainer().PersistSavedSearch(*searchFilter); + return true; + } + return false; +} + +bool CPVRGUIActionsEPG::DeleteSavedSearch(const CFileItem& item) +{ + const auto searchFilter = item.GetEPGSearchFilter(); + + if (!searchFilter) + { + CLog::LogF(LOGERROR, "Wrong item type. No EPG search filter present."); + return false; + } + + if (CGUIDialogYesNo::ShowAndGetInput(CVariant{122}, // "Confirm delete" + CVariant{19338}, // "Delete this saved search?" + CVariant{""}, CVariant{item.GetLabel()})) + { + return CServiceBroker::GetPVRManager().EpgContainer().DeleteSavedSearch(*searchFilter); + } + return false; +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsEPG.h b/xbmc/pvr/guilib/PVRGUIActionsEPG.h new file mode 100644 index 0000000..e0a3edc --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsEPG.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" + +class CFileItem; + +namespace PVR +{ +class CPVRGUIActionsEPG : public IPVRComponent +{ +public: + CPVRGUIActionsEPG() = default; + ~CPVRGUIActionsEPG() override = default; + + /*! + * @brief Open a dialog with epg information for a given item. + * @param item containing epg data to show. item must be an epg tag, a channel or a timer. + * @return true on success, false otherwise. + */ + bool ShowEPGInfo(const CFileItem& item) const; + + /*! + * @brief Open a dialog with the epg list for a given item. + * @param item containing channel info. item must be an epg tag, a channel or a timer. + * @return true on success, false otherwise. + */ + bool ShowChannelEPG(const CFileItem& item) const; + + /*! + * @brief Open a window containing a list of epg tags 'similar' to a given item. + * @param item containing epg data for matching. item must be an epg tag, a channel or a + * recording. + * @return true on success, false otherwise. + */ + bool FindSimilar(const CFileItem& item) const; + + /*! + * @brief Execute a saved search. Displays result in search window if it is open. + * @param item The item containing a search filter. + * @return True on success, false otherwise. + */ + bool ExecuteSavedSearch(const CFileItem& item); + + /*! + * @brief Edit a saved search. Opens the search dialog. + * @param item The item containing a search filter. + * @return True on success, false otherwise. + */ + bool EditSavedSearch(const CFileItem& item); + + /*! + * @brief Rename a saved search. Opens a title input dialog. + * @param item The item containing a search filter. + * @return True on success, false otherwise. + */ + bool RenameSavedSearch(const CFileItem& item); + + /*! + * @brief Delete a saved search. Opens confirmation dialog before deleting. + * @param item The item containing a search filter. + * @return True on success, false otherwise. + */ + bool DeleteSavedSearch(const CFileItem& item); + +private: + CPVRGUIActionsEPG(const CPVRGUIActionsEPG&) = delete; + CPVRGUIActionsEPG const& operator=(CPVRGUIActionsEPG const&) = delete; +}; + +namespace GUI +{ +// pretty scope and name +using EPG = CPVRGUIActionsEPG; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp new file mode 100644 index 0000000..96947c4 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionsParentalControl.h" + +#include "ServiceBroker.h" +#include "dialogs/GUIDialogNumeric.h" +#include "guilib/LocalizeStrings.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRManager.h" +#include "pvr/channels/PVRChannel.h" +#include "settings/Settings.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <memory> +#include <string> + +using namespace PVR; +using namespace KODI::MESSAGING; + +CPVRGUIActionsParentalControl::CPVRGUIActionsParentalControl() + : m_settings({CSettings::SETTING_PVRPARENTAL_PIN, CSettings::SETTING_PVRPARENTAL_ENABLED}) +{ +} + +ParentalCheckResult CPVRGUIActionsParentalControl::CheckParentalLock( + const std::shared_ptr<CPVRChannel>& channel) const +{ + if (!CServiceBroker::GetPVRManager().IsParentalLocked(channel)) + return ParentalCheckResult::SUCCESS; + + ParentalCheckResult ret = CheckParentalPIN(); + + if (ret == ParentalCheckResult::FAILED) + CLog::LogF(LOGERROR, "Parental lock verification failed for channel '{}': wrong PIN entered.", + channel->ChannelName()); + + return ret; +} + +ParentalCheckResult CPVRGUIActionsParentalControl::CheckParentalPIN() const +{ + if (!m_settings.GetBoolValue(CSettings::SETTING_PVRPARENTAL_ENABLED)) + return ParentalCheckResult::SUCCESS; + + std::string pinCode = m_settings.GetStringValue(CSettings::SETTING_PVRPARENTAL_PIN); + if (pinCode.empty()) + return ParentalCheckResult::SUCCESS; + + InputVerificationResult ret = CGUIDialogNumeric::ShowAndVerifyInput( + pinCode, g_localizeStrings.Get(19262), true); // "Parental control. Enter PIN:" + + if (ret == InputVerificationResult::SUCCESS) + { + CServiceBroker::GetPVRManager().RestartParentalTimer(); + return ParentalCheckResult::SUCCESS; + } + else if (ret == InputVerificationResult::FAILED) + { + HELPERS::ShowOKDialogText(CVariant{19264}, + CVariant{19265}); // "Incorrect PIN", "The entered PIN was incorrect." + return ParentalCheckResult::FAILED; + } + else + { + return ParentalCheckResult::CANCELED; + } +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h new file mode 100644 index 0000000..921f597 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsParentalControl.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "pvr/settings/PVRSettings.h" + +#include <memory> + +namespace PVR +{ +enum class ParentalCheckResult +{ + CANCELED, + FAILED, + SUCCESS +}; + +class CPVRChannel; + +class CPVRGUIActionsParentalControl : public IPVRComponent +{ +public: + CPVRGUIActionsParentalControl(); + ~CPVRGUIActionsParentalControl() override = default; + + /*! + * @brief Check if channel is parental locked. Ask for PIN if necessary. + * @param channel The channel to do the check for. + * @return the result of the check (success, failed, or canceled by user). + */ + ParentalCheckResult CheckParentalLock(const std::shared_ptr<CPVRChannel>& channel) const; + + /*! + * @brief Open Numeric dialog to check for parental PIN. + * @return the result of the check (success, failed, or canceled by user). + */ + ParentalCheckResult CheckParentalPIN() const; + +private: + CPVRGUIActionsParentalControl(const CPVRGUIActionsParentalControl&) = delete; + CPVRGUIActionsParentalControl const& operator=(CPVRGUIActionsParentalControl const&) = delete; + + CPVRSettings m_settings; +}; + +namespace GUI +{ +// pretty scope and name +using Parental = CPVRGUIActionsParentalControl; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp b/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp new file mode 100644 index 0000000..66fdb80 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsPlayback.cpp @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionsPlayback.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "application/ApplicationEnums.h" +#include "cores/DataCacheCore.h" +#include "dialogs/GUIDialogContextMenu.h" +#include "dialogs/GUIDialogKaiToast.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "messaging/ApplicationMessenger.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/PVRStreamProperties.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroups.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "settings/MediaSettings.h" +#include "settings/Settings.h" +#include "utils/StringUtils.h" +#include "utils/URIUtils.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <memory> +#include <string> +#include <vector> + +using namespace PVR; +using namespace KODI::MESSAGING; + +CPVRGUIActionsPlayback::CPVRGUIActionsPlayback() + : m_settings({CSettings::SETTING_LOOKANDFEEL_STARTUPACTION, + CSettings::SETTING_PVRPLAYBACK_SWITCHTOFULLSCREENCHANNELTYPES}) +{ +} + +std::string CPVRGUIActionsPlayback::GetResumeLabel(const CFileItem& item) const +{ + std::string resumeString; + + const std::shared_ptr<CPVRRecording> recording( + CPVRItem(CFileItemPtr(new CFileItem(item))).GetRecording()); + if (recording && !recording->IsDeleted()) + { + int positionInSeconds = lrint(recording->GetResumePoint().timeInSeconds); + if (positionInSeconds > 0) + resumeString = StringUtils::Format( + g_localizeStrings.Get(12022), + StringUtils::SecondsToTimeString(positionInSeconds, TIME_FORMAT_HH_MM_SS)); + } + return resumeString; +} + +bool CPVRGUIActionsPlayback::CheckResumeRecording(const CFileItem& item) const +{ + bool bPlayIt(true); + std::string resumeString(GetResumeLabel(item)); + if (!resumeString.empty()) + { + CContextButtons choices; + choices.Add(CONTEXT_BUTTON_RESUME_ITEM, resumeString); + choices.Add(CONTEXT_BUTTON_PLAY_ITEM, 12021); // Play from beginning + int choice = CGUIDialogContextMenu::ShowAndGetChoice(choices); + if (choice > 0) + const_cast<CFileItem*>(&item)->SetStartOffset( + choice == CONTEXT_BUTTON_RESUME_ITEM ? STARTOFFSET_RESUME : 0); + else + bPlayIt = false; // context menu cancelled + } + return bPlayIt; +} + +bool CPVRGUIActionsPlayback::ResumePlayRecording(const CFileItem& item, bool bFallbackToPlay) const +{ + bool bCanResume = !GetResumeLabel(item).empty(); + if (bCanResume) + { + const_cast<CFileItem*>(&item)->SetStartOffset(STARTOFFSET_RESUME); + } + else + { + if (bFallbackToPlay) + const_cast<CFileItem*>(&item)->SetStartOffset(0); + else + return false; + } + + return PlayRecording(item, false); +} + +void CPVRGUIActionsPlayback::CheckAndSwitchToFullscreen(bool bFullscreen) const +{ + CMediaSettings::GetInstance().SetMediaStartWindowed(!bFullscreen); + + if (bFullscreen) + { + CGUIMessage msg(GUI_MSG_FULLSCREEN, 0, + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + } +} + +void CPVRGUIActionsPlayback::StartPlayback(CFileItem* item, + bool bFullscreen, + const CPVRStreamProperties* epgProps) const +{ + // Obtain dynamic playback url and properties from the respective pvr client + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item); + if (client) + { + CPVRStreamProperties props; + + if (item->IsPVRChannel()) + { + // If this was an EPG Tag to be played as live then PlayEpgTag() will create a channel + // fileitem instead and pass the epg tags props so we use those and skip the client call + if (epgProps) + props = *epgProps; + else + client->GetChannelStreamProperties(item->GetPVRChannelInfoTag(), props); + } + else if (item->IsPVRRecording()) + { + client->GetRecordingStreamProperties(item->GetPVRRecordingInfoTag(), props); + } + else if (item->IsEPG()) + { + if (epgProps) // we already have props from PlayEpgTag() + props = *epgProps; + else + client->GetEpgTagStreamProperties(item->GetEPGInfoTag(), props); + } + + if (props.size()) + { + const std::string url = props.GetStreamURL(); + if (!url.empty()) + item->SetDynPath(url); + + const std::string mime = props.GetStreamMimeType(); + if (!mime.empty()) + { + item->SetMimeType(mime); + item->SetContentLookup(false); + } + + for (const auto& prop : props) + item->SetProperty(prop.first, prop.second); + } + } + + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_PLAY, 0, 0, static_cast<void*>(item)); + CheckAndSwitchToFullscreen(bFullscreen); +} + +bool CPVRGUIActionsPlayback::PlayRecording(const CFileItem& item, bool bCheckResume) const +{ + const std::shared_ptr<CPVRRecording> recording(CPVRItem(item).GetRecording()); + if (!recording) + return false; + + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRecording(recording)) + { + CGUIMessage msg(GUI_MSG_FULLSCREEN, 0, + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + return true; + } + + if (!bCheckResume || CheckResumeRecording(item)) + { + CFileItem* itemToPlay = new CFileItem(recording); + itemToPlay->SetStartOffset(item.GetStartOffset()); + StartPlayback(itemToPlay, true); + } + return true; +} + +bool CPVRGUIActionsPlayback::PlayEpgTag(const CFileItem& item) const +{ + const std::shared_ptr<CPVREpgInfoTag> epgTag(CPVRItem(item).GetEpgInfoTag()); + if (!epgTag) + return false; + + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingEpgTag(epgTag)) + { + CGUIMessage msg(GUI_MSG_FULLSCREEN, 0, + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + return true; + } + + // Obtain dynamic playback url and properties from the respective pvr client + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(epgTag->ClientID()); + if (!client) + return false; + + CPVRStreamProperties props; + client->GetEpgTagStreamProperties(epgTag, props); + + CFileItem* itemToPlay = nullptr; + if (props.EPGPlaybackAsLive()) + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(item); + if (!groupMember) + return false; + + itemToPlay = new CFileItem(groupMember); + } + else + { + itemToPlay = new CFileItem(epgTag); + } + + StartPlayback(itemToPlay, true, &props); + return true; +} + +bool CPVRGUIActionsPlayback::SwitchToChannel(const CFileItem& item, bool bCheckResume) const +{ + if (item.m_bIsFolder) + return false; + + std::shared_ptr<CPVRRecording> recording; + const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel()); + if (channel) + { + bool bSwitchToFullscreen = + CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingChannel(channel); + + if (!bSwitchToFullscreen) + { + recording = + CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(channel->GetEPGNow()); + bSwitchToFullscreen = + recording && + CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRecording(recording); + } + + if (bSwitchToFullscreen) + { + CGUIMessage msg(GUI_MSG_FULLSCREEN, 0, + CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow()); + CServiceBroker::GetGUI()->GetWindowManager().SendMessage(msg); + return true; + } + } + + ParentalCheckResult result = + channel ? CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel) + : ParentalCheckResult::FAILED; + if (result == ParentalCheckResult::SUCCESS) + { + // switch to channel or if recording present, ask whether to switch or play recording... + if (!recording) + recording = + CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(channel->GetEPGNow()); + + if (recording) + { + bool bCancel(false); + bool bPlayRecording = CGUIDialogYesNo::ShowAndGetInput( + CVariant{19687}, // "Play recording" + CVariant{""}, CVariant{12021}, // "Play from beginning" + CVariant{recording->m_strTitle}, bCancel, CVariant{19000}, // "Switch to channel" + CVariant{19687}, // "Play recording" + 0); // no autoclose + if (bCancel) + return false; + + if (bPlayRecording) + return PlayRecording(CFileItem(recording), bCheckResume); + } + + bool bFullscreen; + switch (m_settings.GetIntValue(CSettings::SETTING_PVRPLAYBACK_SWITCHTOFULLSCREENCHANNELTYPES)) + { + case 0: // never + bFullscreen = false; + break; + case 1: // TV channels + bFullscreen = !channel->IsRadio(); + break; + case 2: // Radio channels + bFullscreen = channel->IsRadio(); + break; + case 3: // TV and radio channels + default: + bFullscreen = true; + break; + } + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(item); + if (!groupMember) + return false; + + StartPlayback(new CFileItem(groupMember), bFullscreen); + return true; + } + else if (result == ParentalCheckResult::FAILED) + { + const std::string channelName = + channel ? channel->ChannelName() : g_localizeStrings.Get(19029); // Channel + const std::string msg = StringUtils::Format( + g_localizeStrings.Get(19035), + channelName); // CHANNELNAME could not be played. Check the log for details. + + CGUIDialogKaiToast::QueueNotification(CGUIDialogKaiToast::Error, g_localizeStrings.Get(19166), + msg); // PVR information + } + + return false; +} + +bool CPVRGUIActionsPlayback::SwitchToChannel(PlaybackType type) const +{ + std::shared_ptr<CPVRChannelGroupMember> groupMember; + bool bIsRadio(false); + + // check if the desired PlaybackType is already playing, + // and if not, try to grab the last played channel of this type + switch (type) + { + case PlaybackTypeRadio: + { + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio()) + return true; + + const std::shared_ptr<CPVRChannelGroup> allGroup = + CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAllRadio(); + if (allGroup) + groupMember = allGroup->GetLastPlayedChannelGroupMember(); + + bIsRadio = true; + break; + } + case PlaybackTypeTV: + { + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV()) + return true; + + const std::shared_ptr<CPVRChannelGroup> allGroup = + CServiceBroker::GetPVRManager().ChannelGroups()->GetGroupAllTV(); + if (allGroup) + groupMember = allGroup->GetLastPlayedChannelGroupMember(); + + break; + } + default: + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlaying()) + return true; + + groupMember = + CServiceBroker::GetPVRManager().ChannelGroups()->GetLastPlayedChannelGroupMember(); + break; + } + + // if we have a last played channel, start playback + if (groupMember) + { + return SwitchToChannel(CFileItem(groupMember), true); + } + else + { + // if we don't, find the active channel group of the demanded type and play it's first channel + const std::shared_ptr<CPVRChannelGroup> channelGroup = + CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bIsRadio); + if (channelGroup) + { + // try to start playback of first channel in this group + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> groupMembers = + channelGroup->GetMembers(); + if (!groupMembers.empty()) + { + return SwitchToChannel(CFileItem(*groupMembers.begin()), true); + } + } + } + + CLog::LogF(LOGERROR, + "Could not determine {} channel to playback. No last played channel found, and " + "first channel of active group could also not be determined.", + bIsRadio ? "Radio" : "TV"); + + CGUIDialogKaiToast::QueueNotification( + CGUIDialogKaiToast::Error, + g_localizeStrings.Get(19166), // PVR information + StringUtils::Format( + g_localizeStrings.Get(19035), + g_localizeStrings.Get( + bIsRadio ? 19021 + : 19020))); // Radio/TV could not be played. Check the log for details. + return false; +} + +bool CPVRGUIActionsPlayback::PlayChannelOnStartup() const +{ + int iAction = m_settings.GetIntValue(CSettings::SETTING_LOOKANDFEEL_STARTUPACTION); + if (iAction != STARTUP_ACTION_PLAY_TV && iAction != STARTUP_ACTION_PLAY_RADIO) + return false; + + bool playRadio = (iAction == STARTUP_ACTION_PLAY_RADIO); + + // get the last played channel or fallback to first channel of all channels group + std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().PlaybackState()->GetLastPlayedChannelGroupMember(playRadio); + + if (!groupMember) + { + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().ChannelGroups()->Get(playRadio)->GetGroupAll(); + auto channels = group->GetMembers(); + if (channels.empty()) + return false; + + groupMember = channels.front(); + if (!groupMember) + return false; + } + + CLog::Log(LOGINFO, "PVR is starting playback of channel '{}'", + groupMember->Channel()->ChannelName()); + return SwitchToChannel(CFileItem(groupMember), true); +} + +bool CPVRGUIActionsPlayback::PlayMedia(const CFileItem& item) const +{ + std::unique_ptr<CFileItem> pvrItem = std::make_unique<CFileItem>(item); + if (URIUtils::IsPVRChannel(item.GetPath()) && !item.HasPVRChannelInfoTag()) + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().ChannelGroups()->GetChannelGroupMemberByPath( + item.GetPath()); + if (groupMember) + pvrItem = std::make_unique<CFileItem>(groupMember); + } + else if (URIUtils::IsPVRRecording(item.GetPath()) && !item.HasPVRRecordingInfoTag()) + { + const std::shared_ptr<CPVRRecording> recording = + CServiceBroker::GetPVRManager().Recordings()->GetByPath(item.GetPath()); + if (recording) + { + pvrItem = std::make_unique<CFileItem>(recording); + pvrItem->SetStartOffset(item.GetStartOffset()); + } + } + bool bCheckResume = true; + if (item.HasProperty("check_resume")) + bCheckResume = item.GetProperty("check_resume").asBoolean(); + + if (pvrItem && pvrItem->HasPVRChannelInfoTag()) + { + return SwitchToChannel(*pvrItem, bCheckResume); + } + else if (pvrItem && pvrItem->HasPVRRecordingInfoTag()) + { + return PlayRecording(*pvrItem, bCheckResume); + } + + return false; +} + +void CPVRGUIActionsPlayback::SeekForward() +{ + time_t playbackStartTime = CServiceBroker::GetDataCacheCore().GetStartTime(); + if (playbackStartTime > 0) + { + const std::shared_ptr<CPVRChannel> playingChannel = + CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (playingChannel) + { + time_t nextTime = 0; + std::shared_ptr<CPVREpgInfoTag> next = playingChannel->GetEPGNext(); + if (next) + { + next->StartAsUTC().GetAsTime(nextTime); + } + else + { + // if there is no next event, jump to end of currently playing event + next = playingChannel->GetEPGNow(); + if (next) + next->EndAsUTC().GetAsTime(nextTime); + } + + int64_t seekTime = 0; + if (nextTime != 0) + { + seekTime = (nextTime - playbackStartTime) * 1000; + } + else + { + // no epg; jump to end of buffer + seekTime = CServiceBroker::GetDataCacheCore().GetMaxTime(); + } + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_SEEK_TIME, seekTime); + } + } +} + +void CPVRGUIActionsPlayback::SeekBackward(unsigned int iThreshold) +{ + time_t playbackStartTime = CServiceBroker::GetDataCacheCore().GetStartTime(); + if (playbackStartTime > 0) + { + const std::shared_ptr<CPVRChannel> playingChannel = + CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (playingChannel) + { + time_t prevTime = 0; + std::shared_ptr<CPVREpgInfoTag> prev = playingChannel->GetEPGNow(); + if (prev) + { + prev->StartAsUTC().GetAsTime(prevTime); + + // if playback time of current event is above threshold jump to start of current event + int64_t playTime = CServiceBroker::GetDataCacheCore().GetPlayTime() / 1000; + if ((playbackStartTime + playTime - prevTime) <= iThreshold) + { + // jump to start of previous event + prevTime = 0; + prev = playingChannel->GetEPGPrevious(); + if (prev) + prev->StartAsUTC().GetAsTime(prevTime); + } + } + + int64_t seekTime = 0; + if (prevTime != 0) + { + seekTime = (prevTime - playbackStartTime) * 1000; + } + else + { + // no epg; jump to begin of buffer + seekTime = CServiceBroker::GetDataCacheCore().GetMinTime(); + } + CServiceBroker::GetAppMessenger()->PostMsg(TMSG_MEDIA_SEEK_TIME, seekTime); + } + } +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsPlayback.h b/xbmc/pvr/guilib/PVRGUIActionsPlayback.h new file mode 100644 index 0000000..8b0ab55 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsPlayback.h @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "pvr/settings/PVRSettings.h" + +#include <string> + +class CFileItem; + +namespace PVR +{ +enum PlaybackType +{ + PlaybackTypeAny = 0, + PlaybackTypeTV, + PlaybackTypeRadio +}; + +class CPVRStreamProperties; + +class CPVRGUIActionsPlayback : public IPVRComponent +{ +public: + CPVRGUIActionsPlayback(); + ~CPVRGUIActionsPlayback() override = default; + + /*! + * @brief Get a localized resume play label, if the given item can be resumed. + * @param item containing a recording or an epg tag. + * @return the localized resume play label that can be used for instance as context menu item + * label or an empty string if resume is not possible. + */ + std::string GetResumeLabel(const CFileItem& item) const; + + /*! + * @brief Resume a previously not completely played recording. + * @param item containing a recording or an epg tag. + * @param bFallbackToPlay controls whether playback of the recording should be started at the + * beginning ig no resume data are available. + * @return true on success, false otherwise. + */ + bool ResumePlayRecording(const CFileItem& item, bool bFallbackToPlay) const; + + /*! + * @brief Play recording. + * @param item containing a recording or an epg tag. + * @param bCheckResume controls resume check. + * @return true on success, false otherwise. + */ + bool PlayRecording(const CFileItem& item, bool bCheckResume) const; + + /*! + * @brief Play EPG tag. + * @param item containing an epg tag. + * @return true on success, false otherwise. + */ + bool PlayEpgTag(const CFileItem& item) const; + + /*! + * @brief Switch channel. + * @param item containing a channel or an epg tag. + * @param bCheckResume controls resume check in case a recording for the current epg event is + * present. + * @return true on success, false otherwise. + */ + bool SwitchToChannel(const CFileItem& item, bool bCheckResume) const; + + /*! + * @brief Playback the given file item. + * @param item containing a channel or a recording. + * @return True if the item could be played, false otherwise. + */ + bool PlayMedia(const CFileItem& item) const; + + /*! + * @brief Start playback of the last played channel, and if there is none, play first channel in + * the current channelgroup. + * @param type The type of playback to be started (any, radio, tv). See PlaybackType enum + * @return True if playback was started, false otherwise. + */ + bool SwitchToChannel(PlaybackType type) const; + + /*! + * @brief Plays the last played channel or the first channel of TV or Radio on startup. + * @return True if playback was started, false otherwise. + */ + bool PlayChannelOnStartup() const; + + /*! + * @brief Seek to the start of the next epg event in timeshift buffer, relative to the currently + * playing event. If there is no next event, seek to the end of the currently playing event (to + * the 'live' position). + */ + void SeekForward(); + + /*! + * @brief Seek to the start of the previous epg event in timeshift buffer, relative to the + * currently playing event or if there is no previous event or if playback time is greater than + * given threshold, seek to the start of the playing event. + * @param iThreshold the value in seconds to trigger seek to start of current event instead of + * start of previous event. + */ + void SeekBackward(unsigned int iThreshold); + +private: + CPVRGUIActionsPlayback(const CPVRGUIActionsPlayback&) = delete; + CPVRGUIActionsPlayback const& operator=(CPVRGUIActionsPlayback const&) = delete; + + /*! + * @brief Check whether resume play is possible for a given item, display "resume from ..."/"play + * from start" context menu in case. + * @param item containing a recording or an epg tag. + * @return true, to play/resume the item, false otherwise. + */ + bool CheckResumeRecording(const CFileItem& item) const; + + /*! + * @brief Check "play minimized" settings value and switch to fullscreen if not set. + * @param bFullscreen switch to fullscreen or set windowed playback. + */ + void CheckAndSwitchToFullscreen(bool bFullscreen) const; + + /*! + * @brief Start playback of the given item. + * @param bFullscreen start playback fullscreen or not. + * @param epgProps properties to be used instead of calling to the client if supplied. + * @param item containing a channel or a recording. + */ + void StartPlayback(CFileItem* item, + bool bFullscreen, + const CPVRStreamProperties* epgProps = nullptr) const; + + CPVRSettings m_settings; +}; + +namespace GUI +{ +// pretty scope and name +using Playback = CPVRGUIActionsPlayback; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp new file mode 100644 index 0000000..1a0a99a --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.cpp @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionsPowerManagement.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "messaging/helpers/DialogHelper.h" +#include "network/Network.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/Settings.h" +#include "utils/StringUtils.h" +#include "utils/Variant.h" + +#include <memory> +#include <string> +#include <vector> + +using namespace PVR; +using namespace KODI::MESSAGING; + +CPVRGUIActionsPowerManagement::CPVRGUIActionsPowerManagement() + : m_settings({CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME, + CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME}) +{ +} + +bool CPVRGUIActionsPowerManagement::CanSystemPowerdown(bool bAskUser /*= true*/) const +{ + bool bReturn(true); + if (CServiceBroker::GetPVRManager().IsStarted()) + { + std::shared_ptr<CPVRTimerInfoTag> cause; + if (!AllLocalBackendsIdle(cause)) + { + if (bAskUser) + { + std::string text; + + if (cause) + { + if (cause->IsRecording()) + { + text = StringUtils::Format( + g_localizeStrings.Get(19691), // "PVR is currently recording...." + cause->Title(), cause->ChannelName()); + } + else + { + // Next event is due to a local recording or reminder. + const CDateTime now(CDateTime::GetUTCDateTime()); + const CDateTime start(cause->StartAsUTC()); + const CDateTimeSpan prestart(0, 0, cause->MarginStart(), 0); + + CDateTimeSpan diff(start - now); + diff -= prestart; + int mins = diff.GetSecondsTotal() / 60; + + std::string dueStr; + if (mins > 1) + { + // "%d minutes" + dueStr = StringUtils::Format(g_localizeStrings.Get(19694), mins); + } + else + { + // "about a minute" + dueStr = g_localizeStrings.Get(19695); + } + + text = StringUtils::Format( + cause->IsReminder() + ? g_localizeStrings.Get(19690) // "PVR has scheduled a reminder...." + : g_localizeStrings.Get(19692), // "PVR will start recording...." + cause->Title(), cause->ChannelName(), dueStr); + } + } + else + { + // Next event is due to automatic daily wakeup of PVR. + const CDateTime now(CDateTime::GetUTCDateTime()); + + CDateTime dailywakeuptime; + dailywakeuptime.SetFromDBTime( + m_settings.GetStringValue(CSettings::SETTING_PVRPOWERMANAGEMENT_DAILYWAKEUPTIME)); + dailywakeuptime = dailywakeuptime.GetAsUTCDateTime(); + + const CDateTimeSpan diff(dailywakeuptime - now); + int mins = diff.GetSecondsTotal() / 60; + + std::string dueStr; + if (mins > 1) + { + // "%d minutes" + dueStr = StringUtils::Format(g_localizeStrings.Get(19694), mins); + } + else + { + // "about a minute" + dueStr = g_localizeStrings.Get(19695); + } + + text = StringUtils::Format(g_localizeStrings.Get(19693), // "Daily wakeup is due in...." + dueStr); + } + + // Inform user about PVR being busy. Ask if user wants to powerdown anyway. + bReturn = HELPERS::ShowYesNoDialogText(CVariant{19685}, // "Confirm shutdown" + CVariant{text}, CVariant{222}, // "Shutdown anyway", + CVariant{19696}, // "Cancel" + 10000) // timeout value before closing + == HELPERS::DialogResponse::CHOICE_YES; + } + else + bReturn = false; // do not powerdown (busy, but no user interaction requested). + } + } + return bReturn; +} + +bool CPVRGUIActionsPowerManagement::AllLocalBackendsIdle( + std::shared_ptr<CPVRTimerInfoTag>& causingEvent) const +{ + // active recording on local backend? + const std::vector<std::shared_ptr<CPVRTimerInfoTag>> activeRecordings = + CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings(); + for (const auto& timer : activeRecordings) + { + if (EventOccursOnLocalBackend(timer)) + { + causingEvent = timer; + return false; + } + } + + // soon recording on local backend? + if (IsNextEventWithinBackendIdleTime()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = + CServiceBroker::GetPVRManager().Timers()->GetNextActiveTimer(false); + if (!timer) + { + // Next event is due to automatic daily wakeup of PVR! + causingEvent.reset(); + return false; + } + + if (EventOccursOnLocalBackend(timer)) + { + causingEvent = timer; + return false; + } + } + return true; +} + +bool CPVRGUIActionsPowerManagement::EventOccursOnLocalBackend( + const std::shared_ptr<CPVRTimerInfoTag>& event) const +{ + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(CFileItem(event)); + if (client) + { + const std::string hostname = client->GetBackendHostname(); + if (!hostname.empty() && CServiceBroker::GetNetwork().IsLocalHost(hostname)) + return true; + } + return false; +} + +bool CPVRGUIActionsPowerManagement::IsNextEventWithinBackendIdleTime() const +{ + // timers going off soon? + const CDateTime now(CDateTime::GetUTCDateTime()); + const CDateTimeSpan idle( + 0, 0, m_settings.GetIntValue(CSettings::SETTING_PVRPOWERMANAGEMENT_BACKENDIDLETIME), 0); + const CDateTime next(CServiceBroker::GetPVRManager().Timers()->GetNextEventTime()); + const CDateTimeSpan delta(next - now); + + return (delta <= idle); +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h new file mode 100644 index 0000000..bf99819 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsPowerManagement.h @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "pvr/settings/PVRSettings.h" + +#include <memory> + +class CFileItem; + +namespace PVR +{ +class CPVRTimerInfoTag; + +class CPVRGUIActionsPowerManagement : public IPVRComponent +{ +public: + CPVRGUIActionsPowerManagement(); + ~CPVRGUIActionsPowerManagement() override = default; + + /*! + * @brief Check whether the system Kodi is running on can be powered down + * (shutdown/reboot/suspend/hibernate) without stopping any active recordings and/or without + * preventing the start of recordings scheduled for now + pvrpowermanagement.backendidletime. + * @param bAskUser True to informs user in case of potential data loss. User can decide to allow + * powerdown anyway. False to not to ask user and to not confirm power down. + * @return True if system can be safely powered down, false otherwise. + */ + bool CanSystemPowerdown(bool bAskUser = true) const; + +private: + CPVRGUIActionsPowerManagement(const CPVRGUIActionsPowerManagement&) = delete; + CPVRGUIActionsPowerManagement const& operator=(CPVRGUIActionsPowerManagement const&) = delete; + + bool AllLocalBackendsIdle(std::shared_ptr<CPVRTimerInfoTag>& causingEvent) const; + bool EventOccursOnLocalBackend(const std::shared_ptr<CPVRTimerInfoTag>& event) const; + bool IsNextEventWithinBackendIdleTime() const; + + CPVRSettings m_settings; +}; + +namespace GUI +{ +// pretty scope and name +using PowerManagement = CPVRGUIActionsPowerManagement; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp b/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp new file mode 100644 index 0000000..df23f38 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsRecordings.cpp @@ -0,0 +1,361 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionsRecordings.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "dialogs/GUIDialogBusy.h" +#include "dialogs/GUIDialogYesNo.h" +#include "filesystem/IDirectory.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/dialogs/GUIDialogPVRRecordingInfo.h" +#include "pvr/dialogs/GUIDialogPVRRecordingSettings.h" +#include "pvr/recordings/PVRRecording.h" +#include "settings/Settings.h" +#include "threads/IRunnable.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <memory> +#include <numeric> +#include <string> + +using namespace PVR; +using namespace KODI::MESSAGING; + +namespace +{ +class AsyncRecordingAction : private IRunnable +{ +public: + bool Execute(const CFileItem& item); + +protected: + AsyncRecordingAction() = default; + +private: + // IRunnable implementation + void Run() override; + + // the worker function + virtual bool DoRun(const std::shared_ptr<CFileItem>& item) = 0; + + std::shared_ptr<CFileItem> m_item; + bool m_bSuccess = false; +}; + +bool AsyncRecordingAction::Execute(const CFileItem& item) +{ + m_item = std::make_shared<CFileItem>(item); + CGUIDialogBusy::Wait(this, 100, false); + return m_bSuccess; +} + +void AsyncRecordingAction::Run() +{ + m_bSuccess = DoRun(m_item); + + if (m_bSuccess) + CServiceBroker::GetPVRManager().TriggerRecordingsUpdate(); +} + +class AsyncRenameRecording : public AsyncRecordingAction +{ +public: + explicit AsyncRenameRecording(const std::string& strNewName) : m_strNewName(strNewName) {} + +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + if (item->IsUsablePVRRecording()) + { + return item->GetPVRRecordingInfoTag()->Rename(m_strNewName); + } + else + { + CLog::LogF(LOGERROR, "Cannot rename item '{}': no valid recording tag", item->GetPath()); + return false; + } + } + std::string m_strNewName; +}; + +class AsyncDeleteRecording : public AsyncRecordingAction +{ +public: + explicit AsyncDeleteRecording(bool bWatchedOnly = false) : m_bWatchedOnly(bWatchedOnly) {} + +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + CFileItemList items; + if (item->m_bIsFolder) + { + CUtil::GetRecursiveListing(item->GetPath(), items, "", XFILE::DIR_FLAG_NO_FILE_INFO); + } + else + { + items.Add(item); + } + + return std::accumulate( + items.cbegin(), items.cend(), true, [this](bool success, const auto& itemToDelete) { + return (itemToDelete->IsPVRRecording() && + (!m_bWatchedOnly || itemToDelete->GetPVRRecordingInfoTag()->GetPlayCount() > 0) && + !itemToDelete->GetPVRRecordingInfoTag()->Delete()) + ? false + : success; + }); + } + bool m_bWatchedOnly = false; +}; + +class AsyncEmptyRecordingsTrash : public AsyncRecordingAction +{ +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + return CServiceBroker::GetPVRManager().Clients()->DeleteAllRecordingsFromTrash() == + PVR_ERROR_NO_ERROR; + } +}; + +class AsyncUndeleteRecording : public AsyncRecordingAction +{ +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + if (item->IsDeletedPVRRecording()) + { + return item->GetPVRRecordingInfoTag()->Undelete(); + } + else + { + CLog::LogF(LOGERROR, "Cannot undelete item '{}': no valid recording tag", item->GetPath()); + return false; + } + } +}; + +class AsyncSetRecordingPlayCount : public AsyncRecordingAction +{ +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item); + if (client) + { + const std::shared_ptr<CPVRRecording> recording = item->GetPVRRecordingInfoTag(); + return client->SetRecordingPlayCount(*recording, recording->GetLocalPlayCount()) == + PVR_ERROR_NO_ERROR; + } + return false; + } +}; + +class AsyncSetRecordingLifetime : public AsyncRecordingAction +{ +private: + bool DoRun(const std::shared_ptr<CFileItem>& item) override + { + const std::shared_ptr<CPVRClient> client = CServiceBroker::GetPVRManager().GetClient(*item); + if (client) + return client->SetRecordingLifetime(*item->GetPVRRecordingInfoTag()) == PVR_ERROR_NO_ERROR; + return false; + } +}; + +} // unnamed namespace + +bool CPVRGUIActionsRecordings::ShowRecordingInfo(const CFileItem& item) const +{ + if (!item.IsPVRRecording()) + { + CLog::LogF(LOGERROR, "No recording!"); + return false; + } + + CGUIDialogPVRRecordingInfo* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRRecordingInfo>( + WINDOW_DIALOG_PVR_RECORDING_INFO); + if (!pDlgInfo) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_RECORDING_INFO!"); + return false; + } + + pDlgInfo->SetRecording(item); + pDlgInfo->Open(); + return true; +} + +bool CPVRGUIActionsRecordings::EditRecording(const CFileItem& item) const +{ + const std::shared_ptr<CPVRRecording> recording = CPVRItem(item).GetRecording(); + if (!recording) + { + CLog::LogF(LOGERROR, "No recording!"); + return false; + } + + std::shared_ptr<CPVRRecording> origRecording(new CPVRRecording); + origRecording->Update(*recording, + *CServiceBroker::GetPVRManager().GetClient(recording->ClientID())); + + if (!ShowRecordingSettings(recording)) + return false; + + if (origRecording->m_strTitle != recording->m_strTitle) + { + if (!AsyncRenameRecording(recording->m_strTitle).Execute(item)) + CLog::LogF(LOGERROR, "Renaming recording failed!"); + } + + if (origRecording->GetLocalPlayCount() != recording->GetLocalPlayCount()) + { + if (!AsyncSetRecordingPlayCount().Execute(item)) + CLog::LogF(LOGERROR, "Setting recording playcount failed!"); + } + + if (origRecording->LifeTime() != recording->LifeTime()) + { + if (!AsyncSetRecordingLifetime().Execute(item)) + CLog::LogF(LOGERROR, "Setting recording lifetime failed!"); + } + + return true; +} + +bool CPVRGUIActionsRecordings::CanEditRecording(const CFileItem& item) const +{ + return CGUIDialogPVRRecordingSettings::CanEditRecording(item); +} + +bool CPVRGUIActionsRecordings::DeleteRecording(const CFileItem& item) const +{ + if ((!item.IsPVRRecording() && !item.m_bIsFolder) || item.IsParentFolder()) + return false; + + if (!ConfirmDeleteRecording(item)) + return false; + + if (!AsyncDeleteRecording().Execute(item)) + { + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19111}); // "Error", "PVR backend error. Check the log for more information about this message." + return false; + } + + return true; +} + +bool CPVRGUIActionsRecordings::ConfirmDeleteRecording(const CFileItem& item) const +{ + return CGUIDialogYesNo::ShowAndGetInput( + CVariant{122}, // "Confirm delete" + item.m_bIsFolder + ? CVariant{19113} // "Delete all recordings in this folder?" + : item.GetPVRRecordingInfoTag()->IsDeleted() + ? CVariant{19294} + // "Remove this deleted recording from trash? This operation cannot be reverted." + : CVariant{19112}, // "Delete this recording?" + CVariant{""}, CVariant{item.GetLabel()}); +} + +bool CPVRGUIActionsRecordings::DeleteWatchedRecordings(const CFileItem& item) const +{ + if (!item.m_bIsFolder || item.IsParentFolder()) + return false; + + if (!ConfirmDeleteWatchedRecordings(item)) + return false; + + if (!AsyncDeleteRecording(true).Execute(item)) + { + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19111}); // "Error", "PVR backend error. Check the log for more information about this message." + return false; + } + + return true; +} + +bool CPVRGUIActionsRecordings::ConfirmDeleteWatchedRecordings(const CFileItem& item) const +{ + return CGUIDialogYesNo::ShowAndGetInput( + CVariant{122}, // "Confirm delete" + CVariant{19328}, // "Delete all watched recordings in this folder?" + CVariant{""}, CVariant{item.GetLabel()}); +} + +bool CPVRGUIActionsRecordings::DeleteAllRecordingsFromTrash() const +{ + if (!ConfirmDeleteAllRecordingsFromTrash()) + return false; + + if (!AsyncEmptyRecordingsTrash().Execute({})) + return false; + + return true; +} + +bool CPVRGUIActionsRecordings::ConfirmDeleteAllRecordingsFromTrash() const +{ + return CGUIDialogYesNo::ShowAndGetInput( + CVariant{19292}, // "Delete all permanently" + CVariant{ + 19293}); // "Remove all deleted recordings from trash? This operation cannot be reverted." +} + +bool CPVRGUIActionsRecordings::UndeleteRecording(const CFileItem& item) const +{ + if (!item.IsDeletedPVRRecording()) + return false; + + if (!AsyncUndeleteRecording().Execute(item)) + { + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19111}); // "Error", "PVR backend error. Check the log for more information about this message." + return false; + } + + return true; +} + +bool CPVRGUIActionsRecordings::ShowRecordingSettings( + const std::shared_ptr<CPVRRecording>& recording) const +{ + CGUIDialogPVRRecordingSettings* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRRecordingSettings>( + WINDOW_DIALOG_PVR_RECORDING_SETTING); + if (!pDlgInfo) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_RECORDING_SETTING!"); + return false; + } + + pDlgInfo->SetRecording(recording); + pDlgInfo->Open(); + + return pDlgInfo->IsConfirmed(); +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsRecordings.h b/xbmc/pvr/guilib/PVRGUIActionsRecordings.h new file mode 100644 index 0000000..795a2c7 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsRecordings.h @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" + +#include <memory> + +class CFileItem; + +namespace PVR +{ +class CPVRRecording; + +class CPVRGUIActionsRecordings : public IPVRComponent +{ +public: + CPVRGUIActionsRecordings() = default; + ~CPVRGUIActionsRecordings() override = default; + + /*! + * @brief Open a dialog with information for a given recording. + * @param item containing a recording. + * @return true on success, false otherwise. + */ + bool ShowRecordingInfo(const CFileItem& item) const; + + /*! + * @brief Open the recording settings dialog to edit a recording. + * @param item containing the recording to edit. + * @return true on success, false otherwise. + */ + bool EditRecording(const CFileItem& item) const; + + /*! + * @brief Check if any recording settings can be edited. + * @param item containing the recording to edit. + * @return true on success, false otherwise. + */ + bool CanEditRecording(const CFileItem& item) const; + + /*! + * @brief Delete a recording, always showing a confirmation dialog. + * @param item containing a recording to delete. + * @return true, if the recording was deleted successfully, false otherwise. + */ + bool DeleteRecording(const CFileItem& item) const; + + /*! + * @brief Delete all watched recordings contained in the given folder, always showing a + * confirmation dialog. + * @param item containing a recording folder containing the items to delete. + * @return true, if the recordings were deleted successfully, false otherwise. + */ + bool DeleteWatchedRecordings(const CFileItem& item) const; + + /*! + * @brief Delete all recordings from trash, always showing a confirmation dialog. + * @return true, if the recordings were permanently deleted successfully, false otherwise. + */ + bool DeleteAllRecordingsFromTrash() const; + + /*! + * @brief Undelete a recording. + * @param item containing a recording to undelete. + * @return true, if the recording was undeleted successfully, false otherwise. + */ + bool UndeleteRecording(const CFileItem& item) const; + +private: + CPVRGUIActionsRecordings(const CPVRGUIActionsRecordings&) = delete; + CPVRGUIActionsRecordings const& operator=(CPVRGUIActionsRecordings const&) = delete; + + /*! + * @brief Open a dialog to confirm to delete a recording. + * @param item the recording to delete. + * @return true, to proceed with delete, false otherwise. + */ + bool ConfirmDeleteRecording(const CFileItem& item) const; + + /*! + * @brief Open a dialog to confirm delete all watched recordings contained in the given folder. + * @param item containing a recording folder containing the items to delete. + * @return true, to proceed with delete, false otherwise. + */ + bool ConfirmDeleteWatchedRecordings(const CFileItem& item) const; + + /*! + * @brief Open a dialog to confirm to permanently remove all deleted recordings. + * @return true, to proceed with delete, false otherwise. + */ + bool ConfirmDeleteAllRecordingsFromTrash() const; + + /*! + * @brief Open the recording settings dialog. + * @param recording containing the recording the settings shall be displayed for. + * @return true, if the dialog was ended successfully, false otherwise. + */ + bool ShowRecordingSettings(const std::shared_ptr<CPVRRecording>& recording) const; +}; + +namespace GUI +{ +// pretty scope and name +using Recordings = CPVRGUIActionsRecordings; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp b/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp new file mode 100644 index 0000000..be8e313 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsTimers.cpp @@ -0,0 +1,1009 @@ +/* + * Copyright (C) 2016-2022 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionsTimers.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "dialogs/GUIDialogProgress.h" +#include "dialogs/GUIDialogSelect.h" +#include "dialogs/GUIDialogYesNo.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/WindowIDs.h" +#include "messaging/helpers/DialogHelper.h" +#include "messaging/helpers/DialogOKHelper.h" +#include "pvr/PVREventLogJob.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/dialogs/GUIDialogPVRTimerSettings.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/guilib/PVRGUIActionsParentalControl.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/Settings.h" +#include "utils/StringUtils.h" +#include "utils/SystemInfo.h" +#include "utils/Variant.h" +#include "utils/log.h" + +#include <algorithm> +#include <map> +#include <memory> +#include <string> +#include <thread> +#include <utility> + +using namespace PVR; +using namespace KODI::MESSAGING; + +CPVRGUIActionsTimers::CPVRGUIActionsTimers() + : m_settings({CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME, + CSettings::SETTING_PVRRECORD_INSTANTRECORDACTION, + CSettings::SETTING_PVRREMINDERS_AUTOCLOSEDELAY, + CSettings::SETTING_PVRREMINDERS_AUTORECORD, + CSettings::SETTING_PVRREMINDERS_AUTOSWITCH}) +{ +} + +bool CPVRGUIActionsTimers::ShowTimerSettings(const std::shared_ptr<CPVRTimerInfoTag>& timer) const +{ + CGUIDialogPVRTimerSettings* pDlgInfo = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogPVRTimerSettings>( + WINDOW_DIALOG_PVR_TIMER_SETTING); + if (!pDlgInfo) + { + CLog::LogF(LOGERROR, "Unable to get WINDOW_DIALOG_PVR_TIMER_SETTING!"); + return false; + } + + pDlgInfo->SetTimer(timer); + pDlgInfo->Open(); + + return pDlgInfo->IsConfirmed(); +} + +bool CPVRGUIActionsTimers::AddReminder(const CFileItem& item) const +{ + const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + if (!epgTag) + { + CLog::LogF(LOGERROR, "No epg tag!"); + return false; + } + + if (CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag)) + { + HELPERS::ShowOKDialogText(CVariant{19033}, // "Information" + CVariant{19034}); // "There is already a timer set for this event" + return false; + } + + const std::shared_ptr<CPVRTimerInfoTag> newTimer = + CPVRTimerInfoTag::CreateReminderFromEpg(epgTag); + if (!newTimer) + { + HELPERS::ShowOKDialogText(CVariant{19033}, // "Information" + CVariant{19094}); // Timer creation failed. Unsupported timer type. + return false; + } + + return AddTimer(newTimer); +} + +bool CPVRGUIActionsTimers::AddTimer(bool bRadio) const +{ + const std::shared_ptr<CPVRTimerInfoTag> newTimer(new CPVRTimerInfoTag(bRadio)); + if (ShowTimerSettings(newTimer)) + { + return AddTimer(newTimer); + } + return false; +} + +bool CPVRGUIActionsTimers::AddTimer(const CFileItem& item, bool bShowTimerSettings) const +{ + return AddTimer(item, false, bShowTimerSettings, false); +} + +bool CPVRGUIActionsTimers::AddTimerRule(const CFileItem& item, + bool bShowTimerSettings, + bool bFallbackToOneShotTimer) const +{ + return AddTimer(item, true, bShowTimerSettings, bFallbackToOneShotTimer); +} + +bool CPVRGUIActionsTimers::AddTimer(const CFileItem& item, + bool bCreateRule, + bool bShowTimerSettings, + bool bFallbackToOneShotTimer) const +{ + const std::shared_ptr<CPVRChannel> channel(CPVRItem(item).GetChannel()); + if (!channel) + { + CLog::LogF(LOGERROR, "No channel!"); + return false; + } + + if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel) != + ParentalCheckResult::SUCCESS) + return false; + + std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + if (epgTag) + { + if (epgTag->IsGapTag()) + epgTag.reset(); // for gap tags, we can only create instant timers + } + else if (bCreateRule) + { + CLog::LogF(LOGERROR, "No epg tag!"); + return false; + } + + std::shared_ptr<CPVRTimerInfoTag> timer( + bCreateRule || !epgTag ? nullptr + : CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag)); + std::shared_ptr<CPVRTimerInfoTag> rule( + bCreateRule ? CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer) : nullptr); + if (timer || rule) + { + HELPERS::ShowOKDialogText( + CVariant{19033}, + CVariant{19034}); // "Information", "There is already a timer set for this event" + return false; + } + + std::shared_ptr<CPVRTimerInfoTag> newTimer( + epgTag ? CPVRTimerInfoTag::CreateFromEpg(epgTag, bCreateRule) + : CPVRTimerInfoTag::CreateInstantTimerTag(channel)); + if (!newTimer) + { + if (bCreateRule && bFallbackToOneShotTimer) + newTimer = CPVRTimerInfoTag::CreateFromEpg(epgTag, false); + + if (!newTimer) + { + HELPERS::ShowOKDialogText( + CVariant{19033}, // "Information" + bCreateRule ? CVariant{19095} // Timer rule creation failed. Unsupported timer type. + : CVariant{19094}); // Timer creation failed. Unsupported timer type. + return false; + } + } + + if (bShowTimerSettings) + { + if (!ShowTimerSettings(newTimer)) + return false; + } + + return AddTimer(newTimer); +} + +bool CPVRGUIActionsTimers::AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& item) const +{ + if (!item->Channel() && !item->GetTimerType()->IsEpgBasedTimerRule()) + { + CLog::LogF(LOGERROR, "No channel given"); + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19109}); // "Error", "Could not save the timer. Check the log for more information about this message." + return false; + } + + if (!item->IsTimerRule() && item->GetEpgInfoTag() && !item->GetEpgInfoTag()->IsRecordable()) + { + HELPERS::ShowOKDialogText( + CVariant{19033}, + CVariant{19189}); // "Information", "The PVR backend does not allow to record this event." + return false; + } + + if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock( + item->Channel()) != ParentalCheckResult::SUCCESS) + return false; + + if (!CServiceBroker::GetPVRManager().Timers()->AddTimer(item)) + { + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19109}); // "Error", "Could not save the timer. Check the log for more information about this message." + return false; + } + + return true; +} + +namespace +{ +enum PVRRECORD_INSTANTRECORDACTION +{ + NONE = -1, + RECORD_CURRENT_SHOW = 0, + RECORD_INSTANTRECORDTIME = 1, + ASK = 2, + RECORD_30_MINUTES = 3, + RECORD_60_MINUTES = 4, + RECORD_120_MINUTES = 5, + RECORD_NEXT_SHOW = 6 +}; + +class InstantRecordingActionSelector +{ +public: + explicit InstantRecordingActionSelector(int iInstantRecordTime); + virtual ~InstantRecordingActionSelector() = default; + + void AddAction(PVRRECORD_INSTANTRECORDACTION eAction, const std::string& title); + void PreSelectAction(PVRRECORD_INSTANTRECORDACTION eAction); + PVRRECORD_INSTANTRECORDACTION Select(); + +private: + int m_iInstantRecordTime; + CGUIDialogSelect* m_pDlgSelect; // not owner! + std::map<PVRRECORD_INSTANTRECORDACTION, int> m_actions; +}; + +InstantRecordingActionSelector::InstantRecordingActionSelector(int iInstantRecordTime) + : m_iInstantRecordTime(iInstantRecordTime), + m_pDlgSelect(CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogSelect>( + WINDOW_DIALOG_SELECT)) +{ + if (m_pDlgSelect) + { + m_pDlgSelect->Reset(); + m_pDlgSelect->SetMultiSelection(false); + m_pDlgSelect->SetHeading(CVariant{19086}); // Instant recording action + } + else + { + CLog::LogF(LOGERROR, "Unable to obtain WINDOW_DIALOG_SELECT instance"); + } +} + +void InstantRecordingActionSelector::AddAction(PVRRECORD_INSTANTRECORDACTION eAction, + const std::string& title) +{ + if (m_actions.find(eAction) == m_actions.end()) + { + switch (eAction) + { + case RECORD_INSTANTRECORDTIME: + m_pDlgSelect->Add( + StringUtils::Format(g_localizeStrings.Get(19090), + m_iInstantRecordTime)); // Record next <default duration> minutes + break; + case RECORD_30_MINUTES: + m_pDlgSelect->Add( + StringUtils::Format(g_localizeStrings.Get(19090), 30)); // Record next 30 minutes + break; + case RECORD_60_MINUTES: + m_pDlgSelect->Add( + StringUtils::Format(g_localizeStrings.Get(19090), 60)); // Record next 60 minutes + break; + case RECORD_120_MINUTES: + m_pDlgSelect->Add( + StringUtils::Format(g_localizeStrings.Get(19090), 120)); // Record next 120 minutes + break; + case RECORD_CURRENT_SHOW: + m_pDlgSelect->Add(StringUtils::Format(g_localizeStrings.Get(19091), + title)); // Record current show (<title>) + break; + case RECORD_NEXT_SHOW: + m_pDlgSelect->Add(StringUtils::Format(g_localizeStrings.Get(19092), + title)); // Record next show (<title>) + break; + case NONE: + case ASK: + default: + return; + } + + m_actions.insert(std::make_pair(eAction, static_cast<int>(m_actions.size()))); + } +} + +void InstantRecordingActionSelector::PreSelectAction(PVRRECORD_INSTANTRECORDACTION eAction) +{ + const auto& it = m_actions.find(eAction); + if (it != m_actions.end()) + m_pDlgSelect->SetSelected(it->second); +} + +PVRRECORD_INSTANTRECORDACTION InstantRecordingActionSelector::Select() +{ + PVRRECORD_INSTANTRECORDACTION eAction = NONE; + + m_pDlgSelect->Open(); + + if (m_pDlgSelect->IsConfirmed()) + { + int iSelection = m_pDlgSelect->GetSelectedItem(); + const auto it = + std::find_if(m_actions.cbegin(), m_actions.cend(), + [iSelection](const auto& action) { return action.second == iSelection; }); + + if (it != m_actions.cend()) + eAction = (*it).first; + } + + return eAction; +} + +} // unnamed namespace + +bool CPVRGUIActionsTimers::ToggleRecordingOnPlayingChannel() +{ + const std::shared_ptr<CPVRChannel> channel = + CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (channel && channel->CanRecord()) + return SetRecordingOnChannel( + channel, !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel)); + + return false; +} + +bool CPVRGUIActionsTimers::SetRecordingOnChannel(const std::shared_ptr<CPVRChannel>& channel, + bool bOnOff) +{ + bool bReturn = false; + + if (!channel) + return bReturn; + + if (CServiceBroker::GetPVRManager().Get<PVR::GUI::Parental>().CheckParentalLock(channel) != + ParentalCheckResult::SUCCESS) + return bReturn; + + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(channel->ClientID()); + if (client && client->GetClientCapabilities().SupportsTimers()) + { + /* timers are supported on this channel */ + if (bOnOff && !CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel)) + { + std::shared_ptr<CPVREpgInfoTag> epgTag; + int iDuration = m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME); + + int iAction = m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDACTION); + switch (iAction) + { + case RECORD_CURRENT_SHOW: + epgTag = channel->GetEPGNow(); + break; + + case RECORD_INSTANTRECORDTIME: + epgTag.reset(); + break; + + case ASK: + { + PVRRECORD_INSTANTRECORDACTION ePreselect = RECORD_INSTANTRECORDTIME; + const int iDurationDefault = + m_settings.GetIntValue(CSettings::SETTING_PVRRECORD_INSTANTRECORDTIME); + InstantRecordingActionSelector selector(iDurationDefault); + std::shared_ptr<CPVREpgInfoTag> epgTagNext; + + // fixed length recordings + selector.AddAction(RECORD_30_MINUTES, ""); + selector.AddAction(RECORD_60_MINUTES, ""); + selector.AddAction(RECORD_120_MINUTES, ""); + + if (iDurationDefault != 30 && iDurationDefault != 60 && iDurationDefault != 120) + selector.AddAction(RECORD_INSTANTRECORDTIME, ""); + + // epg-based recordings + epgTag = channel->GetEPGNow(); + if (epgTag) + { + bool bLocked = CServiceBroker::GetPVRManager().IsParentalLocked(epgTag); + + // "now" + const std::string currentTitle = + bLocked ? g_localizeStrings.Get(19266) /* Parental locked */ : epgTag->Title(); + selector.AddAction(RECORD_CURRENT_SHOW, currentTitle); + ePreselect = RECORD_CURRENT_SHOW; + + // "next" + epgTagNext = channel->GetEPGNext(); + if (epgTagNext) + { + const std::string nextTitle = bLocked + ? g_localizeStrings.Get(19266) /* Parental locked */ + : epgTagNext->Title(); + selector.AddAction(RECORD_NEXT_SHOW, nextTitle); + + // be smart. if current show is almost over, preselect next show. + if (epgTag->ProgressPercentage() > 90.0f) + ePreselect = RECORD_NEXT_SHOW; + } + } + + if (ePreselect == RECORD_INSTANTRECORDTIME) + { + if (iDurationDefault == 30) + ePreselect = RECORD_30_MINUTES; + else if (iDurationDefault == 60) + ePreselect = RECORD_60_MINUTES; + else if (iDurationDefault == 120) + ePreselect = RECORD_120_MINUTES; + } + + selector.PreSelectAction(ePreselect); + + PVRRECORD_INSTANTRECORDACTION eSelected = selector.Select(); + switch (eSelected) + { + case NONE: + return false; // dialog canceled + + case RECORD_30_MINUTES: + iDuration = 30; + epgTag.reset(); + break; + + case RECORD_60_MINUTES: + iDuration = 60; + epgTag.reset(); + break; + + case RECORD_120_MINUTES: + iDuration = 120; + epgTag.reset(); + break; + + case RECORD_INSTANTRECORDTIME: + iDuration = iDurationDefault; + epgTag.reset(); + break; + + case RECORD_CURRENT_SHOW: + break; + + case RECORD_NEXT_SHOW: + epgTag = epgTagNext; + break; + + default: + CLog::LogF(LOGERROR, + "Unknown instant record action selection ({}), defaulting to fixed " + "length recording.", + static_cast<int>(eSelected)); + epgTag.reset(); + break; + } + break; + } + + default: + CLog::LogF(LOGERROR, + "Unknown instant record action setting value ({}), defaulting to fixed " + "length recording.", + iAction); + break; + } + + const std::shared_ptr<CPVRTimerInfoTag> newTimer( + epgTag ? CPVRTimerInfoTag::CreateFromEpg(epgTag, false) + : CPVRTimerInfoTag::CreateInstantTimerTag(channel, iDuration)); + + if (newTimer) + bReturn = CServiceBroker::GetPVRManager().Timers()->AddTimer(newTimer); + + if (!bReturn) + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19164}); // "Error", "Could not start recording. Check the log for more information about this message." + } + else if (!bOnOff && CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel(*channel)) + { + /* delete active timers */ + bReturn = + CServiceBroker::GetPVRManager().Timers()->DeleteTimersOnChannel(channel, true, true); + + if (!bReturn) + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19170}); // "Error", "Could not stop recording. Check the log for more information about this message." + } + } + + return bReturn; +} + +bool CPVRGUIActionsTimers::ToggleTimer(const CFileItem& item) const +{ + if (!item.HasEPGInfoTag()) + return false; + + const std::shared_ptr<CPVRTimerInfoTag> timer(CPVRItem(item).GetTimerInfoTag()); + if (timer) + { + if (timer->IsRecording()) + return StopRecording(item); + else + return DeleteTimer(item); + } + else + return AddTimer(item, false); +} + +bool CPVRGUIActionsTimers::ToggleTimerState(const CFileItem& item) const +{ + if (!item.HasPVRTimerInfoTag()) + return false; + + const std::shared_ptr<CPVRTimerInfoTag> timer = item.GetPVRTimerInfoTag(); + if (timer->IsDisabled()) + timer->SetState(PVR_TIMER_STATE_SCHEDULED); + else + timer->SetState(PVR_TIMER_STATE_DISABLED); + + if (CServiceBroker::GetPVRManager().Timers()->UpdateTimer(timer)) + return true; + + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19263}); // "Error", "Could not update the timer. Check the log for more information about this message." + return false; +} + +bool CPVRGUIActionsTimers::EditTimer(const CFileItem& item) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer(CPVRItem(item).GetTimerInfoTag()); + if (!timer) + { + CLog::LogF(LOGERROR, "No timer!"); + return false; + } + + // clone the timer. + const std::shared_ptr<CPVRTimerInfoTag> newTimer(new CPVRTimerInfoTag); + newTimer->UpdateEntry(timer); + + if (ShowTimerSettings(newTimer) && + (!timer->GetTimerType()->IsReadOnly() || timer->GetTimerType()->SupportsEnableDisable())) + { + if (newTimer->GetTimerType() == timer->GetTimerType()) + { + if (CServiceBroker::GetPVRManager().Timers()->UpdateTimer(newTimer)) + return true; + + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19263}); // "Error", "Could not update the timer. Check the log for more information about this message." + return false; + } + else + { + // timer type changed. delete the original timer, then create the new timer. this order is + // important. for instance, the new timer might be a rule which schedules the original timer. + // deleting the original timer after creating the rule would do literally this and we would + // end up with one timer missing wrt to the rule defined by the new timer. + if (DeleteTimer(timer, timer->IsRecording(), false)) + { + if (AddTimer(newTimer)) + return true; + + // rollback. + return AddTimer(timer); + } + } + } + return false; +} + +bool CPVRGUIActionsTimers::EditTimerRule(const CFileItem& item) const +{ + const std::shared_ptr<CFileItem> parentTimer = GetTimerRule(item); + if (parentTimer) + return EditTimer(*parentTimer); + + return false; +} + +std::shared_ptr<CFileItem> CPVRGUIActionsTimers::GetTimerRule(const CFileItem& item) const +{ + std::shared_ptr<CPVRTimerInfoTag> timer; + if (item.HasEPGInfoTag()) + timer = CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(item.GetEPGInfoTag()); + else if (item.HasPVRTimerInfoTag()) + timer = item.GetPVRTimerInfoTag(); + + if (timer) + { + timer = CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer); + if (timer) + return std::make_shared<CFileItem>(timer); + } + return {}; +} + +bool CPVRGUIActionsTimers::DeleteTimer(const CFileItem& item) const +{ + return DeleteTimer(item, false, false); +} + +bool CPVRGUIActionsTimers::DeleteTimerRule(const CFileItem& item) const +{ + return DeleteTimer(item, false, true); +} + +bool CPVRGUIActionsTimers::DeleteTimer(const CFileItem& item, + bool bIsRecording, + bool bDeleteRule) const +{ + std::shared_ptr<CPVRTimerInfoTag> timer; + const std::shared_ptr<CPVRRecording> recording(CPVRItem(item).GetRecording()); + if (recording) + timer = recording->GetRecordingTimer(); + + if (!timer) + timer = CPVRItem(item).GetTimerInfoTag(); + + if (!timer) + { + CLog::LogF(LOGERROR, "No timer!"); + return false; + } + + if (bDeleteRule && !timer->IsTimerRule()) + timer = CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer); + + if (!timer) + { + CLog::LogF(LOGERROR, "No timer rule!"); + return false; + } + + if (bIsRecording) + { + if (ConfirmStopRecording(timer)) + { + if (CServiceBroker::GetPVRManager().Timers()->DeleteTimer(timer, true, false) == + TimerOperationResult::OK) + return true; + + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19170}); // "Error", "Could not stop recording. Check the log for more information about this message." + return false; + } + } + else if (!timer->GetTimerType()->AllowsDelete()) + { + return false; + } + else + { + bool bAlsoDeleteRule(false); + if (ConfirmDeleteTimer(timer, bAlsoDeleteRule)) + return DeleteTimer(timer, false, bAlsoDeleteRule); + } + return false; +} + +bool CPVRGUIActionsTimers::DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer, + bool bIsRecording, + bool bDeleteRule) const +{ + TimerOperationResult result = + CServiceBroker::GetPVRManager().Timers()->DeleteTimer(timer, bIsRecording, bDeleteRule); + switch (result) + { + case TimerOperationResult::RECORDING: + { + // recording running. ask the user if it should be deleted anyway + if (HELPERS::ShowYesNoDialogText( + CVariant{122}, // "Confirm delete" + CVariant{ + 19122}) // "This timer is still recording. Are you sure you want to delete this timer?" + != HELPERS::DialogResponse::CHOICE_YES) + return false; + + return DeleteTimer(timer, true, bDeleteRule); + } + case TimerOperationResult::OK: + { + return true; + } + case TimerOperationResult::FAILED: + { + HELPERS::ShowOKDialogText( + CVariant{257}, + CVariant{ + 19110}); // "Error", "Could not delete the timer. Check the log for more information about this message." + return false; + } + default: + { + CLog::LogF(LOGERROR, "Unhandled TimerOperationResult ({})!", static_cast<int>(result)); + break; + } + } + return false; +} + +bool CPVRGUIActionsTimers::ConfirmDeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer, + bool& bDeleteRule) const +{ + bool bConfirmed(false); + const std::shared_ptr<CPVRTimerInfoTag> parentTimer( + CServiceBroker::GetPVRManager().Timers()->GetTimerRule(timer)); + + if (parentTimer && parentTimer->GetTimerType()->AllowsDelete()) + { + // timer was scheduled by a deletable timer rule. prompt user for confirmation for deleting the timer rule, including scheduled timers. + bool bCancel(false); + bDeleteRule = CGUIDialogYesNo::ShowAndGetInput( + CVariant{122}, // "Confirm delete" + CVariant{ + 840}, // "Do you want to delete only this timer or also the timer rule that has scheduled it?" + CVariant{""}, CVariant{timer->Title()}, bCancel, CVariant{841}, // "Only this" + CVariant{593}, // "All" + 0); // no autoclose + bConfirmed = !bCancel; + } + else + { + bDeleteRule = false; + + // prompt user for confirmation for deleting the timer + bConfirmed = CGUIDialogYesNo::ShowAndGetInput( + CVariant{122}, // "Confirm delete" + timer->IsTimerRule() + ? CVariant{845} + // "Are you sure you want to delete this timer rule and all timers it has scheduled?" + : CVariant{846}, // "Are you sure you want to delete this timer?" + CVariant{""}, CVariant{timer->Title()}); + } + + return bConfirmed; +} + +bool CPVRGUIActionsTimers::StopRecording(const CFileItem& item) const +{ + if (!DeleteTimer(item, true, false)) + return false; + + CServiceBroker::GetPVRManager().TriggerRecordingsUpdate(); + return true; +} + +bool CPVRGUIActionsTimers::ConfirmStopRecording( + const std::shared_ptr<CPVRTimerInfoTag>& timer) const +{ + return CGUIDialogYesNo::ShowAndGetInput( + CVariant{847}, // "Confirm stop recording" + CVariant{848}, // "Are you sure you want to stop this recording?" + CVariant{""}, CVariant{timer->Title()}); +} + +namespace +{ +std::string GetAnnouncerText(const std::shared_ptr<CPVRTimerInfoTag>& timer, int idEpg, int idNoEpg) +{ + std::string text; + if (timer->IsEpgBased()) + { + text = StringUtils::Format(g_localizeStrings.Get(idEpg), + timer->Title(), // tv show title + timer->ChannelName(), + timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false)); + } + else + { + text = StringUtils::Format(g_localizeStrings.Get(idNoEpg), timer->ChannelName(), + timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false)); + } + return text; +} + +void AddEventLogEntry(const std::shared_ptr<CPVRTimerInfoTag>& timer, int idEpg, int idNoEpg) +{ + std::string name; + std::string icon; + + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(timer->GetTimerType()->GetClientId()); + if (client) + { + name = client->GetFriendlyName(); + icon = client->Icon(); + } + else + { + name = g_sysinfo.GetAppName(); + icon = "special://xbmc/media/icon256x256.png"; + } + + CPVREventLogJob* job = new CPVREventLogJob; + job->AddEvent(false, // do not display a toast, only log event + EventLevel::Information, // info, no error + name, GetAnnouncerText(timer, idEpg, idNoEpg), icon); + CServiceBroker::GetJobManager()->AddJob(job, nullptr); +} +} // unnamed namespace + +void CPVRGUIActionsTimers::AnnounceReminder(const std::shared_ptr<CPVRTimerInfoTag>& timer) const +{ + if (!timer->IsReminder()) + { + CLog::LogF(LOGERROR, "No reminder timer!"); + return; + } + + if (timer->EndAsUTC() < CDateTime::GetUTCDateTime()) + { + // expired. timer end is in the past. write event log entry. + AddEventLogEntry(timer, 19305, 19306); // Deleted missed PVR reminder ... + return; + } + + if (CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingChannel(timer->Channel())) + { + // no need for an announcement. channel in question is already playing. + return; + } + + // show the reminder dialog + CGUIDialogProgress* dialog = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogProgress>( + WINDOW_DIALOG_PROGRESS); + if (!dialog) + return; + + dialog->Reset(); + + dialog->SetHeading(CVariant{19312}); // "PVR reminder" + dialog->ShowChoice(0, CVariant{19165}); // "Switch" + + std::string text = GetAnnouncerText(timer, 19307, 19308); // Reminder for ... + + bool bCanRecord = false; + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().GetClient(timer->ClientID()); + if (client && client->GetClientCapabilities().SupportsTimers()) + { + bCanRecord = true; + dialog->ShowChoice(1, CVariant{264}); // "Record" + dialog->ShowChoice(2, CVariant{222}); // "Cancel" + + if (m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTORECORD)) + text += "\n\n" + g_localizeStrings.Get( + 19309); // (Auto-close of this reminder will schedule a recording...) + else if (m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTOSWITCH)) + text += "\n\n" + g_localizeStrings.Get( + 19331); // (Auto-close of this reminder will switch to channel...) + } + else + { + dialog->ShowChoice(1, CVariant{222}); // "Cancel" + } + + dialog->SetText(text); + dialog->SetPercentage(100); + + dialog->Open(); + + int result = CGUIDialogProgress::CHOICE_NONE; + + static constexpr int PROGRESS_TIMESLICE_MILLISECS = 50; + + const int iWait = m_settings.GetIntValue(CSettings::SETTING_PVRREMINDERS_AUTOCLOSEDELAY) * 1000; + int iRemaining = iWait; + while (iRemaining > 0) + { + result = dialog->GetChoice(); + if (result != CGUIDialogProgress::CHOICE_NONE) + break; + + std::this_thread::sleep_for(std::chrono::milliseconds(PROGRESS_TIMESLICE_MILLISECS)); + + iRemaining -= PROGRESS_TIMESLICE_MILLISECS; + dialog->SetPercentage(iRemaining * 100 / iWait); + dialog->Progress(); + } + + dialog->Close(); + + bool bAutoClosed = (iRemaining <= 0); + bool bSwitch = (result == 0); + bool bRecord = (result == 1); + + if (bAutoClosed) + { + bRecord = (bCanRecord && m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTORECORD)); + bSwitch = m_settings.GetBoolValue(CSettings::SETTING_PVRREMINDERS_AUTOSWITCH); + } + + if (bRecord) + { + std::shared_ptr<CPVRTimerInfoTag> newTimer; + + std::shared_ptr<CPVREpgInfoTag> epgTag = timer->GetEpgInfoTag(); + if (epgTag) + { + newTimer = CPVRTimerInfoTag::CreateFromEpg(epgTag, false); + if (newTimer) + { + // an epgtag can only have max one timer - we need to clear the reminder to be able to + // attach the recording timer + DeleteTimer(timer, false, false); + } + } + else + { + int iDuration = (timer->EndAsUTC() - timer->StartAsUTC()).GetSecondsTotal() / 60; + newTimer = CPVRTimerInfoTag::CreateTimerTag(timer->Channel(), timer->StartAsUTC(), iDuration); + } + + if (newTimer) + { + // schedule recording + AddTimer(CFileItem(newTimer), false); + } + + if (bAutoClosed) + { + AddEventLogEntry(timer, 19310, + 19311); // Scheduled recording for auto-closed PVR reminder ... + } + } + + if (bSwitch) + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember( + timer->Channel()); + if (groupMember) + { + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel( + CFileItem(groupMember), false); + + if (bAutoClosed) + { + AddEventLogEntry(timer, 19332, + 19333); // Switched channel for auto-closed PVR reminder ... + } + } + } +} + +void CPVRGUIActionsTimers::AnnounceReminders() const +{ + // Prevent multiple yesno dialogs, all on same call stack, due to gui message processing while dialog is open. + if (m_bReminderAnnouncementRunning) + return; + + m_bReminderAnnouncementRunning = true; + std::shared_ptr<CPVRTimerInfoTag> timer = + CServiceBroker::GetPVRManager().Timers()->GetNextReminderToAnnnounce(); + while (timer) + { + AnnounceReminder(timer); + timer = CServiceBroker::GetPVRManager().Timers()->GetNextReminderToAnnnounce(); + } + m_bReminderAnnouncementRunning = false; +} diff --git a/xbmc/pvr/guilib/PVRGUIActionsTimers.h b/xbmc/pvr/guilib/PVRGUIActionsTimers.h new file mode 100644 index 0000000..f6ae8fe --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsTimers.h @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2016-2022 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" +#include "pvr/settings/PVRSettings.h" + +#include <memory> + +class CFileItem; + +namespace PVR +{ +class CPVRChannel; +class CPVRTimerInfoTag; + +class CPVRGUIActionsTimers : public IPVRComponent +{ +public: + CPVRGUIActionsTimers(); + ~CPVRGUIActionsTimers() override = default; + + /*! + * @brief Open the timer settings dialog to create a new tv or radio timer. + * @param bRadio indicates whether a radio or tv timer shall be created. + * @return true on success, false otherwise. + */ + bool AddTimer(bool bRadio) const; + + /*! + * @brief Create a new timer, either interactive or non-interactive. + * @param item containing epg data to create a timer for. item must be an epg tag or a channel. + * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior + * creating the timer. + * @return true, if the timer was created successfully, false otherwise. + */ + bool AddTimer(const CFileItem& item, bool bShowTimerSettings) const; + + /*! + * @brief Add a timer to the client. Doesn't add the timer to the container. The backend will + * do this. + * @return True if it was sent correctly, false if not. + */ + bool AddTimer(const std::shared_ptr<CPVRTimerInfoTag>& item) const; + + /*! + * @brief Create a new timer rule, either interactive or non-interactive. + * @param item containing epg data to create a timer rule for. item must be an epg tag or a + * channel. + * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior + * creating the timer rule. + * @param bFallbackToOneShotTimer if no timer rule can be created, try to create a one-shot + * timer instead. + * @return true, if the timer rule was created successfully, false otherwise. + */ + bool AddTimerRule(const CFileItem& item, + bool bShowTimerSettings, + bool bFallbackToOneShotTimer) const; + + /*! + * @brief Creates or deletes a timer for the given epg tag. + * @param item containing an epg tag. + * @return true on success, false otherwise. + */ + bool ToggleTimer(const CFileItem& item) const; + + /*! + * @brief Toggles a given timer's enabled/disabled state. + * @param item containing a timer. + * @return true on success, false otherwise. + */ + bool ToggleTimerState(const CFileItem& item) const; + + /*! + * @brief Open the timer settings dialog to edit an existing timer. + * @param item containing an epg tag or a timer. + * @return true on success, false otherwise. + */ + bool EditTimer(const CFileItem& item) const; + + /*! + * @brief Open the timer settings dialog to edit an existing timer rule. + * @param item containing an epg tag or a timer. + * @return true on success, false otherwise. + */ + bool EditTimerRule(const CFileItem& item) const; + + /*! + * @brief Get the timer rule for a given timer + * @param item containing an item to query the timer rule for. item must be a timer or an epg tag. + * @return The timer rule item, or nullptr if none was found. + */ + std::shared_ptr<CFileItem> GetTimerRule(const CFileItem& item) const; + + /*! + * @brief Delete a timer, always showing a confirmation dialog. + * @param item containing a timer to delete. item must be a timer, an epg tag or a channel. + * @return true, if the timer was deleted successfully, false otherwise. + */ + bool DeleteTimer(const CFileItem& item) const; + + /*! + * @brief Delete a timer rule, always showing a confirmation dialog. + * @param item containing a timer rule to delete. item must be a timer, an epg tag or a channel. + * @return true, if the timer rule was deleted successfully, false otherwise. + */ + bool DeleteTimerRule(const CFileItem& item) const; + + /*! + * @brief Toggle recording on the currently playing channel, if any. + * @return True if the recording was started or stopped successfully, false otherwise. + */ + bool ToggleRecordingOnPlayingChannel(); + + /*! + * @brief Start or stop recording on a given channel. + * @param channel the channel to start/stop recording. + * @param bOnOff True to start recording, false to stop. + * @return True if the recording was started or stopped successfully, false otherwise. + */ + bool SetRecordingOnChannel(const std::shared_ptr<CPVRChannel>& channel, bool bOnOff); + + /*! + * @brief Stop a currently active recording, always showing a confirmation dialog. + * @param item containing a recording to stop. item must be a timer, an epg tag or a channel. + * @return true, if the recording was stopped successfully, false otherwise. + */ + bool StopRecording(const CFileItem& item) const; + + /*! + * @brief Create a new reminder timer, non-interactive. + * @param item containing epg data to create a reminder timer for. item must be an epg tag. + * @return true, if the timer was created successfully, false otherwise. + */ + bool AddReminder(const CFileItem& item) const; + + /*! + * @brief Announce due reminders, if any. + */ + void AnnounceReminders() const; + +private: + CPVRGUIActionsTimers(const CPVRGUIActionsTimers&) = delete; + CPVRGUIActionsTimers const& operator=(CPVRGUIActionsTimers const&) = delete; + + /*! + * @brief Open the timer settings dialog. + * @param timer containing the timer the settings shall be displayed for. + * @return true, if the dialog was ended successfully, false otherwise. + */ + bool ShowTimerSettings(const std::shared_ptr<CPVRTimerInfoTag>& timer) const; + + /*! + * @brief Add a timer or timer rule, either interactive or non-interactive. + * @param item containing epg data to create a timer or timer rule for. item must be an epg tag + * or a channel. + * @param bCreateteRule denotes whether to create a one-shot timer or a timer rule. + * @param bShowTimerSettings is used to control whether a settings dialog will be opened prior + * creating the timer or timer rule. + * @param bFallbackToOneShotTimer if bCreateteRule is true and no timer rule can be created, try + * to create a one-shot timer instead. + * @return true, if the timer or timer rule was created successfully, false otherwise. + */ + bool AddTimer(const CFileItem& item, + bool bCreateRule, + bool bShowTimerSettings, + bool bFallbackToOneShotTimer) const; + + /*! + * @brief Delete a timer or timer rule, always showing a confirmation dialog. + * @param item containing a timer or timer rule to delete. item must be a timer, an epg tag or + * a channel. + * @param bIsRecording denotes whether the timer is currently recording (controls correct + * confirmation dialog). + * @param bDeleteRule denotes to delete a timer rule. For convenience, one can pass a timer + * created by a rule. + * @return true, if the timer or timer rule was deleted successfully, false otherwise. + */ + bool DeleteTimer(const CFileItem& item, bool bIsRecording, bool bDeleteRule) const; + + /*! + * @brief Delete a timer or timer rule, showing a confirmation dialog in case a timer currently + * recording shall be deleted. + * @param timer containing a timer or timer rule to delete. + * @param bIsRecording denotes whether the timer is currently recording (controls correct + * confirmation dialog). + * @param bDeleteRule denotes to delete a timer rule. For convenience, one can pass a timer + * created by a rule. + * @return true, if the timer or timer rule was deleted successfully, false otherwise. + */ + bool DeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer, + bool bIsRecording, + bool bDeleteRule) const; + + /*! + * @brief Open a dialog to confirm timer delete. + * @param timer the timer to delete. + * @param bDeleteRule in: ignored. out, for one shot timer scheduled by a timer rule: true to + * also delete the timer rule that has scheduled this timer, false to only delete the one shot + * timer. out, for one shot timer not scheduled by a timer rule: ignored + * @return true, to proceed with delete, false otherwise. + */ + bool ConfirmDeleteTimer(const std::shared_ptr<CPVRTimerInfoTag>& timer, bool& bDeleteRule) const; + + /*! + * @brief Open a dialog to confirm stop recording. + * @param timer the recording to stop (actually the timer to delete). + * @return true, to proceed with delete, false otherwise. + */ + bool ConfirmStopRecording(const std::shared_ptr<CPVRTimerInfoTag>& timer) const; + + /*! + * @brief Announce and process a reminder timer. + * @param timer The reminder timer. + */ + void AnnounceReminder(const std::shared_ptr<CPVRTimerInfoTag>& timer) const; + + CPVRSettings m_settings; + mutable bool m_bReminderAnnouncementRunning{false}; +}; + +namespace GUI +{ +// pretty scope and name +using Timers = CPVRGUIActionsTimers; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp b/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp new file mode 100644 index 0000000..28e5582 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsUtils.cpp @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIActionsUtils.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "pvr/PVRManager.h" +#include "pvr/guilib/PVRGUIActionsEPG.h" +#include "pvr/guilib/PVRGUIActionsRecordings.h" + +namespace PVR +{ +bool CPVRGUIActionsUtils::OnInfo(const CFileItem& item) +{ + if (item.HasPVRRecordingInfoTag()) + { + return CServiceBroker::GetPVRManager().Get<PVR::GUI::Recordings>().ShowRecordingInfo(item); + } + else if (item.HasPVRChannelInfoTag() || item.HasPVRTimerInfoTag()) + { + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().ShowEPGInfo(item); + } + else if (item.HasEPGSearchFilter()) + { + return CServiceBroker::GetPVRManager().Get<PVR::GUI::EPG>().EditSavedSearch(item); + } + return false; +} + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIActionsUtils.h b/xbmc/pvr/guilib/PVRGUIActionsUtils.h new file mode 100644 index 0000000..a880548 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIActionsUtils.h @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2016-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "pvr/IPVRComponent.h" + +class CFileItem; + +namespace PVR +{ +class CPVRGUIActionsUtils : public IPVRComponent +{ +public: + CPVRGUIActionsUtils() = default; + ~CPVRGUIActionsUtils() override = default; + + /*! + * @brief Process info action for the given item. + * @param item The item. + */ + bool OnInfo(const CFileItem& item); + +private: + CPVRGUIActionsUtils(const CPVRGUIActionsUtils&) = delete; + CPVRGUIActionsUtils const& operator=(CPVRGUIActionsUtils const&) = delete; +}; + +namespace GUI +{ +// pretty scope and name +using Utils = CPVRGUIActionsUtils; +} // namespace GUI + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp new file mode 100644 index 0000000..a239fe2 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2012-2019 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIChannelIconUpdater.h" + +#include "FileItem.h" +#include "ServiceBroker.h" +#include "Util.h" +#include "filesystem/Directory.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/guilib/PVRGUIProgressHandler.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "utils/FileUtils.h" +#include "utils/URIUtils.h" +#include "utils/log.h" + +#include <map> +#include <string> +#include <vector> + +using namespace PVR; + +void CPVRGUIChannelIconUpdater::SearchAndUpdateMissingChannelIcons() const +{ + const std::string iconPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_PVRMENU_ICONPATH); + if (iconPath.empty()) + return; + + // fetch files in icon path for fast lookup + CFileItemList fileItemList; + XFILE::CDirectory::GetDirectory(iconPath, fileItemList, ".jpg|.png|.tbn", XFILE::DIR_FLAG_DEFAULTS); + + if (fileItemList.IsEmpty()) + return; + + CLog::Log(LOGINFO, "Starting PVR channel icon search"); + + // create a map for fast lookup of normalized file base name + std::map<std::string, std::string> fileItemMap; + for (const auto& item : fileItemList) + { + std::string baseName = URIUtils::GetFileName(item->GetPath()); + URIUtils::RemoveExtension(baseName); + StringUtils::ToLower(baseName); + fileItemMap.insert({baseName, item->GetPath()}); + } + + std::unique_ptr<CPVRGUIProgressHandler> progressHandler; + if (!m_groups.empty()) + progressHandler.reset( + new CPVRGUIProgressHandler(g_localizeStrings.Get(19286))); // Searching for channel icons + + for (const auto& group : m_groups) + { + const std::vector<std::shared_ptr<CPVRChannelGroupMember>> members = group->GetMembers(); + int channelIndex = 0; + for (const auto& member : members) + { + const std::shared_ptr<CPVRChannel> channel = member->Channel(); + + progressHandler->UpdateProgress(channel->ChannelName(), channelIndex++, members.size()); + + // skip if an icon is already set and exists + if (CFileUtils::Exists(channel->IconPath())) + continue; + + // reset icon before searching for a new one + channel->SetIconPath(""); + + const std::string strChannelUid = StringUtils::Format("{:08}", channel->UniqueID()); + std::string strLegalClientChannelName = + CUtil::MakeLegalFileName(channel->ClientChannelName()); + StringUtils::ToLower(strLegalClientChannelName); + std::string strLegalChannelName = CUtil::MakeLegalFileName(channel->ChannelName()); + StringUtils::ToLower(strLegalChannelName); + + std::map<std::string, std::string>::iterator itItem; + if ((itItem = fileItemMap.find(strLegalClientChannelName)) != fileItemMap.end() || + (itItem = fileItemMap.find(strLegalChannelName)) != fileItemMap.end() || + (itItem = fileItemMap.find(strChannelUid)) != fileItemMap.end()) + { + channel->SetIconPath(itItem->second, CServiceBroker::GetSettingsComponent() + ->GetAdvancedSettings() + ->m_bPVRAutoScanIconsUserSet); + } + + if (m_bUpdateDb) + channel->Persist(); + } + } +} diff --git a/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h new file mode 100644 index 0000000..d64a41f --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIChannelIconUpdater.h @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012-2019 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include <memory> +#include <vector> + +namespace PVR +{ + +class CPVRChannelGroup; + +class CPVRGUIChannelIconUpdater +{ +public: + /*! + * @brief ctor. + * @param groups The channel groups for which the channel icons shall be updated. + * @param bUpdateDb If true, persist the changed values in the PVR database. + */ + CPVRGUIChannelIconUpdater(const std::vector<std::shared_ptr<CPVRChannelGroup>>& groups, bool bUpdateDb) + : m_groups(groups), m_bUpdateDb(bUpdateDb) {} + + CPVRGUIChannelIconUpdater() = delete; + CPVRGUIChannelIconUpdater(const CPVRGUIChannelIconUpdater&) = delete; + CPVRGUIChannelIconUpdater& operator=(const CPVRGUIChannelIconUpdater&) = delete; + + virtual ~CPVRGUIChannelIconUpdater() = default; + + /*! + * @brief Search and update missing channel icons. + */ + void SearchAndUpdateMissingChannelIcons() const; + +private: + const std::vector<std::shared_ptr<CPVRChannelGroup>> m_groups; + const bool m_bUpdateDb = false; +}; + +} diff --git a/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp b/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp new file mode 100644 index 0000000..0ccfdf8 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIChannelNavigator.cpp @@ -0,0 +1,373 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIChannelNavigator.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/guilib/PVRGUIActionsPlayback.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/SystemClock.h" +#include "utils/Job.h" +#include "utils/JobManager.h" +#include "utils/XTimeUtils.h" + +#include <mutex> + +using namespace KODI::GUILIB::GUIINFO; +using namespace PVR; +using namespace std::chrono_literals; + +namespace +{ +class CPVRChannelTimeoutJobBase : public CJob, public IJobCallback +{ +public: + CPVRChannelTimeoutJobBase() = delete; + CPVRChannelTimeoutJobBase(PVR::CPVRGUIChannelNavigator& channelNavigator, + std::chrono::milliseconds timeout) + : m_channelNavigator(channelNavigator) + { + m_delayTimer.Set(timeout); + } + + ~CPVRChannelTimeoutJobBase() override = default; + + virtual void OnTimeout() = 0; + + void OnJobComplete(unsigned int iJobID, bool bSuccess, CJob* job) override {} + + bool DoWork() override + { + while (!ShouldCancel(0, 0)) + { + if (m_delayTimer.IsTimePast()) + { + OnTimeout(); + return true; + } + KODI::TIME::Sleep(10ms); + } + return false; + } + +protected: + PVR::CPVRGUIChannelNavigator& m_channelNavigator; + +private: + XbmcThreads::EndTime<> m_delayTimer; +}; + +class CPVRChannelEntryTimeoutJob : public CPVRChannelTimeoutJobBase +{ +public: + CPVRChannelEntryTimeoutJob(PVR::CPVRGUIChannelNavigator& channelNavigator, + std::chrono::milliseconds timeout) + : CPVRChannelTimeoutJobBase(channelNavigator, timeout) + { + } + ~CPVRChannelEntryTimeoutJob() override = default; + const char* GetType() const override { return "pvr-channel-entry-timeout-job"; } + void OnTimeout() override { m_channelNavigator.SwitchToCurrentChannel(); } +}; + +class CPVRChannelInfoTimeoutJob : public CPVRChannelTimeoutJobBase +{ +public: + CPVRChannelInfoTimeoutJob(PVR::CPVRGUIChannelNavigator& channelNavigator, + std::chrono::milliseconds timeout) + : CPVRChannelTimeoutJobBase(channelNavigator, timeout) + { + } + ~CPVRChannelInfoTimeoutJob() override = default; + const char* GetType() const override { return "pvr-channel-info-timeout-job"; } + void OnTimeout() override { m_channelNavigator.HideInfo(); } +}; +} // unnamed namespace + +CPVRGUIChannelNavigator::CPVRGUIChannelNavigator() +{ + // Note: we cannot subscribe to PlayerInfoProvider here, as we're getting constructed + // before the info providers. We will subscribe once our first subscriber appears. +} + +CPVRGUIChannelNavigator::~CPVRGUIChannelNavigator() +{ + const auto gui = CServiceBroker::GetGUI(); + if (!gui) + return; + + gui->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().Events().Unsubscribe(this); +} + +void CPVRGUIChannelNavigator::SubscribeToShowInfoEventStream() +{ + CServiceBroker::GetGUI() + ->GetInfoManager() + .GetInfoProviders() + .GetPlayerInfoProvider() + .Events() + .Subscribe(this, &CPVRGUIChannelNavigator::Notify); +} + +void CPVRGUIChannelNavigator::CheckAndPublishPreviewAndPlayerShowInfoChangedEvent() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + const bool currentValue = IsPreview() && m_playerShowInfo; + if (m_previewAndPlayerShowInfo != currentValue) + { + m_previewAndPlayerShowInfo = currentValue; + + // inform subscribers + m_events.Publish(PVRPreviewAndPlayerShowInfoChangedEvent(currentValue)); + } +} + +void CPVRGUIChannelNavigator::Notify(const PlayerShowInfoChangedEvent& event) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_playerShowInfo = event.m_showInfo; + CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); +} + +void CPVRGUIChannelNavigator::SelectNextChannel(ChannelSwitchMode eSwitchMode) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (!m_playerShowInfo && eSwitchMode == ChannelSwitchMode::NO_SWITCH) + { + // show info for current channel on first next channel selection. + ShowInfo(false); + return; + } + + const std::shared_ptr<CPVRChannelGroupMember> nextMember = GetNextOrPrevChannel(true); + if (nextMember) + SelectChannel(nextMember, eSwitchMode); +} + +void CPVRGUIChannelNavigator::SelectPreviousChannel(ChannelSwitchMode eSwitchMode) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (!m_playerShowInfo && eSwitchMode == ChannelSwitchMode::NO_SWITCH) + { + // show info for current channel on first previous channel selection. + ShowInfo(false); + return; + } + + const std::shared_ptr<CPVRChannelGroupMember> prevMember = GetNextOrPrevChannel(false); + if (prevMember) + SelectChannel(prevMember, eSwitchMode); +} + +std::shared_ptr<CPVRChannelGroupMember> CPVRGUIChannelNavigator::GetNextOrPrevChannel(bool bNext) +{ + const bool bPlayingRadio = CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio(); + const bool bPlayingTV = CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV(); + + if (bPlayingTV || bPlayingRadio) + { + const std::shared_ptr<CPVRChannelGroup> group = + CServiceBroker::GetPVRManager().PlaybackState()->GetActiveChannelGroup(bPlayingRadio); + if (group) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + return bNext ? group->GetNextChannelGroupMember(m_currentChannel) + : group->GetPreviousChannelGroupMember(m_currentChannel); + } + } + return {}; +} + +void CPVRGUIChannelNavigator::SelectChannel( + const std::shared_ptr<CPVRChannelGroupMember>& groupMember, ChannelSwitchMode eSwitchMode) +{ + CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(CFileItem(groupMember)); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_currentChannel = groupMember; + ShowInfo(false); + + CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); + + if (IsPreview() && eSwitchMode == ChannelSwitchMode::INSTANT_OR_DELAYED_SWITCH) + { + auto timeout = + std::chrono::milliseconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_PVRPLAYBACK_CHANNELENTRYTIMEOUT)); + if (timeout > 0ms) + { + // delayed switch + if (m_iChannelEntryJobId >= 0) + CServiceBroker::GetJobManager()->CancelJob(m_iChannelEntryJobId); + + CPVRChannelEntryTimeoutJob* job = new CPVRChannelEntryTimeoutJob(*this, timeout); + m_iChannelEntryJobId = + CServiceBroker::GetJobManager()->AddJob(job, dynamic_cast<IJobCallback*>(job)); + } + else + { + // instant switch + SwitchToCurrentChannel(); + } + } +} + +void CPVRGUIChannelNavigator::SwitchToCurrentChannel() +{ + std::unique_ptr<CFileItem> item; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iChannelEntryJobId >= 0) + { + CServiceBroker::GetJobManager()->CancelJob(m_iChannelEntryJobId); + m_iChannelEntryJobId = -1; + } + + item = std::make_unique<CFileItem>(m_currentChannel); + } + + if (item) + CServiceBroker::GetPVRManager().Get<PVR::GUI::Playback>().SwitchToChannel(*item, false); +} + +bool CPVRGUIChannelNavigator::IsPreview() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_currentChannel != m_playingChannel; +} + +bool CPVRGUIChannelNavigator::IsPreviewAndShowInfo() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_previewAndPlayerShowInfo; +} + +void CPVRGUIChannelNavigator::ShowInfo() +{ + ShowInfo(true); +} + +void CPVRGUIChannelNavigator::ShowInfo(bool bForce) +{ + auto timeout = std::chrono::seconds(CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt( + CSettings::SETTING_PVRMENU_DISPLAYCHANNELINFO)); + + if (bForce || timeout > 0s) + { + CServiceBroker::GetGUI() + ->GetInfoManager() + .GetInfoProviders() + .GetPlayerInfoProvider() + .SetShowInfo(true); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iChannelInfoJobId >= 0) + { + CServiceBroker::GetJobManager()->CancelJob(m_iChannelInfoJobId); + m_iChannelInfoJobId = -1; + } + + if (!bForce && timeout > 0s) + { + CPVRChannelInfoTimeoutJob* job = new CPVRChannelInfoTimeoutJob(*this, timeout); + m_iChannelInfoJobId = + CServiceBroker::GetJobManager()->AddJob(job, dynamic_cast<IJobCallback*>(job)); + } + } +} + +void CPVRGUIChannelNavigator::HideInfo() +{ + CServiceBroker::GetGUI()->GetInfoManager().GetInfoProviders().GetPlayerInfoProvider().SetShowInfo( + false); + + CFileItemPtr item; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_iChannelInfoJobId >= 0) + { + CServiceBroker::GetJobManager()->CancelJob(m_iChannelInfoJobId); + m_iChannelInfoJobId = -1; + } + + if (m_currentChannel != m_playingChannel) + { + m_currentChannel = m_playingChannel; + if (m_playingChannel) + item.reset(new CFileItem(m_playingChannel)); + } + + CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); + } + + if (item) + CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*item); +} + +void CPVRGUIChannelNavigator::ToggleInfo() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (m_playerShowInfo) + HideInfo(); + else + ShowInfo(); +} + +void CPVRGUIChannelNavigator::SetPlayingChannel( + const std::shared_ptr<CPVRChannelGroupMember>& groupMember) +{ + CFileItemPtr item; + + if (groupMember) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_playingChannel = groupMember; + if (m_currentChannel != m_playingChannel) + { + m_currentChannel = m_playingChannel; + if (m_playingChannel) + item.reset(new CFileItem(m_playingChannel)); + } + + CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); + } + + if (item) + CServiceBroker::GetGUI()->GetInfoManager().SetCurrentItem(*item); + + ShowInfo(false); +} + +void CPVRGUIChannelNavigator::ClearPlayingChannel() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_playingChannel.reset(); + HideInfo(); + + CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); +} diff --git a/xbmc/pvr/guilib/PVRGUIChannelNavigator.h b/xbmc/pvr/guilib/PVRGUIChannelNavigator.h new file mode 100644 index 0000000..5c27074 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIChannelNavigator.h @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "utils/EventStream.h" + +#include <memory> + +namespace KODI +{ +namespace GUILIB +{ +namespace GUIINFO +{ +struct PlayerShowInfoChangedEvent; +} +} // namespace GUILIB +} // namespace KODI + +namespace PVR +{ +enum class ChannelSwitchMode +{ + NO_SWITCH, // no channel switch + INSTANT_OR_DELAYED_SWITCH // switch according to SETTING_PVRPLAYBACK_CHANNELENTRYTIMEOUT +}; + +struct PVRPreviewAndPlayerShowInfoChangedEvent +{ + explicit PVRPreviewAndPlayerShowInfoChangedEvent(bool previewAndPlayerShowInfo) + : m_previewAndPlayerShowInfo(previewAndPlayerShowInfo) + { + } + virtual ~PVRPreviewAndPlayerShowInfoChangedEvent() = default; + + bool m_previewAndPlayerShowInfo{false}; +}; + +class CPVRChannelGroupMember; + +class CPVRGUIChannelNavigator +{ +public: + CPVRGUIChannelNavigator(); + virtual ~CPVRGUIChannelNavigator(); + + /*! + * @brief Subscribe to the event stream for changes of channel preview and player show info. + * @param owner The subscriber. + * @param fn The callback function of the subscriber for the events. + */ + template<typename A> + void Subscribe(A* owner, void (A::*fn)(const PVRPreviewAndPlayerShowInfoChangedEvent&)) + { + SubscribeToShowInfoEventStream(); + m_events.Subscribe(owner, fn); + } + + /*! + * @brief Unsubscribe from the event stream for changes of channel preview and player show info. + * @param obj The subscriber. + */ + template<typename A> + void Unsubscribe(A* obj) + { + m_events.Unsubscribe(obj); + } + + /*! + * @brief CEventStream callback for player show info flag changes. + * @param event The event. + */ + void Notify(const KODI::GUILIB::GUIINFO::PlayerShowInfoChangedEvent& event); + + /*! + * @brief Select the next channel in currently playing channel group, relative to the currently + * selected channel. + * @param eSwitchMode controls whether only the channel info OSD is triggered or whether + * additionally a (delayed) channel switch will be done. + */ + void SelectNextChannel(ChannelSwitchMode eSwitchMode); + + /*! + * @brief Select the previous channel in currently playing channel group, relative to the + * currently selected channel. + * @param eSwitchMode controls whether only the channel info OSD is triggered or whether + * additionally a (delayed) channel switch will be done. + */ + void SelectPreviousChannel(ChannelSwitchMode eSwitchMode); + + /*! + * @brief Switch to the currently selected channel. + */ + void SwitchToCurrentChannel(); + + /*! + * @brief Query the state of channel preview. + * @return True, if the currently selected channel is different from the currently playing + * channel, False otherwise. + */ + bool IsPreview() const; + + /*! + * @brief Query the state of channel preview and channel info OSD. + * @return True, if the currently selected channel is different from the currently playing channel + * and channel info OSD is active, False otherwise. + */ + bool IsPreviewAndShowInfo() const; + + /*! + * @brief Show the channel info OSD. + */ + void ShowInfo(); + + /*! + * @brief Hide the channel info OSD. + */ + void HideInfo(); + + /*! + * @brief Toggle the channel info OSD visibility. + */ + void ToggleInfo(); + + /*! + * @brief Set a new playing channel group member and show the channel info OSD for the new + * channel. + * @param groupMember The new playing channel group member + */ + void SetPlayingChannel(const std::shared_ptr<CPVRChannelGroupMember>& groupMember); + + /*! + * @brief Clear the currently playing channel and hide the channel info OSD. + */ + void ClearPlayingChannel(); + +private: + /*! + * @brief Get next or previous channel group member of the playing channel group, relative to the + * currently selected channel group member. + * @param bNext True to get the next channel group member, false to get the previous channel group + * member. + * @param return The channel or nullptr if not found. + */ + std::shared_ptr<CPVRChannelGroupMember> GetNextOrPrevChannel(bool bNext); + + /*! + * @brief Select a given channel group member, display channel info OSD, switch according to given + * switch mode. + * @param groupMember The channel group member to select. + * @param eSwitchMode The channel switch mode. + */ + void SelectChannel(const std::shared_ptr<CPVRChannelGroupMember>& groupMember, + ChannelSwitchMode eSwitchMode); + + /*! + * @brief Show the channel info OSD. + * @param bForce True ignores value of SETTING_PVRMENU_DISPLAYCHANNELINFO and always activates the + * info, False acts aaccording settings value. + */ + void ShowInfo(bool bForce); + + /*! + * @brief Subscribe to the event stream for changes of player show info. + */ + void SubscribeToShowInfoEventStream(); + + /*! + * @brief Check if property preview and show info value changed, inform subscribers in case. + */ + void CheckAndPublishPreviewAndPlayerShowInfoChangedEvent(); + + mutable CCriticalSection m_critSection; + std::shared_ptr<CPVRChannelGroupMember> m_playingChannel; + std::shared_ptr<CPVRChannelGroupMember> m_currentChannel; + int m_iChannelEntryJobId = -1; + int m_iChannelInfoJobId = -1; + CEventSource<PVRPreviewAndPlayerShowInfoChangedEvent> m_events; + bool m_playerShowInfo{false}; + bool m_previewAndPlayerShowInfo{false}; +}; +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp b/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp new file mode 100644 index 0000000..f876522 --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIProgressHandler.cpp @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIProgressHandler.h" + +#include "ServiceBroker.h" +#include "dialogs/GUIDialogExtendedProgressBar.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/WindowIDs.h" + +#include <algorithm> +#include <cmath> +#include <mutex> +#include <string> + +using namespace std::chrono_literals; + +namespace PVR +{ + +CPVRGUIProgressHandler::CPVRGUIProgressHandler(const std::string& strTitle) + : CThread("PVRGUIProgressHandler"), m_strTitle(strTitle) +{ +} + +void CPVRGUIProgressHandler::UpdateProgress(const std::string& strText, float fProgress) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_bChanged = true; + m_strText = strText; + m_fProgress = fProgress; + + if (!m_bCreated) + { + m_bCreated = true; + Create(); + } +} + +void CPVRGUIProgressHandler::UpdateProgress(const std::string& strText, int iCurrent, int iMax) +{ + float fPercentage = (iCurrent * 100.0f) / iMax; + if (!std::isnan(fPercentage)) + fPercentage = std::min(100.0f, fPercentage); + + UpdateProgress(strText, fPercentage); +} + +void CPVRGUIProgressHandler::Process() +{ + CGUIDialogExtendedProgressBar* progressBar = + CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogExtendedProgressBar>( + WINDOW_DIALOG_EXT_PROGRESS); + if (m_bStop || !progressBar) + return; + + CGUIDialogProgressBarHandle* progressHandle = progressBar->GetHandle(m_strTitle); + if (!progressHandle) + return; + + while (!m_bStop) + { + float fProgress = 0.0; + std::string strText; + bool bUpdate = false; + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_bChanged) + { + m_bChanged = false; + fProgress = m_fProgress; + strText = m_strText; + bUpdate = true; + } + } + + if (bUpdate) + { + progressHandle->SetPercentage(fProgress); + progressHandle->SetText(strText); + } + + // Intentionally ignore some changes that come in too fast. Humans cannot read as fast as Mr. Data ;-) + CThread::Sleep(100ms); + } + + progressHandle->MarkFinished(); +} + +} // namespace PVR diff --git a/xbmc/pvr/guilib/PVRGUIProgressHandler.h b/xbmc/pvr/guilib/PVRGUIProgressHandler.h new file mode 100644 index 0000000..6e7b72f --- /dev/null +++ b/xbmc/pvr/guilib/PVRGUIProgressHandler.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2017-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "threads/Thread.h" + +#include <string> + +namespace PVR +{ + class CPVRGUIProgressHandler : private CThread + { + public: + CPVRGUIProgressHandler() = delete; + + /*! + * @brief Creates and asynchronously shows a progress dialog with the given title. + * @param strTitle The title for the progress dialog. + */ + explicit CPVRGUIProgressHandler(const std::string& strTitle); + + ~CPVRGUIProgressHandler() override = default; + + /*! + * @brief Update the progress dialogs's content. + * @param strText The new progress text. + * @param fProgress The new progress value, in a range from 0.0 to 100.0. + */ + void UpdateProgress(const std::string& strText, float fProgress); + + /*! + * @brief Update the progress dialogs's content. + * @param strText The new progress text. + * @param iCurrent The new current progress value, must be less or equal iMax. + * @param iMax The new maximum progress value, must be greater or equal iCurrent. + */ + void UpdateProgress(const std::string& strText, int iCurrent, int iMax); + + protected: + // CThread implementation + void Process() override; + + private: + CCriticalSection m_critSection; + const std::string m_strTitle; + std::string m_strText; + float m_fProgress{0.0f}; + bool m_bChanged{false}; + bool m_bCreated{false}; + }; + +} // namespace PVR diff --git a/xbmc/pvr/guilib/guiinfo/CMakeLists.txt b/xbmc/pvr/guilib/guiinfo/CMakeLists.txt new file mode 100644 index 0000000..18491ee --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/CMakeLists.txt @@ -0,0 +1,9 @@ +set(SOURCES PVRGUIInfo.cpp + PVRGUITimerInfo.cpp + PVRGUITimesInfo.cpp) + +set(HEADERS PVRGUIInfo.h + PVRGUITimerInfo.h + PVRGUITimesInfo.h) + +core_add_library(pvr_guilib_guiinfo) diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp new file mode 100644 index 0000000..9cffb9a --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.cpp @@ -0,0 +1,2017 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUIInfo.h" + +#include "FileItem.h" +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "application/Application.h" +#include "application/ApplicationComponents.h" +#include "application/ApplicationPlayer.h" +#include "guilib/GUIComponent.h" +#include "guilib/GUIWindowManager.h" +#include "guilib/LocalizeStrings.h" +#include "guilib/guiinfo/GUIInfo.h" +#include "guilib/guiinfo/GUIInfoLabels.h" +#include "pvr/PVRItem.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/addons/PVRClient.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroup.h" +#include "pvr/channels/PVRChannelGroupMember.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/channels/PVRRadioRDSInfoTag.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/epg/EpgSearchFilter.h" +#include "pvr/guilib/PVRGUIActionsChannels.h" +#include "pvr/providers/PVRProvider.h" +#include "pvr/providers/PVRProviders.h" +#include "pvr/recordings/PVRRecording.h" +#include "pvr/recordings/PVRRecordings.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/AdvancedSettings.h" +#include "settings/Settings.h" +#include "settings/SettingsComponent.h" +#include "threads/SingleLock.h" +#include "threads/SystemClock.h" +#include "utils/StringUtils.h" + +#include <cmath> +#include <ctime> +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +using namespace PVR; +using namespace KODI::GUILIB::GUIINFO; +using namespace std::chrono_literals; + +CPVRGUIInfo::CPVRGUIInfo() : CThread("PVRGUIInfo") +{ + ResetProperties(); +} + +void CPVRGUIInfo::ResetProperties() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_anyTimersInfo.ResetProperties(); + m_tvTimersInfo.ResetProperties(); + m_radioTimersInfo.ResetProperties(); + m_timesInfo.Reset(); + m_bHasTVRecordings = false; + m_bHasRadioRecordings = false; + m_iCurrentActiveClient = 0; + m_strPlayingClientName.clear(); + m_strBackendName.clear(); + m_strBackendVersion.clear(); + m_strBackendHost.clear(); + m_strBackendTimers.clear(); + m_strBackendRecordings.clear(); + m_strBackendDeletedRecordings.clear(); + m_strBackendProviders.clear(); + m_strBackendChannelGroups.clear(); + m_strBackendChannels.clear(); + m_iBackendDiskTotal = 0; + m_iBackendDiskUsed = 0; + m_bIsPlayingTV = false; + m_bIsPlayingRadio = false; + m_bIsPlayingRecording = false; + m_bIsPlayingEpgTag = false; + m_bIsPlayingEncryptedStream = false; + m_bIsRecordingPlayingChannel = false; + m_bCanRecordPlayingChannel = false; + m_bIsPlayingActiveRecording = false; + m_bHasTVChannels = false; + m_bHasRadioChannels = false; + + ClearQualityInfo(m_qualityInfo); + ClearDescrambleInfo(m_descrambleInfo); + + m_updateBackendCacheRequested = false; + m_bRegistered = false; +} + +void CPVRGUIInfo::ClearQualityInfo(PVR_SIGNAL_STATUS& qualityInfo) +{ + memset(&qualityInfo, 0, sizeof(qualityInfo)); + strncpy(qualityInfo.strAdapterName, g_localizeStrings.Get(13106).c_str(), + PVR_ADDON_NAME_STRING_LENGTH - 1); + strncpy(qualityInfo.strAdapterStatus, g_localizeStrings.Get(13106).c_str(), + PVR_ADDON_NAME_STRING_LENGTH - 1); +} + +void CPVRGUIInfo::ClearDescrambleInfo(PVR_DESCRAMBLE_INFO& descrambleInfo) +{ + descrambleInfo = {}; +} + +void CPVRGUIInfo::Start() +{ + ResetProperties(); + Create(); + SetPriority(ThreadPriority::BELOW_NORMAL); +} + +void CPVRGUIInfo::Stop() +{ + StopThread(); + + auto& mgr = CServiceBroker::GetPVRManager(); + auto& channels = mgr.Get<PVR::GUI::Channels>(); + channels.GetChannelNavigator().Unsubscribe(this); + channels.Events().Unsubscribe(this); + mgr.Events().Unsubscribe(this); + + CGUIComponent* gui = CServiceBroker::GetGUI(); + if (gui) + { + gui->GetInfoManager().UnregisterInfoProvider(this); + m_bRegistered = false; + } +} + +void CPVRGUIInfo::Notify(const PVREvent& event) +{ + if (event == PVREvent::Timers || event == PVREvent::TimersInvalidated) + UpdateTimersCache(); +} + +void CPVRGUIInfo::Notify(const PVRChannelNumberInputChangedEvent& event) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_channelNumberInput = event.m_input; +} + +void CPVRGUIInfo::Notify(const PVRPreviewAndPlayerShowInfoChangedEvent& event) +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_previewAndPlayerShowInfo = event.m_previewAndPlayerShowInfo; +} + +void CPVRGUIInfo::Process() +{ + auto toggleIntervalMs = std::chrono::milliseconds( + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRInfoToggleInterval); + XbmcThreads::EndTime<> cacheTimer(toggleIntervalMs); + + auto& mgr = CServiceBroker::GetPVRManager(); + mgr.Events().Subscribe(this, &CPVRGUIInfo::Notify); + + auto& channels = mgr.Get<PVR::GUI::Channels>(); + channels.Events().Subscribe(this, &CPVRGUIInfo::Notify); + channels.GetChannelNavigator().Subscribe(this, &CPVRGUIInfo::Notify); + + /* updated on request */ + UpdateTimersCache(); + + /* update the backend cache once initially */ + m_updateBackendCacheRequested = true; + + while (!g_application.m_bStop && !m_bStop) + { + if (!m_bRegistered) + { + CGUIComponent* gui = CServiceBroker::GetGUI(); + if (gui) + { + gui->GetInfoManager().RegisterInfoProvider(this); + m_bRegistered = true; + } + } + + if (!m_bStop) + UpdateQualityData(); + std::this_thread::yield(); + + if (!m_bStop) + UpdateDescrambleData(); + std::this_thread::yield(); + + if (!m_bStop) + UpdateMisc(); + std::this_thread::yield(); + + if (!m_bStop) + UpdateTimeshiftData(); + std::this_thread::yield(); + + if (!m_bStop) + UpdateTimersToggle(); + std::this_thread::yield(); + + if (!m_bStop) + UpdateNextTimer(); + std::this_thread::yield(); + + // Update the backend cache every toggleInterval seconds + if (!m_bStop && cacheTimer.IsTimePast()) + { + UpdateBackendCache(); + cacheTimer.Set(toggleIntervalMs); + } + + if (!m_bStop) + CThread::Sleep(500ms); + } +} + +void CPVRGUIInfo::UpdateQualityData() +{ + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_PVRPLAYBACK_SIGNALQUALITY)) + return; + + const std::shared_ptr<CPVRPlaybackState> playbackState = + CServiceBroker::GetPVRManager().PlaybackState(); + if (!playbackState) + return; + + PVR_SIGNAL_STATUS qualityInfo; + ClearQualityInfo(qualityInfo); + + const int channelUid = playbackState->GetPlayingChannelUniqueID(); + if (channelUid > 0) + { + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().Clients()->GetCreatedClient( + playbackState->GetPlayingClientID()); + if (client) + client->SignalQuality(channelUid, qualityInfo); + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_qualityInfo = qualityInfo; +} + +void CPVRGUIInfo::UpdateDescrambleData() +{ + const std::shared_ptr<CPVRPlaybackState> playbackState = + CServiceBroker::GetPVRManager().PlaybackState(); + if (!playbackState) + return; + + PVR_DESCRAMBLE_INFO descrambleInfo; + ClearDescrambleInfo(descrambleInfo); + + const int channelUid = playbackState->GetPlayingChannelUniqueID(); + if (channelUid > 0) + { + const std::shared_ptr<CPVRClient> client = + CServiceBroker::GetPVRManager().Clients()->GetCreatedClient( + playbackState->GetPlayingClientID()); + if (client) + client->GetDescrambleInfo(channelUid, descrambleInfo); + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_descrambleInfo = descrambleInfo; +} + +void CPVRGUIInfo::UpdateMisc() +{ + const CPVRManager& mgr = CServiceBroker::GetPVRManager(); + bool bStarted = mgr.IsStarted(); + const std::shared_ptr<CPVRPlaybackState> state = mgr.PlaybackState(); + + /* safe to fetch these unlocked, since they're updated from the same thread as this one */ + const std::string strPlayingClientName = bStarted ? state->GetPlayingClientName() : ""; + const bool bHasTVRecordings = bStarted && mgr.Recordings()->GetNumTVRecordings() > 0; + const bool bHasRadioRecordings = bStarted && mgr.Recordings()->GetNumRadioRecordings() > 0; + const bool bIsPlayingTV = bStarted && state->IsPlayingTV(); + const bool bIsPlayingRadio = bStarted && state->IsPlayingRadio(); + const bool bIsPlayingRecording = bStarted && state->IsPlayingRecording(); + const bool bIsPlayingEpgTag = bStarted && state->IsPlayingEpgTag(); + const bool bIsPlayingEncryptedStream = bStarted && state->IsPlayingEncryptedChannel(); + const bool bHasTVChannels = bStarted && mgr.ChannelGroups()->GetGroupAllTV()->HasChannels(); + const bool bHasRadioChannels = bStarted && mgr.ChannelGroups()->GetGroupAllRadio()->HasChannels(); + const bool bCanRecordPlayingChannel = bStarted && state->CanRecordOnPlayingChannel(); + const bool bIsRecordingPlayingChannel = bStarted && state->IsRecordingOnPlayingChannel(); + const bool bIsPlayingActiveRecording = bStarted && state->IsPlayingActiveRecording(); + const std::string strPlayingTVGroup = + (bStarted && bIsPlayingTV) ? state->GetActiveChannelGroup(false)->GroupName() : ""; + const std::string strPlayingRadioGroup = + (bStarted && bIsPlayingRadio) ? state->GetActiveChannelGroup(true)->GroupName() : ""; + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strPlayingClientName = strPlayingClientName; + m_bHasTVRecordings = bHasTVRecordings; + m_bHasRadioRecordings = bHasRadioRecordings; + m_bIsPlayingTV = bIsPlayingTV; + m_bIsPlayingRadio = bIsPlayingRadio; + m_bIsPlayingRecording = bIsPlayingRecording; + m_bIsPlayingEpgTag = bIsPlayingEpgTag; + m_bIsPlayingEncryptedStream = bIsPlayingEncryptedStream; + m_bHasTVChannels = bHasTVChannels; + m_bHasRadioChannels = bHasRadioChannels; + m_strPlayingTVGroup = strPlayingTVGroup; + m_strPlayingRadioGroup = strPlayingRadioGroup; + m_bCanRecordPlayingChannel = bCanRecordPlayingChannel; + m_bIsRecordingPlayingChannel = bIsRecordingPlayingChannel; + m_bIsPlayingActiveRecording = bIsPlayingActiveRecording; +} + +void CPVRGUIInfo::UpdateTimeshiftData() +{ + m_timesInfo.Update(); +} + +bool CPVRGUIInfo::InitCurrentItem(CFileItem* item) +{ + CServiceBroker::GetPVRManager().PublishEvent(PVREvent::CurrentItem); + return false; +} + +bool CPVRGUIInfo::GetLabel(std::string& value, + const CFileItem* item, + int contextWindow, + const CGUIInfo& info, + std::string* fallback) const +{ + return GetListItemAndPlayerLabel(item, info, value) || GetPVRLabel(item, info, value) || + GetRadioRDSLabel(item, info, value); +} + +namespace +{ +std::string GetAsLocalizedDateString(const CDateTime& datetime, bool bLongDate) +{ + return datetime.IsValid() ? datetime.GetAsLocalizedDate(bLongDate) : ""; +} + +std::string GetAsLocalizedTimeString(const CDateTime& datetime) +{ + return datetime.IsValid() ? datetime.GetAsLocalizedTime("", false) : ""; +} + +std::string GetAsLocalizedDateTimeString(const CDateTime& datetime) +{ + return datetime.IsValid() ? datetime.GetAsLocalizedDateTime(false, false) : ""; +} + +std::string GetEpgTagTitle(const std::shared_ptr<CPVREpgInfoTag>& epgTag) +{ + if (epgTag) + { + if (CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + return g_localizeStrings.Get(19266); // Parental locked + else if (!epgTag->Title().empty()) + return epgTag->Title(); + } + + if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool( + CSettings::SETTING_EPG_HIDENOINFOAVAILABLE)) + return g_localizeStrings.Get(19055); // no information available + + return {}; +} + +} // unnamed namespace + +bool CPVRGUIInfo::GetListItemAndPlayerLabel(const CFileItem* item, + const CGUIInfo& info, + std::string& strValue) const +{ + const std::shared_ptr<CPVRTimerInfoTag> timer = item->GetPVRTimerInfoTag(); + if (timer) + { + switch (info.m_info) + { + case LISTITEM_DATE: + strValue = timer->Summary(); + return true; + case LISTITEM_STARTDATE: + strValue = GetAsLocalizedDateString(timer->StartAsLocalTime(), true); + return true; + case LISTITEM_STARTTIME: + strValue = GetAsLocalizedTimeString(timer->StartAsLocalTime()); + return true; + case LISTITEM_ENDDATE: + strValue = GetAsLocalizedDateString(timer->EndAsLocalTime(), true); + return true; + case LISTITEM_ENDTIME: + strValue = GetAsLocalizedTimeString(timer->EndAsLocalTime()); + return true; + case LISTITEM_DURATION: + if (timer->GetDuration() > 0) + { + strValue = StringUtils::SecondsToTimeString(timer->GetDuration(), + static_cast<TIME_FORMAT>(info.GetData4())); + return true; + } + return false; + case LISTITEM_TITLE: + strValue = timer->Title(); + return true; + case LISTITEM_COMMENT: + strValue = + timer->GetStatus(CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow() == + WINDOW_RADIO_TIMER_RULES); + return true; + case LISTITEM_TIMERTYPE: + strValue = timer->GetTypeAsString(); + return true; + case LISTITEM_CHANNEL_NAME: + strValue = timer->ChannelName(); + return true; + case LISTITEM_EPG_EVENT_TITLE: + case LISTITEM_EPG_EVENT_ICON: + case LISTITEM_GENRE: + case LISTITEM_PLOT: + case LISTITEM_PLOT_OUTLINE: + case LISTITEM_ORIGINALTITLE: + case LISTITEM_YEAR: + case LISTITEM_SEASON: + case LISTITEM_EPISODE: + case LISTITEM_EPISODENAME: + case LISTITEM_DIRECTOR: + case LISTITEM_CHANNEL_NUMBER: + case LISTITEM_PREMIERED: + break; // obtain value from channel/epg + default: + return false; + } + } + + const std::shared_ptr<CPVRRecording> recording(item->GetPVRRecordingInfoTag()); + if (recording) + { + // Note: CPVRRecoding is derived from CVideoInfoTag. All base class properties will be handled + // by CVideoGUIInfoProvider. Only properties introduced by CPVRRecording need to be handled here. + switch (info.m_info) + { + case LISTITEM_DATE: + strValue = GetAsLocalizedDateTimeString(recording->RecordingTimeAsLocalTime()); + return true; + case LISTITEM_STARTDATE: + strValue = GetAsLocalizedDateString(recording->RecordingTimeAsLocalTime(), true); + return true; + case VIDEOPLAYER_STARTTIME: + case LISTITEM_STARTTIME: + strValue = GetAsLocalizedTimeString(recording->RecordingTimeAsLocalTime()); + return true; + case LISTITEM_ENDDATE: + strValue = GetAsLocalizedDateString(recording->EndTimeAsLocalTime(), true); + return true; + case VIDEOPLAYER_ENDTIME: + case LISTITEM_ENDTIME: + strValue = GetAsLocalizedTimeString(recording->EndTimeAsLocalTime()); + return true; + case LISTITEM_EXPIRATION_DATE: + if (recording->HasExpirationTime()) + { + strValue = GetAsLocalizedDateString(recording->ExpirationTimeAsLocalTime(), false); + return true; + } + break; + case LISTITEM_EXPIRATION_TIME: + if (recording->HasExpirationTime()) + { + strValue = GetAsLocalizedTimeString(recording->ExpirationTimeAsLocalTime()); + ; + return true; + } + break; + case VIDEOPLAYER_EPISODENAME: + case LISTITEM_EPISODENAME: + strValue = recording->EpisodeName(); + // fixup multiline episode name strings (which do not fit in any way in our GUI) + StringUtils::Replace(strValue, "\n", ", "); + return true; + case VIDEOPLAYER_CHANNEL_NAME: + case LISTITEM_CHANNEL_NAME: + strValue = recording->ChannelName(); + if (strValue.empty()) + { + if (recording->ProviderName().empty()) + { + const auto& provider = recording->GetProvider(); + strValue = provider->GetName(); + } + else + { + strValue = recording->ProviderName(); + } + } + return true; + case VIDEOPLAYER_CHANNEL_NUMBER: + case LISTITEM_CHANNEL_NUMBER: + { + const std::shared_ptr<CPVRChannelGroupMember> groupMember = + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember(*item); + if (groupMember) + { + strValue = groupMember->ChannelNumber().FormattedChannelNumber(); + return true; + } + break; + } + case LISTITEM_ICON: + if (recording->ClientIconPath().empty() && recording->ClientThumbnailPath().empty() && + // Only use a fallback if there is more than a single provider available + // Note: an add-on itself is a provider, hence we don't use > 0 + CServiceBroker::GetPVRManager().Providers()->GetNumProviders() > 1 && + !recording->Channel()) + { + auto provider = recording->GetProvider(); + if (!provider->GetIconPath().empty()) + { + strValue = provider->GetIconPath(); + return true; + } + } + return false; + case VIDEOPLAYER_CHANNEL_GROUP: + { + std::unique_lock<CCriticalSection> lock(m_critSection); + strValue = recording->IsRadio() ? m_strPlayingRadioGroup : m_strPlayingTVGroup; + return true; + } + case VIDEOPLAYER_PREMIERED: + case LISTITEM_PREMIERED: + if (recording->FirstAired().IsValid()) + { + strValue = recording->FirstAired().GetAsLocalizedDate(); + return true; + } + else if (recording->HasYear()) + { + strValue = std::to_string(recording->GetYear()); + return true; + } + return false; + case LISTITEM_SIZE: + if (recording->GetSizeInBytes() > 0) + { + strValue = StringUtils::SizeToString(recording->GetSizeInBytes()); + return true; + } + return false; + } + return false; + } + + const std::shared_ptr<CPVREpgSearchFilter> filter = item->GetEPGSearchFilter(); + if (filter) + { + switch (info.m_info) + { + case LISTITEM_DATE: + { + CDateTime lastExecLocal; + lastExecLocal.SetFromUTCDateTime(filter->GetLastExecutedDateTime()); + strValue = GetAsLocalizedDateTimeString(lastExecLocal); + if (strValue.empty()) + strValue = g_localizeStrings.Get(10006); // "N/A" + return true; + } + } + return false; + } + + std::shared_ptr<CPVREpgInfoTag> epgTag; + std::shared_ptr<CPVRChannel> channel; + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + CPVRItem pvrItem(item); + channel = pvrItem.GetChannel(); + + switch (info.m_info) + { + case VIDEOPLAYER_NEXT_TITLE: + case VIDEOPLAYER_NEXT_GENRE: + case VIDEOPLAYER_NEXT_PLOT: + case VIDEOPLAYER_NEXT_PLOT_OUTLINE: + case VIDEOPLAYER_NEXT_STARTTIME: + case VIDEOPLAYER_NEXT_ENDTIME: + case VIDEOPLAYER_NEXT_DURATION: + case LISTITEM_NEXT_TITLE: + case LISTITEM_NEXT_GENRE: + case LISTITEM_NEXT_PLOT: + case LISTITEM_NEXT_PLOT_OUTLINE: + case LISTITEM_NEXT_STARTDATE: + case LISTITEM_NEXT_STARTTIME: + case LISTITEM_NEXT_ENDDATE: + case LISTITEM_NEXT_ENDTIME: + case LISTITEM_NEXT_DURATION: + // next playing event + epgTag = pvrItem.GetNextEpgInfoTag(); + break; + default: + // now playing event + epgTag = pvrItem.GetEpgInfoTag(); + break; + } + + switch (info.m_info) + { + // special handling for channels without epg or with radio rds data + case PLAYER_TITLE: + case LISTITEM_TITLE: + case VIDEOPLAYER_NEXT_TITLE: + case LISTITEM_NEXT_TITLE: + case LISTITEM_EPG_EVENT_TITLE: + // Note: in difference to LISTITEM_TITLE, LISTITEM_EPG_EVENT_TITLE returns the title + // associated with the epg event of a timer, if any, and not the title of the timer. + strValue = GetEpgTagTitle(epgTag); + return true; + } + } + + if (epgTag) + { + switch (info.m_info) + { + case VIDEOPLAYER_GENRE: + case LISTITEM_GENRE: + case VIDEOPLAYER_NEXT_GENRE: + case LISTITEM_NEXT_GENRE: + strValue = epgTag->GetGenresLabel(); + return true; + case VIDEOPLAYER_PLOT: + case LISTITEM_PLOT: + case VIDEOPLAYER_NEXT_PLOT: + case LISTITEM_NEXT_PLOT: + if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + strValue = epgTag->Plot(); + return true; + case VIDEOPLAYER_PLOT_OUTLINE: + case LISTITEM_PLOT_OUTLINE: + case VIDEOPLAYER_NEXT_PLOT_OUTLINE: + case LISTITEM_NEXT_PLOT_OUTLINE: + if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + strValue = epgTag->PlotOutline(); + return true; + case LISTITEM_DATE: + strValue = GetAsLocalizedDateTimeString(epgTag->StartAsLocalTime()); + return true; + case LISTITEM_STARTDATE: + case LISTITEM_NEXT_STARTDATE: + strValue = GetAsLocalizedDateString(epgTag->StartAsLocalTime(), true); + return true; + case VIDEOPLAYER_STARTTIME: + case VIDEOPLAYER_NEXT_STARTTIME: + case LISTITEM_STARTTIME: + case LISTITEM_NEXT_STARTTIME: + strValue = GetAsLocalizedTimeString(epgTag->StartAsLocalTime()); + return true; + case LISTITEM_ENDDATE: + case LISTITEM_NEXT_ENDDATE: + strValue = GetAsLocalizedDateString(epgTag->EndAsLocalTime(), true); + return true; + case VIDEOPLAYER_ENDTIME: + case VIDEOPLAYER_NEXT_ENDTIME: + case LISTITEM_ENDTIME: + case LISTITEM_NEXT_ENDTIME: + strValue = GetAsLocalizedTimeString(epgTag->EndAsLocalTime()); + return true; + // note: for some reason, there is no VIDEOPLAYER_DURATION + case LISTITEM_DURATION: + case VIDEOPLAYER_NEXT_DURATION: + case LISTITEM_NEXT_DURATION: + if (epgTag->GetDuration() > 0) + { + strValue = StringUtils::SecondsToTimeString(epgTag->GetDuration(), + static_cast<TIME_FORMAT>(info.GetData4())); + return true; + } + return false; + case VIDEOPLAYER_IMDBNUMBER: + case LISTITEM_IMDBNUMBER: + strValue = epgTag->IMDBNumber(); + return true; + case VIDEOPLAYER_ORIGINALTITLE: + case LISTITEM_ORIGINALTITLE: + if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + strValue = epgTag->OriginalTitle(); + return true; + case VIDEOPLAYER_YEAR: + case LISTITEM_YEAR: + if (epgTag->Year() > 0) + { + strValue = std::to_string(epgTag->Year()); + return true; + } + return false; + case VIDEOPLAYER_SEASON: + case LISTITEM_SEASON: + if (epgTag->SeriesNumber() >= 0) + { + strValue = std::to_string(epgTag->SeriesNumber()); + return true; + } + return false; + case VIDEOPLAYER_EPISODE: + case LISTITEM_EPISODE: + if (epgTag->EpisodeNumber() >= 0) + { + strValue = std::to_string(epgTag->EpisodeNumber()); + return true; + } + return false; + case VIDEOPLAYER_EPISODENAME: + case LISTITEM_EPISODENAME: + if (!CServiceBroker::GetPVRManager().IsParentalLocked(epgTag)) + { + strValue = epgTag->EpisodeName(); + // fixup multiline episode name strings (which do not fit in any way in our GUI) + StringUtils::Replace(strValue, "\n", ", "); + } + return true; + case VIDEOPLAYER_CAST: + case LISTITEM_CAST: + strValue = epgTag->GetCastLabel(); + return true; + case VIDEOPLAYER_DIRECTOR: + case LISTITEM_DIRECTOR: + strValue = epgTag->GetDirectorsLabel(); + return true; + case VIDEOPLAYER_WRITER: + case LISTITEM_WRITER: + strValue = epgTag->GetWritersLabel(); + return true; + case LISTITEM_EPG_EVENT_ICON: + strValue = epgTag->IconPath(); + return true; + case VIDEOPLAYER_PARENTAL_RATING: + case LISTITEM_PARENTAL_RATING: + if (epgTag->ParentalRating() > 0) + { + strValue = std::to_string(epgTag->ParentalRating()); + return true; + } + return false; + case VIDEOPLAYER_PREMIERED: + case LISTITEM_PREMIERED: + if (epgTag->FirstAired().IsValid()) + { + strValue = epgTag->FirstAired().GetAsLocalizedDate(); + return true; + } + else if (epgTag->Year() > 0) + { + strValue = std::to_string(epgTag->Year()); + return true; + } + return false; + case VIDEOPLAYER_RATING: + case LISTITEM_RATING: + { + int iStarRating = epgTag->StarRating(); + if (iStarRating > 0) + { + strValue = StringUtils::FormatNumber(iStarRating); + return true; + } + return false; + } + } + } + + if (channel) + { + switch (info.m_info) + { + case MUSICPLAYER_CHANNEL_NAME: + { + const std::shared_ptr<CPVRRadioRDSInfoTag> rdsTag = channel->GetRadioRDSInfoTag(); + if (rdsTag) + { + strValue = rdsTag->GetProgStation(); + if (!strValue.empty()) + return true; + } + // fall-thru is intended + [[fallthrough]]; + } + case VIDEOPLAYER_CHANNEL_NAME: + case LISTITEM_CHANNEL_NAME: + strValue = channel->ChannelName(); + return true; + case MUSICPLAYER_CHANNEL_NUMBER: + case VIDEOPLAYER_CHANNEL_NUMBER: + case LISTITEM_CHANNEL_NUMBER: + { + auto groupMember = item->GetPVRChannelGroupMemberInfoTag(); + if (!groupMember) + groupMember = + CServiceBroker::GetPVRManager().Get<PVR::GUI::Channels>().GetChannelGroupMember( + *item); + if (groupMember) + { + strValue = groupMember->ChannelNumber().FormattedChannelNumber(); + return true; + } + break; + } + case MUSICPLAYER_CHANNEL_GROUP: + case VIDEOPLAYER_CHANNEL_GROUP: + { + std::unique_lock<CCriticalSection> lock(m_critSection); + strValue = channel->IsRadio() ? m_strPlayingRadioGroup : m_strPlayingTVGroup; + return true; + } + } + } + + return false; +} + +bool CPVRGUIInfo::GetPVRLabel(const CFileItem* item, + const CGUIInfo& info, + std::string& strValue) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + switch (info.m_info) + { + case PVR_EPG_EVENT_ICON: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + if (epgTag) + { + strValue = epgTag->IconPath(); + } + return true; + } + case PVR_EPG_EVENT_DURATION: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + strValue = m_timesInfo.GetEpgEventDuration(epgTag, static_cast<TIME_FORMAT>(info.GetData1())); + return true; + } + case PVR_EPG_EVENT_ELAPSED_TIME: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + strValue = + m_timesInfo.GetEpgEventElapsedTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1())); + return true; + } + case PVR_EPG_EVENT_REMAINING_TIME: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + strValue = + m_timesInfo.GetEpgEventRemainingTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1())); + return true; + } + case PVR_EPG_EVENT_FINISH_TIME: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + strValue = + m_timesInfo.GetEpgEventFinishTime(epgTag, static_cast<TIME_FORMAT>(info.GetData1())); + return true; + } + case PVR_TIMESHIFT_START_TIME: + strValue = m_timesInfo.GetTimeshiftStartTime(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_END_TIME: + strValue = m_timesInfo.GetTimeshiftEndTime(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_PLAY_TIME: + strValue = m_timesInfo.GetTimeshiftPlayTime(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_OFFSET: + strValue = m_timesInfo.GetTimeshiftOffset(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_PROGRESS_DURATION: + strValue = + m_timesInfo.GetTimeshiftProgressDuration(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_PROGRESS_START_TIME: + strValue = + m_timesInfo.GetTimeshiftProgressStartTime(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_TIMESHIFT_PROGRESS_END_TIME: + strValue = m_timesInfo.GetTimeshiftProgressEndTime(static_cast<TIME_FORMAT>(info.GetData1())); + return true; + case PVR_EPG_EVENT_SEEK_TIME: + { + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + strValue = m_timesInfo.GetEpgEventSeekTime(appPlayer->GetSeekHandler().GetSeekSize(), + static_cast<TIME_FORMAT>(info.GetData1())); + return true; + } + case PVR_NOW_RECORDING_TITLE: + strValue = m_anyTimersInfo.GetActiveTimerTitle(); + return true; + case PVR_NOW_RECORDING_CHANNEL: + strValue = m_anyTimersInfo.GetActiveTimerChannelName(); + return true; + case PVR_NOW_RECORDING_CHAN_ICO: + strValue = m_anyTimersInfo.GetActiveTimerChannelIcon(); + return true; + case PVR_NOW_RECORDING_DATETIME: + strValue = m_anyTimersInfo.GetActiveTimerDateTime(); + return true; + case PVR_NEXT_RECORDING_TITLE: + strValue = m_anyTimersInfo.GetNextTimerTitle(); + return true; + case PVR_NEXT_RECORDING_CHANNEL: + strValue = m_anyTimersInfo.GetNextTimerChannelName(); + return true; + case PVR_NEXT_RECORDING_CHAN_ICO: + strValue = m_anyTimersInfo.GetNextTimerChannelIcon(); + return true; + case PVR_NEXT_RECORDING_DATETIME: + strValue = m_anyTimersInfo.GetNextTimerDateTime(); + return true; + case PVR_TV_NOW_RECORDING_TITLE: + strValue = m_tvTimersInfo.GetActiveTimerTitle(); + return true; + case PVR_TV_NOW_RECORDING_CHANNEL: + strValue = m_tvTimersInfo.GetActiveTimerChannelName(); + return true; + case PVR_TV_NOW_RECORDING_CHAN_ICO: + strValue = m_tvTimersInfo.GetActiveTimerChannelIcon(); + return true; + case PVR_TV_NOW_RECORDING_DATETIME: + strValue = m_tvTimersInfo.GetActiveTimerDateTime(); + return true; + case PVR_TV_NEXT_RECORDING_TITLE: + strValue = m_tvTimersInfo.GetNextTimerTitle(); + return true; + case PVR_TV_NEXT_RECORDING_CHANNEL: + strValue = m_tvTimersInfo.GetNextTimerChannelName(); + return true; + case PVR_TV_NEXT_RECORDING_CHAN_ICO: + strValue = m_tvTimersInfo.GetNextTimerChannelIcon(); + return true; + case PVR_TV_NEXT_RECORDING_DATETIME: + strValue = m_tvTimersInfo.GetNextTimerDateTime(); + return true; + case PVR_RADIO_NOW_RECORDING_TITLE: + strValue = m_radioTimersInfo.GetActiveTimerTitle(); + return true; + case PVR_RADIO_NOW_RECORDING_CHANNEL: + strValue = m_radioTimersInfo.GetActiveTimerChannelName(); + return true; + case PVR_RADIO_NOW_RECORDING_CHAN_ICO: + strValue = m_radioTimersInfo.GetActiveTimerChannelIcon(); + return true; + case PVR_RADIO_NOW_RECORDING_DATETIME: + strValue = m_radioTimersInfo.GetActiveTimerDateTime(); + return true; + case PVR_RADIO_NEXT_RECORDING_TITLE: + strValue = m_radioTimersInfo.GetNextTimerTitle(); + return true; + case PVR_RADIO_NEXT_RECORDING_CHANNEL: + strValue = m_radioTimersInfo.GetNextTimerChannelName(); + return true; + case PVR_RADIO_NEXT_RECORDING_CHAN_ICO: + strValue = m_radioTimersInfo.GetNextTimerChannelIcon(); + return true; + case PVR_RADIO_NEXT_RECORDING_DATETIME: + strValue = m_radioTimersInfo.GetNextTimerDateTime(); + return true; + case PVR_NEXT_TIMER: + strValue = m_anyTimersInfo.GetNextTimer(); + return true; + case PVR_ACTUAL_STREAM_SIG: + CharInfoSignal(strValue); + return true; + case PVR_ACTUAL_STREAM_SNR: + CharInfoSNR(strValue); + return true; + case PVR_ACTUAL_STREAM_BER: + CharInfoBER(strValue); + return true; + case PVR_ACTUAL_STREAM_UNC: + CharInfoUNC(strValue); + return true; + case PVR_ACTUAL_STREAM_CLIENT: + CharInfoPlayingClientName(strValue); + return true; + case PVR_ACTUAL_STREAM_DEVICE: + CharInfoFrontendName(strValue); + return true; + case PVR_ACTUAL_STREAM_STATUS: + CharInfoFrontendStatus(strValue); + return true; + case PVR_ACTUAL_STREAM_CRYPTION: + CharInfoEncryption(strValue); + return true; + case PVR_ACTUAL_STREAM_SERVICE: + CharInfoService(strValue); + return true; + case PVR_ACTUAL_STREAM_MUX: + CharInfoMux(strValue); + return true; + case PVR_ACTUAL_STREAM_PROVIDER: + CharInfoProvider(strValue); + return true; + case PVR_BACKEND_NAME: + CharInfoBackendName(strValue); + return true; + case PVR_BACKEND_VERSION: + CharInfoBackendVersion(strValue); + return true; + case PVR_BACKEND_HOST: + CharInfoBackendHost(strValue); + return true; + case PVR_BACKEND_DISKSPACE: + CharInfoBackendDiskspace(strValue); + return true; + case PVR_BACKEND_PROVIDERS: + CharInfoBackendProviders(strValue); + return true; + case PVR_BACKEND_CHANNEL_GROUPS: + CharInfoBackendChannelGroups(strValue); + return true; + case PVR_BACKEND_CHANNELS: + CharInfoBackendChannels(strValue); + return true; + case PVR_BACKEND_TIMERS: + CharInfoBackendTimers(strValue); + return true; + case PVR_BACKEND_RECORDINGS: + CharInfoBackendRecordings(strValue); + return true; + case PVR_BACKEND_DELETED_RECORDINGS: + CharInfoBackendDeletedRecordings(strValue); + return true; + case PVR_BACKEND_NUMBER: + CharInfoBackendNumber(strValue); + return true; + case PVR_TOTAL_DISKSPACE: + CharInfoTotalDiskSpace(strValue); + return true; + case PVR_CHANNEL_NUMBER_INPUT: + strValue = m_channelNumberInput; + return true; + } + + return false; +} + +bool CPVRGUIInfo::GetRadioRDSLabel(const CFileItem* item, + const CGUIInfo& info, + std::string& strValue) const +{ + if (!item->HasPVRChannelInfoTag()) + return false; + + const std::shared_ptr<CPVRRadioRDSInfoTag> tag = + item->GetPVRChannelInfoTag()->GetRadioRDSInfoTag(); + if (tag) + { + switch (info.m_info) + { + case RDS_CHANNEL_COUNTRY: + strValue = tag->GetCountry(); + return true; + case RDS_TITLE: + strValue = tag->GetTitle(); + return true; + case RDS_ARTIST: + strValue = tag->GetArtist(); + return true; + case RDS_BAND: + strValue = tag->GetBand(); + return true; + case RDS_COMPOSER: + strValue = tag->GetComposer(); + return true; + case RDS_CONDUCTOR: + strValue = tag->GetConductor(); + return true; + case RDS_ALBUM: + strValue = tag->GetAlbum(); + return true; + case RDS_ALBUM_TRACKNUMBER: + if (tag->GetAlbumTrackNumber() > 0) + { + strValue = std::to_string(tag->GetAlbumTrackNumber()); + return true; + } + break; + case RDS_GET_RADIO_STYLE: + strValue = tag->GetRadioStyle(); + return true; + case RDS_COMMENT: + strValue = tag->GetComment(); + return true; + case RDS_INFO_NEWS: + strValue = tag->GetInfoNews(); + return true; + case RDS_INFO_NEWS_LOCAL: + strValue = tag->GetInfoNewsLocal(); + return true; + case RDS_INFO_STOCK: + strValue = tag->GetInfoStock(); + return true; + case RDS_INFO_STOCK_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoStock().size())); + return true; + case RDS_INFO_SPORT: + strValue = tag->GetInfoSport(); + return true; + case RDS_INFO_SPORT_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoSport().size())); + return true; + case RDS_INFO_LOTTERY: + strValue = tag->GetInfoLottery(); + return true; + case RDS_INFO_LOTTERY_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoLottery().size())); + return true; + case RDS_INFO_WEATHER: + strValue = tag->GetInfoWeather(); + return true; + case RDS_INFO_WEATHER_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoWeather().size())); + return true; + case RDS_INFO_HOROSCOPE: + strValue = tag->GetInfoHoroscope(); + return true; + case RDS_INFO_HOROSCOPE_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoHoroscope().size())); + return true; + case RDS_INFO_CINEMA: + strValue = tag->GetInfoCinema(); + return true; + case RDS_INFO_CINEMA_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoCinema().size())); + return true; + case RDS_INFO_OTHER: + strValue = tag->GetInfoOther(); + return true; + case RDS_INFO_OTHER_SIZE: + strValue = std::to_string(static_cast<int>(tag->GetInfoOther().size())); + return true; + case RDS_PROG_HOST: + strValue = tag->GetProgHost(); + return true; + case RDS_PROG_EDIT_STAFF: + strValue = tag->GetEditorialStaff(); + return true; + case RDS_PROG_HOMEPAGE: + strValue = tag->GetProgWebsite(); + return true; + case RDS_PROG_STYLE: + strValue = tag->GetProgStyle(); + return true; + case RDS_PHONE_HOTLINE: + strValue = tag->GetPhoneHotline(); + return true; + case RDS_PHONE_STUDIO: + strValue = tag->GetPhoneStudio(); + return true; + case RDS_SMS_STUDIO: + strValue = tag->GetSMSStudio(); + return true; + case RDS_EMAIL_HOTLINE: + strValue = tag->GetEMailHotline(); + return true; + case RDS_EMAIL_STUDIO: + strValue = tag->GetEMailStudio(); + return true; + case RDS_PROG_STATION: + strValue = tag->GetProgStation(); + return true; + case RDS_PROG_NOW: + strValue = tag->GetProgNow(); + return true; + case RDS_PROG_NEXT: + strValue = tag->GetProgNext(); + return true; + case RDS_AUDIO_LANG: + strValue = tag->GetLanguage(); + return true; + case RDS_GET_RADIOTEXT_LINE: + strValue = tag->GetRadioText(info.GetData1()); + return true; + } + } + return false; +} + +bool CPVRGUIInfo::GetFallbackLabel(std::string& value, + const CFileItem* item, + int contextWindow, + const CGUIInfo& info, + std::string* fallback) +{ + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + switch (info.m_info) + { + ///////////////////////////////////////////////////////////////////////////////////////////// + // VIDEOPLAYER_*, MUSICPLAYER_* + ///////////////////////////////////////////////////////////////////////////////////////////// + case VIDEOPLAYER_TITLE: + case MUSICPLAYER_TITLE: + value = GetEpgTagTitle(CPVRItem(item).GetEpgInfoTag()); + return !value.empty(); + default: + break; + } + } + return false; +} + +bool CPVRGUIInfo::GetInt(int& value, + const CGUIListItem* item, + int contextWindow, + const CGUIInfo& info) const +{ + if (!item->IsFileItem()) + return false; + + const CFileItem* fitem = static_cast<const CFileItem*>(item); + return GetListItemAndPlayerInt(fitem, info, value) || GetPVRInt(fitem, info, value); +} + +bool CPVRGUIInfo::GetListItemAndPlayerInt(const CFileItem* item, + const CGUIInfo& info, + int& iValue) const +{ + switch (info.m_info) + { + case LISTITEM_PROGRESS: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + if (epgTag) + iValue = static_cast<int>(epgTag->ProgressPercentage()); + } + return true; + } + return false; +} + +bool CPVRGUIInfo::GetPVRInt(const CFileItem* item, const CGUIInfo& info, int& iValue) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + switch (info.m_info) + { + case PVR_EPG_EVENT_DURATION: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + iValue = m_timesInfo.GetEpgEventDuration(epgTag); + return true; + } + case PVR_EPG_EVENT_PROGRESS: + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = + (item->IsPVRChannel() || item->IsEPG()) ? CPVRItem(item).GetEpgInfoTag() : nullptr; + iValue = m_timesInfo.GetEpgEventProgress(epgTag); + return true; + } + case PVR_TIMESHIFT_PROGRESS: + iValue = m_timesInfo.GetTimeshiftProgress(); + return true; + case PVR_TIMESHIFT_PROGRESS_DURATION: + iValue = m_timesInfo.GetTimeshiftProgressDuration(); + return true; + case PVR_TIMESHIFT_PROGRESS_PLAY_POS: + iValue = m_timesInfo.GetTimeshiftProgressPlayPosition(); + return true; + case PVR_TIMESHIFT_PROGRESS_EPG_START: + iValue = m_timesInfo.GetTimeshiftProgressEpgStart(); + return true; + case PVR_TIMESHIFT_PROGRESS_EPG_END: + iValue = m_timesInfo.GetTimeshiftProgressEpgEnd(); + return true; + case PVR_TIMESHIFT_PROGRESS_BUFFER_START: + iValue = m_timesInfo.GetTimeshiftProgressBufferStart(); + return true; + case PVR_TIMESHIFT_PROGRESS_BUFFER_END: + iValue = m_timesInfo.GetTimeshiftProgressBufferEnd(); + return true; + case PVR_TIMESHIFT_SEEKBAR: + iValue = GetTimeShiftSeekPercent(); + return true; + case PVR_ACTUAL_STREAM_SIG_PROGR: + iValue = std::lrintf(static_cast<float>(m_qualityInfo.iSignal) / 0xFFFF * 100); + return true; + case PVR_ACTUAL_STREAM_SNR_PROGR: + iValue = std::lrintf(static_cast<float>(m_qualityInfo.iSNR) / 0xFFFF * 100); + return true; + case PVR_BACKEND_DISKSPACE_PROGR: + if (m_iBackendDiskTotal > 0) + iValue = std::lrintf(static_cast<float>(m_iBackendDiskUsed) / m_iBackendDiskTotal * 100); + else + iValue = 0xFF; + return true; + } + return false; +} + +bool CPVRGUIInfo::GetBool(bool& value, + const CGUIListItem* item, + int contextWindow, + const CGUIInfo& info) const +{ + if (!item->IsFileItem()) + return false; + + const CFileItem* fitem = static_cast<const CFileItem*>(item); + return GetListItemAndPlayerBool(fitem, info, value) || GetPVRBool(fitem, info, value) || + GetRadioRDSBool(fitem, info, value); +} + +bool CPVRGUIInfo::GetListItemAndPlayerBool(const CFileItem* item, + const CGUIInfo& info, + bool& bValue) const +{ + switch (info.m_info) + { + case LISTITEM_HASARCHIVE: + if (item->IsPVRChannel()) + { + bValue = item->GetPVRChannelInfoTag()->HasArchive(); + return true; + } + break; + case LISTITEM_ISPLAYABLE: + if (item->IsEPG()) + { + bValue = item->GetEPGInfoTag()->IsPlayable(); + return true; + } + break; + case LISTITEM_ISRECORDING: + if (item->IsPVRChannel()) + { + bValue = CServiceBroker::GetPVRManager().Timers()->IsRecordingOnChannel( + *item->GetPVRChannelInfoTag()); + return true; + } + else if (item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->IsRecording(); + return true; + } + else if (item->IsPVRRecording()) + { + bValue = item->GetPVRRecordingInfoTag()->IsInProgress(); + return true; + } + break; + case LISTITEM_INPROGRESS: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + if (epgTag) + bValue = epgTag->IsActive(); + return true; + } + break; + case LISTITEM_HASTIMER: + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = true; + return true; + } + break; + case LISTITEM_HASTIMERSCHEDULE: + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->HasParent(); + return true; + } + break; + case LISTITEM_HASREMINDER: + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->IsReminder(); + return true; + } + break; + case LISTITEM_HASREMINDERRULE: + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->IsReminder() && timer->HasParent(); + return true; + } + break; + case LISTITEM_TIMERISACTIVE: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->IsActive(); + break; + } + break; + case LISTITEM_TIMERHASCONFLICT: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = timer->HasConflict(); + return true; + } + break; + case LISTITEM_TIMERHASERROR: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVRTimerInfoTag> timer = CPVRItem(item).GetTimerInfoTag(); + if (timer) + bValue = (timer->IsBroken() && !timer->HasConflict()); + return true; + } + break; + case LISTITEM_HASRECORDING: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + if (epgTag) + bValue = !!CServiceBroker::GetPVRManager().Recordings()->GetRecordingForEpgTag(epgTag); + return true; + } + break; + case LISTITEM_HAS_EPG: + if (item->IsPVRChannel() || item->IsEPG() || item->IsPVRTimer()) + { + const std::shared_ptr<CPVREpgInfoTag> epgTag = CPVRItem(item).GetEpgInfoTag(); + bValue = (epgTag != nullptr); + return true; + } + break; + case LISTITEM_ISENCRYPTED: + if (item->IsPVRChannel() || item->IsEPG()) + { + const std::shared_ptr<CPVRChannel> channel = CPVRItem(item).GetChannel(); + if (channel) + bValue = channel->IsEncrypted(); + return true; + } + break; + case LISTITEM_IS_NEW: + if (item->IsEPG()) + { + if (item->GetEPGInfoTag()) + { + bValue = item->GetEPGInfoTag()->IsNew(); + return true; + } + } + else if (item->IsPVRRecording()) + { + bValue = item->GetPVRRecordingInfoTag()->IsNew(); + return true; + } + else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag()) + { + bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsNew(); + return true; + } + else if (item->IsPVRChannel()) + { + const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow(); + bValue = epgNow ? epgNow->IsNew() : false; + return true; + } + break; + case LISTITEM_IS_PREMIERE: + if (item->IsEPG()) + { + bValue = item->GetEPGInfoTag()->IsPremiere(); + return true; + } + else if (item->IsPVRRecording()) + { + bValue = item->GetPVRRecordingInfoTag()->IsPremiere(); + return true; + } + else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag()) + { + bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsPremiere(); + return true; + } + else if (item->IsPVRChannel()) + { + const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow(); + bValue = epgNow ? epgNow->IsPremiere() : false; + return true; + } + break; + case LISTITEM_IS_FINALE: + if (item->IsEPG()) + { + bValue = item->GetEPGInfoTag()->IsFinale(); + return true; + } + else if (item->IsPVRRecording()) + { + bValue = item->GetPVRRecordingInfoTag()->IsFinale(); + return true; + } + else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag()) + { + bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsFinale(); + return true; + } + else if (item->IsPVRChannel()) + { + const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow(); + bValue = epgNow ? epgNow->IsFinale() : false; + return true; + } + break; + case LISTITEM_IS_LIVE: + if (item->IsEPG()) + { + bValue = item->GetEPGInfoTag()->IsLive(); + return true; + } + else if (item->IsPVRRecording()) + { + bValue = item->GetPVRRecordingInfoTag()->IsLive(); + return true; + } + else if (item->IsPVRTimer() && item->GetPVRTimerInfoTag()->GetEpgInfoTag()) + { + bValue = item->GetPVRTimerInfoTag()->GetEpgInfoTag()->IsLive(); + return true; + } + else if (item->IsPVRChannel()) + { + const std::shared_ptr<CPVREpgInfoTag> epgNow = item->GetPVRChannelInfoTag()->GetEPGNow(); + bValue = epgNow ? epgNow->IsLive() : false; + return true; + } + break; + case MUSICPLAYER_CONTENT: + case VIDEOPLAYER_CONTENT: + if (item->IsPVRChannel()) + { + bValue = StringUtils::EqualsNoCase(info.GetData3(), "livetv"); + return bValue; // if no match for this provider, other providers shall be asked. + } + break; + case VIDEOPLAYER_HAS_INFO: + if (item->IsPVRChannel()) + { + bValue = !item->GetPVRChannelInfoTag()->ChannelName().empty(); + return true; + } + break; + case VIDEOPLAYER_HAS_EPG: + if (item->IsPVRChannel()) + { + bValue = (item->GetPVRChannelInfoTag()->GetEPGNow() != nullptr); + return true; + } + break; + case VIDEOPLAYER_CAN_RESUME_LIVE_TV: + if (item->IsPVRRecording()) + { + const std::shared_ptr<CPVRRecording> recording = item->GetPVRRecordingInfoTag(); + const std::shared_ptr<CPVREpg> epg = + recording->Channel() ? recording->Channel()->GetEPG() : nullptr; + const std::shared_ptr<CPVREpgInfoTag> epgTag = + CServiceBroker::GetPVRManager().EpgContainer().GetTagById(epg, + recording->BroadcastUid()); + bValue = (epgTag && epgTag->IsActive()); + return true; + } + break; + case PLAYER_IS_CHANNEL_PREVIEW_ACTIVE: + if (item->IsPVRChannel()) + { + if (m_previewAndPlayerShowInfo) + { + bValue = true; + } + else + { + bValue = !m_videoInfo.valid; + if (bValue && item->GetPVRChannelInfoTag()->IsRadio()) + bValue = !m_audioInfo.valid; + } + return true; + } + break; + } + return false; +} + +bool CPVRGUIInfo::GetPVRBool(const CFileItem* item, const CGUIInfo& info, bool& bValue) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + switch (info.m_info) + { + case PVR_IS_RECORDING: + bValue = m_anyTimersInfo.HasRecordingTimers(); + return true; + case PVR_IS_RECORDING_TV: + bValue = m_tvTimersInfo.HasRecordingTimers(); + return true; + case PVR_IS_RECORDING_RADIO: + bValue = m_radioTimersInfo.HasRecordingTimers(); + return true; + case PVR_HAS_TIMER: + bValue = m_anyTimersInfo.HasTimers(); + return true; + case PVR_HAS_TV_TIMER: + bValue = m_tvTimersInfo.HasTimers(); + return true; + case PVR_HAS_RADIO_TIMER: + bValue = m_radioTimersInfo.HasTimers(); + return true; + case PVR_HAS_TV_CHANNELS: + bValue = m_bHasTVChannels; + return true; + case PVR_HAS_RADIO_CHANNELS: + bValue = m_bHasRadioChannels; + return true; + case PVR_HAS_NONRECORDING_TIMER: + bValue = m_anyTimersInfo.HasNonRecordingTimers(); + return true; + case PVR_HAS_NONRECORDING_TV_TIMER: + bValue = m_tvTimersInfo.HasNonRecordingTimers(); + return true; + case PVR_HAS_NONRECORDING_RADIO_TIMER: + bValue = m_radioTimersInfo.HasNonRecordingTimers(); + return true; + case PVR_IS_PLAYING_TV: + bValue = m_bIsPlayingTV; + return true; + case PVR_IS_PLAYING_RADIO: + bValue = m_bIsPlayingRadio; + return true; + case PVR_IS_PLAYING_RECORDING: + bValue = m_bIsPlayingRecording; + return true; + case PVR_IS_PLAYING_EPGTAG: + bValue = m_bIsPlayingEpgTag; + return true; + case PVR_ACTUAL_STREAM_ENCRYPTED: + bValue = m_bIsPlayingEncryptedStream; + return true; + case PVR_IS_TIMESHIFTING: + bValue = m_timesInfo.IsTimeshifting(); + return true; + case PVR_CAN_RECORD_PLAYING_CHANNEL: + bValue = m_bCanRecordPlayingChannel; + return true; + case PVR_IS_RECORDING_PLAYING_CHANNEL: + bValue = m_bIsRecordingPlayingChannel; + return true; + case PVR_IS_PLAYING_ACTIVE_RECORDING: + bValue = m_bIsPlayingActiveRecording; + return true; + } + return false; +} + +bool CPVRGUIInfo::GetRadioRDSBool(const CFileItem* item, const CGUIInfo& info, bool& bValue) const +{ + if (!item->HasPVRChannelInfoTag()) + return false; + + const std::shared_ptr<CPVRRadioRDSInfoTag> tag = + item->GetPVRChannelInfoTag()->GetRadioRDSInfoTag(); + if (tag) + { + switch (info.m_info) + { + case RDS_HAS_RADIOTEXT: + bValue = tag->IsPlayingRadioText(); + return true; + case RDS_HAS_RADIOTEXT_PLUS: + bValue = tag->IsPlayingRadioTextPlus(); + return true; + case RDS_HAS_HOTLINE_DATA: + bValue = (!tag->GetEMailHotline().empty() || !tag->GetPhoneHotline().empty()); + return true; + case RDS_HAS_STUDIO_DATA: + bValue = (!tag->GetEMailStudio().empty() || !tag->GetSMSStudio().empty() || + !tag->GetPhoneStudio().empty()); + return true; + } + } + + switch (info.m_info) + { + case RDS_HAS_RDS: + { + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + bValue = appPlayer->IsPlayingRDS(); + return true; + } + } + + return false; +} + +void CPVRGUIInfo::CharInfoBackendNumber(std::string& strValue) const +{ + size_t numBackends = m_backendProperties.size(); + + if (numBackends > 0) + strValue = StringUtils::Format("{0} {1} {2}", m_iCurrentActiveClient + 1, + g_localizeStrings.Get(20163), numBackends); + else + strValue = g_localizeStrings.Get(14023); +} + +void CPVRGUIInfo::CharInfoTotalDiskSpace(std::string& strValue) const +{ + strValue = StringUtils::SizeToString(m_iBackendDiskTotal).c_str(); +} + +void CPVRGUIInfo::CharInfoSignal(std::string& strValue) const +{ + strValue = StringUtils::Format("{} %", m_qualityInfo.iSignal / 655); +} + +void CPVRGUIInfo::CharInfoSNR(std::string& strValue) const +{ + strValue = StringUtils::Format("{} %", m_qualityInfo.iSNR / 655); +} + +void CPVRGUIInfo::CharInfoBER(std::string& strValue) const +{ + strValue = StringUtils::Format("{:08X}", m_qualityInfo.iBER); +} + +void CPVRGUIInfo::CharInfoUNC(std::string& strValue) const +{ + strValue = StringUtils::Format("{:08X}", m_qualityInfo.iUNC); +} + +void CPVRGUIInfo::CharInfoFrontendName(std::string& strValue) const +{ + if (!strlen(m_qualityInfo.strAdapterName)) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_qualityInfo.strAdapterName; +} + +void CPVRGUIInfo::CharInfoFrontendStatus(std::string& strValue) const +{ + if (!strlen(m_qualityInfo.strAdapterStatus)) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_qualityInfo.strAdapterStatus; +} + +void CPVRGUIInfo::CharInfoBackendName(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendName; +} + +void CPVRGUIInfo::CharInfoBackendVersion(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendVersion; +} + +void CPVRGUIInfo::CharInfoBackendHost(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendHost; +} + +void CPVRGUIInfo::CharInfoBackendDiskspace(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + + auto diskTotal = m_iBackendDiskTotal; + auto diskUsed = m_iBackendDiskUsed; + + if (diskTotal > 0) + { + strValue = StringUtils::Format(g_localizeStrings.Get(802), + StringUtils::SizeToString(diskTotal - diskUsed), + StringUtils::SizeToString(diskTotal)); + } + else + strValue = g_localizeStrings.Get(13205); +} + +void CPVRGUIInfo::CharInfoBackendProviders(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendProviders; +} + +void CPVRGUIInfo::CharInfoBackendChannelGroups(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendChannelGroups; +} + +void CPVRGUIInfo::CharInfoBackendChannels(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendChannels; +} + +void CPVRGUIInfo::CharInfoBackendTimers(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendTimers; +} + +void CPVRGUIInfo::CharInfoBackendRecordings(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendRecordings; +} + +void CPVRGUIInfo::CharInfoBackendDeletedRecordings(std::string& strValue) const +{ + m_updateBackendCacheRequested = true; + strValue = m_strBackendDeletedRecordings; +} + +void CPVRGUIInfo::CharInfoPlayingClientName(std::string& strValue) const +{ + if (m_strPlayingClientName.empty()) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_strPlayingClientName; +} + +void CPVRGUIInfo::CharInfoEncryption(std::string& strValue) const +{ + if (m_descrambleInfo.iCaid != PVR_DESCRAMBLE_INFO_NOT_AVAILABLE) + { + // prefer dynamically updated info, if available + strValue = CPVRChannel::GetEncryptionName(m_descrambleInfo.iCaid); + return; + } + else + { + const std::shared_ptr<CPVRChannel> channel = + CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + if (channel) + { + strValue = channel->EncryptionName(); + return; + } + } + + strValue.clear(); +} + +void CPVRGUIInfo::CharInfoService(std::string& strValue) const +{ + if (!strlen(m_qualityInfo.strServiceName)) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_qualityInfo.strServiceName; +} + +void CPVRGUIInfo::CharInfoMux(std::string& strValue) const +{ + if (!strlen(m_qualityInfo.strMuxName)) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_qualityInfo.strMuxName; +} + +void CPVRGUIInfo::CharInfoProvider(std::string& strValue) const +{ + if (!strlen(m_qualityInfo.strProviderName)) + strValue = g_localizeStrings.Get(13205); + else + strValue = m_qualityInfo.strProviderName; +} + +void CPVRGUIInfo::UpdateBackendCache() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + // Update the backend information for all backends if + // an update has been requested + if (m_iCurrentActiveClient == 0 && m_updateBackendCacheRequested) + { + std::vector<SBackend> backendProperties; + { + CSingleExit exit(m_critSection); + backendProperties = CServiceBroker::GetPVRManager().Clients()->GetBackendProperties(); + } + + m_backendProperties = backendProperties; + m_updateBackendCacheRequested = false; + } + + // Store some defaults + m_strBackendName = g_localizeStrings.Get(13205); + m_strBackendVersion = g_localizeStrings.Get(13205); + m_strBackendHost = g_localizeStrings.Get(13205); + m_strBackendProviders = g_localizeStrings.Get(13205); + m_strBackendChannelGroups = g_localizeStrings.Get(13205); + m_strBackendChannels = g_localizeStrings.Get(13205); + m_strBackendTimers = g_localizeStrings.Get(13205); + m_strBackendRecordings = g_localizeStrings.Get(13205); + m_strBackendDeletedRecordings = g_localizeStrings.Get(13205); + m_iBackendDiskTotal = 0; + m_iBackendDiskUsed = 0; + + // Update with values from the current client when we have at least one + if (!m_backendProperties.empty()) + { + const auto& backend = m_backendProperties[m_iCurrentActiveClient]; + + m_strBackendName = backend.name; + m_strBackendVersion = backend.version; + m_strBackendHost = backend.host; + + // We always display one extra as the add-on itself counts as a provider + if (backend.numProviders >= 0) + m_strBackendProviders = std::to_string(backend.numProviders + 1); + + if (backend.numChannelGroups >= 0) + m_strBackendChannelGroups = std::to_string(backend.numChannelGroups); + + if (backend.numChannels >= 0) + m_strBackendChannels = std::to_string(backend.numChannels); + + if (backend.numTimers >= 0) + m_strBackendTimers = std::to_string(backend.numTimers); + + if (backend.numRecordings >= 0) + m_strBackendRecordings = std::to_string(backend.numRecordings); + + if (backend.numDeletedRecordings >= 0) + m_strBackendDeletedRecordings = std::to_string(backend.numDeletedRecordings); + + m_iBackendDiskTotal = backend.diskTotal; + m_iBackendDiskUsed = backend.diskUsed; + } + + // Update the current active client, eventually wrapping around + if (++m_iCurrentActiveClient >= m_backendProperties.size()) + m_iCurrentActiveClient = 0; +} + +void CPVRGUIInfo::UpdateTimersCache() +{ + m_anyTimersInfo.UpdateTimersCache(); + m_tvTimersInfo.UpdateTimersCache(); + m_radioTimersInfo.UpdateTimersCache(); +} + +void CPVRGUIInfo::UpdateTimersToggle() +{ + m_anyTimersInfo.UpdateTimersToggle(); + m_tvTimersInfo.UpdateTimersToggle(); + m_radioTimersInfo.UpdateTimersToggle(); +} + +void CPVRGUIInfo::UpdateNextTimer() +{ + m_anyTimersInfo.UpdateNextTimer(); + m_tvTimersInfo.UpdateNextTimer(); + m_radioTimersInfo.UpdateNextTimer(); +} + +int CPVRGUIInfo::GetTimeShiftSeekPercent() const +{ + int progress = m_timesInfo.GetTimeshiftProgressPlayPosition(); + + const auto& components = CServiceBroker::GetAppComponents(); + const auto appPlayer = components.GetComponent<CApplicationPlayer>(); + int seekSize = appPlayer->GetSeekHandler().GetSeekSize(); + if (seekSize != 0) + { + int total = m_timesInfo.GetTimeshiftProgressDuration(); + + float totalTime = static_cast<float>(total); + if (totalTime == 0.0f) + return 0; + + float percentPerSecond = 100.0f / totalTime; + float percent = progress + percentPerSecond * seekSize; + percent = std::max(0.0f, std::min(percent, 100.0f)); + return std::lrintf(percent); + } + return progress; +} diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h new file mode 100644 index 0000000..5591353 --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUIInfo.h @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "addons/kodi-dev-kit/include/kodi/c-api/addon-instance/pvr/pvr_channels.h" +#include "guilib/guiinfo/GUIInfoProvider.h" +#include "pvr/addons/PVRClients.h" +#include "pvr/guilib/guiinfo/PVRGUITimerInfo.h" +#include "pvr/guilib/guiinfo/PVRGUITimesInfo.h" +#include "threads/CriticalSection.h" +#include "threads/Thread.h" + +#include <atomic> +#include <string> +#include <vector> + +class CFileItem; + +namespace KODI +{ +namespace GUILIB +{ +namespace GUIINFO +{ +class CGUIInfo; +} +} // namespace GUILIB +} // namespace KODI + +namespace PVR +{ +enum class PVREvent; +struct PVRChannelNumberInputChangedEvent; +struct PVRPreviewAndPlayerShowInfoChangedEvent; + +class CPVRGUIInfo : public KODI::GUILIB::GUIINFO::CGUIInfoProvider, private CThread +{ +public: + CPVRGUIInfo(); + ~CPVRGUIInfo() override = default; + + void Start(); + void Stop(); + + /*! + * @brief CEventStream callback for PVR events. + * @param event The event. + */ + void Notify(const PVREvent& event); + + /*! + * @brief CEventStream callback for channel number input changes. + * @param event The event. + */ + void Notify(const PVRChannelNumberInputChangedEvent& event); + + /*! + * @brief CEventStream callback for channel preview and player show info changes. + * @param event The event. + */ + void Notify(const PVRPreviewAndPlayerShowInfoChangedEvent& event); + + // KODI::GUILIB::GUIINFO::IGUIInfoProvider implementation + bool InitCurrentItem(CFileItem* item) override; + bool GetLabel(std::string& value, + const CFileItem* item, + int contextWindow, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + std::string* fallback) const override; + bool GetFallbackLabel(std::string& value, + const CFileItem* item, + int contextWindow, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + std::string* fallback) override; + bool GetInt(int& value, + const CGUIListItem* item, + int contextWindow, + const KODI::GUILIB::GUIINFO::CGUIInfo& info) const override; + bool GetBool(bool& value, + const CGUIListItem* item, + int contextWindow, + const KODI::GUILIB::GUIINFO::CGUIInfo& info) const override; + +private: + void ResetProperties(); + void ClearQualityInfo(PVR_SIGNAL_STATUS& qualityInfo); + void ClearDescrambleInfo(PVR_DESCRAMBLE_INFO& descrambleInfo); + + void Process() override; + + void UpdateTimersCache(); + void UpdateBackendCache(); + void UpdateQualityData(); + void UpdateDescrambleData(); + void UpdateMisc(); + void UpdateNextTimer(); + void UpdateTimeshiftData(); + void UpdateTimeshiftProgressData(); + + void UpdateTimersToggle(); + + bool GetListItemAndPlayerLabel(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + std::string& strValue) const; + bool GetPVRLabel(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + std::string& strValue) const; + bool GetRadioRDSLabel(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + std::string& strValue) const; + + bool GetListItemAndPlayerInt(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + int& iValue) const; + bool GetPVRInt(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + int& iValue) const; + int GetTimeShiftSeekPercent() const; + + bool GetListItemAndPlayerBool(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + bool& bValue) const; + bool GetPVRBool(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + bool& bValue) const; + bool GetRadioRDSBool(const CFileItem* item, + const KODI::GUILIB::GUIINFO::CGUIInfo& info, + bool& bValue) const; + + void CharInfoBackendNumber(std::string& strValue) const; + void CharInfoTotalDiskSpace(std::string& strValue) const; + void CharInfoSignal(std::string& strValue) const; + void CharInfoSNR(std::string& strValue) const; + void CharInfoBER(std::string& strValue) const; + void CharInfoUNC(std::string& strValue) const; + void CharInfoFrontendName(std::string& strValue) const; + void CharInfoFrontendStatus(std::string& strValue) const; + void CharInfoBackendName(std::string& strValue) const; + void CharInfoBackendVersion(std::string& strValue) const; + void CharInfoBackendHost(std::string& strValue) const; + void CharInfoBackendDiskspace(std::string& strValue) const; + void CharInfoBackendProviders(std::string& strValue) const; + void CharInfoBackendChannelGroups(std::string& strValue) const; + void CharInfoBackendChannels(std::string& strValue) const; + void CharInfoBackendTimers(std::string& strValue) const; + void CharInfoBackendRecordings(std::string& strValue) const; + void CharInfoBackendDeletedRecordings(std::string& strValue) const; + void CharInfoPlayingClientName(std::string& strValue) const; + void CharInfoEncryption(std::string& strValue) const; + void CharInfoService(std::string& strValue) const; + void CharInfoMux(std::string& strValue) const; + void CharInfoProvider(std::string& strValue) const; + + /** @name PVRGUIInfo data */ + //@{ + CPVRGUIAnyTimerInfo m_anyTimersInfo; // tv + radio + CPVRGUITVTimerInfo m_tvTimersInfo; + CPVRGUIRadioTimerInfo m_radioTimersInfo; + + CPVRGUITimesInfo m_timesInfo; + + bool m_bHasTVRecordings; + bool m_bHasRadioRecordings; + unsigned int m_iCurrentActiveClient; + std::string m_strPlayingClientName; + std::string m_strBackendName; + std::string m_strBackendVersion; + std::string m_strBackendHost; + std::string m_strBackendTimers; + std::string m_strBackendRecordings; + std::string m_strBackendDeletedRecordings; + std::string m_strBackendProviders; + std::string m_strBackendChannelGroups; + std::string m_strBackendChannels; + long long m_iBackendDiskTotal; + long long m_iBackendDiskUsed; + bool m_bIsPlayingTV; + bool m_bIsPlayingRadio; + bool m_bIsPlayingRecording; + bool m_bIsPlayingEpgTag; + bool m_bIsPlayingEncryptedStream; + bool m_bHasTVChannels; + bool m_bHasRadioChannels; + bool m_bCanRecordPlayingChannel; + bool m_bIsRecordingPlayingChannel; + bool m_bIsPlayingActiveRecording; + std::string m_strPlayingTVGroup; + std::string m_strPlayingRadioGroup; + + //@} + + PVR_SIGNAL_STATUS m_qualityInfo; /*!< stream quality information */ + PVR_DESCRAMBLE_INFO m_descrambleInfo; /*!< stream descramble information */ + std::vector<SBackend> m_backendProperties; + + std::string m_channelNumberInput; + bool m_previewAndPlayerShowInfo{false}; + + mutable CCriticalSection m_critSection; + + /** + * The various backend-related fields will only be updated when this + * flag is set. This is done to limit the amount of unnecessary + * backend querying when we're not displaying any of the queried + * information. + */ + mutable std::atomic<bool> m_updateBackendCacheRequested; + + bool m_bRegistered; +}; +} // namespace PVR diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp new file mode 100644 index 0000000..24a468a --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.cpp @@ -0,0 +1,270 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUITimerInfo.h" + +#include "ServiceBroker.h" +#include "guilib/LocalizeStrings.h" +#include "pvr/PVRManager.h" +#include "pvr/timers/PVRTimerInfoTag.h" +#include "pvr/timers/PVRTimers.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" + +#include <memory> +#include <mutex> +#include <string> +#include <vector> + +using namespace PVR; + +CPVRGUITimerInfo::CPVRGUITimerInfo() +{ + ResetProperties(); +} + +void CPVRGUITimerInfo::ResetProperties() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strActiveTimerTitle.clear(); + m_strActiveTimerChannelName.clear(); + m_strActiveTimerChannelIcon.clear(); + m_strActiveTimerTime.clear(); + m_strNextTimerInfo.clear(); + m_strNextRecordingTitle.clear(); + m_strNextRecordingChannelName.clear(); + m_strNextRecordingChannelIcon.clear(); + m_strNextRecordingTime.clear(); + m_iTimerAmount = 0; + m_iRecordingTimerAmount = 0; + m_iTimerInfoToggleStart = {}; + m_iTimerInfoToggleCurrent = 0; +} + +bool CPVRGUITimerInfo::TimerInfoToggle() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_iTimerInfoToggleStart.time_since_epoch().count() == 0) + { + m_iTimerInfoToggleStart = std::chrono::steady_clock::now(); + m_iTimerInfoToggleCurrent = 0; + return true; + } + + auto now = std::chrono::steady_clock::now(); + auto duration = + std::chrono::duration_cast<std::chrono::milliseconds>(now - m_iTimerInfoToggleStart); + + if (duration.count() > + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRInfoToggleInterval) + { + unsigned int iPrevious = m_iTimerInfoToggleCurrent; + unsigned int iBoundary = m_iRecordingTimerAmount > 0 ? m_iRecordingTimerAmount : m_iTimerAmount; + if (++m_iTimerInfoToggleCurrent > iBoundary - 1) + m_iTimerInfoToggleCurrent = 0; + + if (m_iTimerInfoToggleCurrent != iPrevious) + { + m_iTimerInfoToggleStart = std::chrono::steady_clock::now(); + return true; + } + } + + return false; +} + +void CPVRGUITimerInfo::UpdateTimersToggle() +{ + if (!TimerInfoToggle()) + return; + + std::string strActiveTimerTitle; + std::string strActiveTimerChannelName; + std::string strActiveTimerChannelIcon; + std::string strActiveTimerTime; + + /* safe to fetch these unlocked, since they're updated from the same thread as this one */ + if (m_iRecordingTimerAmount > 0) + { + std::vector<std::shared_ptr<CPVRTimerInfoTag>> activeTags = GetActiveRecordings(); + if (m_iTimerInfoToggleCurrent < activeTags.size()) + { + const std::shared_ptr<CPVRTimerInfoTag> tag = activeTags.at(m_iTimerInfoToggleCurrent); + strActiveTimerTitle = tag->Title(); + strActiveTimerChannelName = tag->ChannelName(); + strActiveTimerChannelIcon = tag->ChannelIcon(); + strActiveTimerTime = tag->StartAsLocalTime().GetAsLocalizedDateTime(false, false); + } + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strActiveTimerTitle = strActiveTimerTitle; + m_strActiveTimerChannelName = strActiveTimerChannelName; + m_strActiveTimerChannelIcon = strActiveTimerChannelIcon; + m_strActiveTimerTime = strActiveTimerTime; +} + +void CPVRGUITimerInfo::UpdateTimersCache() +{ + int iTimerAmount = AmountActiveTimers(); + int iRecordingTimerAmount = AmountActiveRecordings(); + + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_iTimerAmount = iTimerAmount; + m_iRecordingTimerAmount = iRecordingTimerAmount; + m_iTimerInfoToggleStart = {}; + } + + UpdateTimersToggle(); +} + +void CPVRGUITimerInfo::UpdateNextTimer() +{ + std::string strNextRecordingTitle; + std::string strNextRecordingChannelName; + std::string strNextRecordingChannelIcon; + std::string strNextRecordingTime; + std::string strNextTimerInfo; + + const std::shared_ptr<CPVRTimerInfoTag> timer = GetNextActiveTimer(); + if (timer) + { + strNextRecordingTitle = timer->Title(); + strNextRecordingChannelName = timer->ChannelName(); + strNextRecordingChannelIcon = timer->ChannelIcon(); + strNextRecordingTime = timer->StartAsLocalTime().GetAsLocalizedDateTime(false, false); + + strNextTimerInfo = StringUtils::Format("{} {} {} {}", g_localizeStrings.Get(19106), + timer->StartAsLocalTime().GetAsLocalizedDate(true), + g_localizeStrings.Get(19107), + timer->StartAsLocalTime().GetAsLocalizedTime("", false)); + } + + std::unique_lock<CCriticalSection> lock(m_critSection); + m_strNextRecordingTitle = strNextRecordingTitle; + m_strNextRecordingChannelName = strNextRecordingChannelName; + m_strNextRecordingChannelIcon = strNextRecordingChannelIcon; + m_strNextRecordingTime = strNextRecordingTime; + m_strNextTimerInfo = strNextTimerInfo; +} + +const std::string& CPVRGUITimerInfo::GetActiveTimerTitle() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strActiveTimerTitle; +} + +const std::string& CPVRGUITimerInfo::GetActiveTimerChannelName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strActiveTimerChannelName; +} + +const std::string& CPVRGUITimerInfo::GetActiveTimerChannelIcon() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strActiveTimerChannelIcon; +} + +const std::string& CPVRGUITimerInfo::GetActiveTimerDateTime() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strActiveTimerTime; +} + +const std::string& CPVRGUITimerInfo::GetNextTimerTitle() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strNextRecordingTitle; +} + +const std::string& CPVRGUITimerInfo::GetNextTimerChannelName() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strNextRecordingChannelName; +} + +const std::string& CPVRGUITimerInfo::GetNextTimerChannelIcon() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strNextRecordingChannelIcon; +} + +const std::string& CPVRGUITimerInfo::GetNextTimerDateTime() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strNextRecordingTime; +} + +const std::string& CPVRGUITimerInfo::GetNextTimer() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_strNextTimerInfo; +} + +int CPVRGUIAnyTimerInfo::AmountActiveTimers() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveTimers(); +} + +int CPVRGUIAnyTimerInfo::AmountActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveRecordings(); +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUIAnyTimerInfo::GetActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->GetActiveRecordings(); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRGUIAnyTimerInfo::GetNextActiveTimer() +{ + return CServiceBroker::GetPVRManager().Timers()->GetNextActiveTimer(); +} + +int CPVRGUITVTimerInfo::AmountActiveTimers() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveTVTimers(); +} + +int CPVRGUITVTimerInfo::AmountActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveTVRecordings(); +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUITVTimerInfo::GetActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->GetActiveTVRecordings(); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRGUITVTimerInfo::GetNextActiveTimer() +{ + return CServiceBroker::GetPVRManager().Timers()->GetNextActiveTVTimer(); +} + +int CPVRGUIRadioTimerInfo::AmountActiveTimers() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveRadioTimers(); +} + +int CPVRGUIRadioTimerInfo::AmountActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->AmountActiveRadioRecordings(); +} + +std::vector<std::shared_ptr<CPVRTimerInfoTag>> CPVRGUIRadioTimerInfo::GetActiveRecordings() +{ + return CServiceBroker::GetPVRManager().Timers()->GetActiveRadioRecordings(); +} + +std::shared_ptr<CPVRTimerInfoTag> CPVRGUIRadioTimerInfo::GetNextActiveTimer() +{ + return CServiceBroker::GetPVRManager().Timers()->GetNextActiveRadioTimer(); +} diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h new file mode 100644 index 0000000..260db6c --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimerInfo.h @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" + +#include <chrono> +#include <memory> +#include <string> +#include <vector> + +namespace PVR +{ + class CPVRTimerInfoTag; + + class CPVRGUITimerInfo + { + public: + CPVRGUITimerInfo(); + virtual ~CPVRGUITimerInfo() = default; + + void ResetProperties(); + + void UpdateTimersCache(); + void UpdateTimersToggle(); + void UpdateNextTimer(); + + const std::string& GetActiveTimerTitle() const; + const std::string& GetActiveTimerChannelName() const; + const std::string& GetActiveTimerChannelIcon() const; + const std::string& GetActiveTimerDateTime() const; + const std::string& GetNextTimerTitle() const; + const std::string& GetNextTimerChannelName() const; + const std::string& GetNextTimerChannelIcon() const; + const std::string& GetNextTimerDateTime() const; + const std::string& GetNextTimer() const; + + bool HasTimers() const { return m_iTimerAmount > 0; } + bool HasRecordingTimers() const { return m_iRecordingTimerAmount > 0; } + bool HasNonRecordingTimers() const { return m_iTimerAmount - m_iRecordingTimerAmount > 0; } + + private: + bool TimerInfoToggle(); + + virtual int AmountActiveTimers() = 0; + virtual int AmountActiveRecordings() = 0; + virtual std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() = 0; + virtual std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() = 0; + + unsigned int m_iTimerAmount; + unsigned int m_iRecordingTimerAmount; + + std::string m_strActiveTimerTitle; + std::string m_strActiveTimerChannelName; + std::string m_strActiveTimerChannelIcon; + std::string m_strActiveTimerTime; + std::string m_strNextRecordingTitle; + std::string m_strNextRecordingChannelName; + std::string m_strNextRecordingChannelIcon; + std::string m_strNextRecordingTime; + std::string m_strNextTimerInfo; + + std::chrono::time_point<std::chrono::steady_clock> m_iTimerInfoToggleStart; + unsigned int m_iTimerInfoToggleCurrent; + + mutable CCriticalSection m_critSection; + }; + + class CPVRGUIAnyTimerInfo : public CPVRGUITimerInfo + { + public: + CPVRGUIAnyTimerInfo() = default; + + private: + int AmountActiveTimers() override; + int AmountActiveRecordings() override; + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override; + std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override; + }; + + class CPVRGUITVTimerInfo : public CPVRGUITimerInfo + { + public: + CPVRGUITVTimerInfo() = default; + + private: + int AmountActiveTimers() override; + int AmountActiveRecordings() override; + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override; + std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override; + }; + + class CPVRGUIRadioTimerInfo : public CPVRGUITimerInfo + { + public: + CPVRGUIRadioTimerInfo() = default; + + private: + int AmountActiveTimers() override; + int AmountActiveRecordings() override; + std::vector<std::shared_ptr<CPVRTimerInfoTag>> GetActiveRecordings() override; + std::shared_ptr<CPVRTimerInfoTag> GetNextActiveTimer() override; + }; + +} // namespace PVR diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp new file mode 100644 index 0000000..e5dfdb9 --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.cpp @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#include "PVRGUITimesInfo.h" + +#include "ServiceBroker.h" +#include "cores/DataCacheCore.h" +#include "pvr/PVRManager.h" +#include "pvr/PVRPlaybackState.h" +#include "pvr/channels/PVRChannel.h" +#include "pvr/channels/PVRChannelGroupsContainer.h" +#include "pvr/epg/EpgInfoTag.h" +#include "pvr/recordings/PVRRecording.h" +#include "settings/AdvancedSettings.h" +#include "settings/SettingsComponent.h" +#include "utils/StringUtils.h" + +#include <cmath> +#include <ctime> +#include <memory> +#include <mutex> + +using namespace PVR; + +CPVRGUITimesInfo::CPVRGUITimesInfo() +{ + Reset(); +} + +void CPVRGUITimesInfo::Reset() +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + + m_iStartTime = 0; + m_iDuration = 0; + m_iTimeshiftStartTime = 0; + m_iTimeshiftEndTime = 0; + m_iTimeshiftPlayTime = 0; + m_iTimeshiftOffset = 0; + + m_iTimeshiftProgressStartTime = 0; + m_iTimeshiftProgressEndTime = 0; + m_iTimeshiftProgressDuration = 0; + + m_playingEpgTag.reset(); + m_playingChannel.reset(); +} + +void CPVRGUITimesInfo::UpdatePlayingTag() +{ + const std::shared_ptr<CPVRChannel> currentChannel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + std::shared_ptr<CPVREpgInfoTag> currentTag = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingEpgTag(); + + if (currentChannel || currentTag) + { + if (currentChannel && !currentTag) + currentTag = currentChannel->GetEPGNow(); + + const std::shared_ptr<CPVRChannelGroupsContainer> groups = CServiceBroker::GetPVRManager().ChannelGroups(); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + const std::shared_ptr<CPVRChannel> playingChannel = + m_playingEpgTag ? groups->GetChannelForEpgTag(m_playingEpgTag) : nullptr; + + if (!m_playingEpgTag || !currentTag || !playingChannel || !currentChannel || + m_playingEpgTag->StartAsUTC() != currentTag->StartAsUTC() || + m_playingEpgTag->EndAsUTC() != currentTag->EndAsUTC() || *playingChannel != *currentChannel) + { + if (currentTag) + { + m_playingEpgTag = currentTag; + m_iDuration = m_playingEpgTag->GetDuration(); + } + else if (m_iTimeshiftEndTime > m_iTimeshiftStartTime) + { + m_playingEpgTag.reset(); + m_iDuration = m_iTimeshiftEndTime - m_iTimeshiftStartTime; + } + else + { + m_playingEpgTag.reset(); + m_iDuration = 0; + } + } + } + else + { + const std::shared_ptr<CPVRRecording> recording = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingRecording(); + if (recording) + { + std::unique_lock<CCriticalSection> lock(m_critSection); + m_playingEpgTag.reset(); + m_iDuration = recording->GetDuration(); + } + } +} + +void CPVRGUITimesInfo::UpdateTimeshiftData() +{ + if (!CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingTV() && !CServiceBroker::GetPVRManager().PlaybackState()->IsPlayingRadio()) + { + // If nothing is playing (anymore), there is no need to update data. + Reset(); + return; + } + + time_t now = std::time(nullptr); + time_t iStartTime; + int64_t iPlayTime, iMinTime, iMaxTime; + CServiceBroker::GetDataCacheCore().GetPlayTimes(iStartTime, iPlayTime, iMinTime, iMaxTime); + bool bPlaying = CServiceBroker::GetDataCacheCore().GetSpeed() == 1.0f; + const std::shared_ptr<CPVRChannel> playingChannel = CServiceBroker::GetPVRManager().PlaybackState()->GetPlayingChannel(); + + std::unique_lock<CCriticalSection> lock(m_critSection); + + if (playingChannel != m_playingChannel) + { + // playing channel changed. we need to reset offset and playtime. + m_iTimeshiftOffset = 0; + m_iTimeshiftPlayTime = 0; + m_playingChannel = playingChannel; + } + + if (!iStartTime) + { + if (m_iStartTime == 0) + iStartTime = now; + else + iStartTime = m_iStartTime; + + iMinTime = iPlayTime; + iMaxTime = iPlayTime; + } + + m_iStartTime = iStartTime; + m_iTimeshiftStartTime = iStartTime + iMinTime / 1000; + m_iTimeshiftEndTime = iStartTime + iMaxTime / 1000; + + if (m_iTimeshiftEndTime > m_iTimeshiftStartTime) + { + // timeshifting supported + m_iTimeshiftPlayTime = iStartTime + iPlayTime / 1000; + if (iMaxTime > iPlayTime) + m_iTimeshiftOffset = (iMaxTime - iPlayTime) / 1000; + else + m_iTimeshiftOffset = 0; + } + else + { + // timeshifting not supported + if (bPlaying) + m_iTimeshiftPlayTime = now - m_iTimeshiftOffset; + + m_iTimeshiftOffset = now - m_iTimeshiftPlayTime; + } + + UpdateTimeshiftProgressData(); +} + +void CPVRGUITimesInfo::UpdateTimeshiftProgressData() +{ + // Note: General idea of the ts progress is always to be able to visualise both the complete + // ts buffer and the complete playing epg event (if any) side by side with the same time + // scale. TS progress start and end times will be calculated accordingly. + // + Start is usually ts buffer start, except if start time of playing epg event is + // before ts buffer start, then progress start is epg event start. + // + End is usually ts buffer end, except if end time of playing epg event is + // after ts buffer end, then progress end is epg event end. + // In simple timeshift mode (settings value), progress start is always the start time of + // playing epg event and progress end is always the end time of playing epg event. + + std::unique_lock<CCriticalSection> lock(m_critSection); + + ////////////////////////////////////////////////////////////////////////////////////// + // start time + ////////////////////////////////////////////////////////////////////////////////////// + bool bUpdatedStartTime = false; + if (m_playingEpgTag) + { + time_t start = 0; + m_playingEpgTag->StartAsUTC().GetAsTime(start); + if (start < m_iTimeshiftStartTime || + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRTimeshiftSimpleOSD) + { + // playing event started before start of ts buffer or simple ts osd to be used + m_iTimeshiftProgressStartTime = start; + bUpdatedStartTime = true; + } + } + + if (!bUpdatedStartTime) + { + // default to ts buffer start + m_iTimeshiftProgressStartTime = m_iTimeshiftStartTime; + } + + ////////////////////////////////////////////////////////////////////////////////////// + // end time + ////////////////////////////////////////////////////////////////////////////////////// + bool bUpdatedEndTime = false; + if (m_playingEpgTag) + { + time_t end = 0; + m_playingEpgTag->EndAsUTC().GetAsTime(end); + if (end > m_iTimeshiftEndTime || + CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_bPVRTimeshiftSimpleOSD) + { + // playing event will end after end of ts buffer or simple ts osd to be used + m_iTimeshiftProgressEndTime = end; + bUpdatedEndTime = true; + } + } + + if (!bUpdatedEndTime) + { + // default to ts buffer end + m_iTimeshiftProgressEndTime = m_iTimeshiftEndTime; + } + + ////////////////////////////////////////////////////////////////////////////////////// + // duration + ////////////////////////////////////////////////////////////////////////////////////// + m_iTimeshiftProgressDuration = m_iTimeshiftProgressEndTime - m_iTimeshiftProgressStartTime; +} + +void CPVRGUITimesInfo::Update() +{ + UpdatePlayingTag(); + UpdateTimeshiftData(); +} + +std::string CPVRGUITimesInfo::TimeToTimeString(time_t datetime, TIME_FORMAT format, bool withSeconds) +{ + CDateTime time; + time.SetFromUTCDateTime(datetime); + return time.GetAsLocalizedTime(format, withSeconds); +} + +std::string CPVRGUITimesInfo::GetTimeshiftStartTime(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return TimeToTimeString(m_iTimeshiftStartTime, format, false); +} + +std::string CPVRGUITimesInfo::GetTimeshiftEndTime(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return TimeToTimeString(m_iTimeshiftEndTime, format, false); +} + +std::string CPVRGUITimesInfo::GetTimeshiftPlayTime(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return TimeToTimeString(m_iTimeshiftPlayTime, format, true); +} + +std::string CPVRGUITimesInfo::GetTimeshiftOffset(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return StringUtils::SecondsToTimeString(m_iTimeshiftOffset, format); +} + +std::string CPVRGUITimesInfo::GetTimeshiftProgressDuration(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return StringUtils::SecondsToTimeString(m_iTimeshiftProgressDuration, format); +} + +std::string CPVRGUITimesInfo::GetTimeshiftProgressStartTime(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return TimeToTimeString(m_iTimeshiftProgressStartTime, format, false); +} + +std::string CPVRGUITimesInfo::GetTimeshiftProgressEndTime(TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return TimeToTimeString(m_iTimeshiftProgressEndTime, format, false); +} + +std::string CPVRGUITimesInfo::GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return StringUtils::SecondsToTimeString(GetEpgEventDuration(epgTag), format); +} + +std::string CPVRGUITimesInfo::GetEpgEventElapsedTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const +{ + int iElapsed = 0; + std::unique_lock<CCriticalSection> lock(m_critSection); + if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag) + iElapsed = epgTag->Progress(); + else + iElapsed = GetElapsedTime(); + + return StringUtils::SecondsToTimeString(iElapsed, format); +} + +std::string CPVRGUITimesInfo::GetEpgEventRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return StringUtils::SecondsToTimeString(GetRemainingTime(epgTag), format); +} + +std::string CPVRGUITimesInfo::GetEpgEventFinishTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const +{ + CDateTime finish = CDateTime::GetCurrentDateTime(); + finish += CDateTimeSpan(0, 0, 0, GetRemainingTime(epgTag)); + return finish.GetAsLocalizedTime(format); +} + +std::string CPVRGUITimesInfo::GetEpgEventSeekTime(int iSeekSize, TIME_FORMAT format) const +{ + return StringUtils::SecondsToTimeString(GetElapsedTime() + iSeekSize, format); +} + +int CPVRGUITimesInfo::GetElapsedTime() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_playingEpgTag || m_iTimeshiftStartTime) + { + CDateTime current(m_iTimeshiftPlayTime); + CDateTime start = m_playingEpgTag ? m_playingEpgTag->StartAsUTC() : CDateTime(m_iTimeshiftStartTime); + CDateTimeSpan time = current > start ? current - start : CDateTimeSpan(0, 0, 0, 0); + return time.GetSecondsTotal(); + } + else + { + return 0; + } +} + +int CPVRGUITimesInfo::GetRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag) + return epgTag->GetDuration() - epgTag->Progress(); + else + return m_iDuration - GetElapsedTime(); +} + +int CPVRGUITimesInfo::GetTimeshiftProgress() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::lrintf(static_cast<float>(m_iTimeshiftPlayTime - m_iTimeshiftStartTime) / (m_iTimeshiftEndTime - m_iTimeshiftStartTime) * 100); +} + +int CPVRGUITimesInfo::GetTimeshiftProgressDuration() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return m_iTimeshiftProgressDuration; +} + +int CPVRGUITimesInfo::GetTimeshiftProgressPlayPosition() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::lrintf(static_cast<float>(m_iTimeshiftPlayTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100); +} + +int CPVRGUITimesInfo::GetTimeshiftProgressEpgStart() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_playingEpgTag) + { + time_t epgStart = 0; + m_playingEpgTag->StartAsUTC().GetAsTime(epgStart); + return std::lrintf(static_cast<float>(epgStart - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100); + } + return 0; +} + +int CPVRGUITimesInfo::GetTimeshiftProgressEpgEnd() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (m_playingEpgTag) + { + time_t epgEnd = 0; + m_playingEpgTag->EndAsUTC().GetAsTime(epgEnd); + return std::lrintf(static_cast<float>(epgEnd - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100); + } + return 0; +} + +int CPVRGUITimesInfo::GetTimeshiftProgressBufferStart() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::lrintf(static_cast<float>(m_iTimeshiftStartTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100); +} + +int CPVRGUITimesInfo::GetTimeshiftProgressBufferEnd() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return std::lrintf(static_cast<float>(m_iTimeshiftEndTime - m_iTimeshiftProgressStartTime) / m_iTimeshiftProgressDuration * 100); +} + +int CPVRGUITimesInfo::GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag) + return epgTag->GetDuration(); + else + return m_iDuration; +} + +int CPVRGUITimesInfo::GetEpgEventProgress(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + if (epgTag && m_playingEpgTag && *epgTag != *m_playingEpgTag) + return std::lrintf(epgTag->ProgressPercentage()); + else + return std::lrintf(static_cast<float>(GetElapsedTime()) / m_iDuration * 100); +} + +bool CPVRGUITimesInfo::IsTimeshifting() const +{ + std::unique_lock<CCriticalSection> lock(m_critSection); + return (m_iTimeshiftOffset > static_cast<unsigned int>(CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_iPVRTimeshiftThreshold)); +} diff --git a/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h new file mode 100644 index 0000000..47dd9f5 --- /dev/null +++ b/xbmc/pvr/guilib/guiinfo/PVRGUITimesInfo.h @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2012-2018 Team Kodi + * This file is part of Kodi - https://kodi.tv + * + * SPDX-License-Identifier: GPL-2.0-or-later + * See LICENSES/README.md for more information. + */ + +#pragma once + +#include "threads/CriticalSection.h" +#include "utils/TimeFormat.h" + +#include <memory> + +namespace PVR +{ + class CPVRChannel; + class CPVREpgInfoTag; + + class CPVRGUITimesInfo + { + public: + CPVRGUITimesInfo(); + virtual ~CPVRGUITimesInfo() = default; + + void Reset(); + void Update(); + + // GUI info labels + std::string GetTimeshiftStartTime(TIME_FORMAT format) const; + std::string GetTimeshiftEndTime(TIME_FORMAT format) const; + std::string GetTimeshiftPlayTime(TIME_FORMAT format) const; + std::string GetTimeshiftOffset(TIME_FORMAT format) const; + std::string GetTimeshiftProgressDuration(TIME_FORMAT format) const; + std::string GetTimeshiftProgressStartTime(TIME_FORMAT format) const; + std::string GetTimeshiftProgressEndTime(TIME_FORMAT format) const; + + std::string GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const; + std::string GetEpgEventElapsedTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const; + std::string GetEpgEventRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const; + std::string GetEpgEventFinishTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag, TIME_FORMAT format) const; + std::string GetEpgEventSeekTime(int iSeekSize, TIME_FORMAT format) const; + + // GUI info ints + int GetTimeshiftProgress() const; + int GetTimeshiftProgressDuration() const; + int GetTimeshiftProgressPlayPosition() const; + int GetTimeshiftProgressEpgStart() const; + int GetTimeshiftProgressEpgEnd() const; + int GetTimeshiftProgressBufferStart() const; + int GetTimeshiftProgressBufferEnd() const; + + int GetEpgEventDuration(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + int GetEpgEventProgress(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + + // GUI info bools + bool IsTimeshifting() const; + + private: + void UpdatePlayingTag(); + void UpdateTimeshiftData(); + void UpdateTimeshiftProgressData(); + + static std::string TimeToTimeString(time_t datetime, TIME_FORMAT format, bool withSeconds); + + int GetElapsedTime() const; + int GetRemainingTime(const std::shared_ptr<CPVREpgInfoTag>& epgTag) const; + + mutable CCriticalSection m_critSection; + + std::shared_ptr<CPVREpgInfoTag> m_playingEpgTag; + std::shared_ptr<CPVRChannel> m_playingChannel; + + time_t m_iStartTime; + unsigned int m_iDuration; + time_t m_iTimeshiftStartTime; + time_t m_iTimeshiftEndTime; + time_t m_iTimeshiftPlayTime; + unsigned int m_iTimeshiftOffset; + + time_t m_iTimeshiftProgressStartTime; + time_t m_iTimeshiftProgressEndTime; + unsigned int m_iTimeshiftProgressDuration; + }; + +} // namespace PVR |