/* * 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 #include #include #include #include #include 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& timer) const { CGUIDialogPVRTimerSettings* pDlgInfo = CServiceBroker::GetGUI()->GetWindowManager().GetWindow( 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 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 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 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 channel(CPVRItem(item).GetChannel()); if (!channel) { CLog::LogF(LOGERROR, "No channel!"); return false; } if (CServiceBroker::GetPVRManager().Get().CheckParentalLock(channel) != ParentalCheckResult::SUCCESS) return false; std::shared_ptr 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 timer( bCreateRule || !epgTag ? nullptr : CServiceBroker::GetPVRManager().Timers()->GetTimerForEpgTag(epgTag)); std::shared_ptr 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 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& 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().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 m_actions; }; InstantRecordingActionSelector::InstantRecordingActionSelector(int iInstantRecordTime) : m_iInstantRecordTime(iInstantRecordTime), m_pDlgSelect(CServiceBroker::GetGUI()->GetWindowManager().GetWindow( 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 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 () 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; }