Warning, file /pim/kalarm/src/messagedisplayhelper.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002  *  messagedisplayhelper.cpp  -  helper class to display an alarm or error message
0003  *  Program:  kalarm
0004  *  SPDX-FileCopyrightText: 2001-2023 David Jarvie <djarvie@kde.org>
0005  *
0006  *  SPDX-License-Identifier: GPL-2.0-or-later
0007  */
0008 
0009 #include "messagedisplayhelper.h"
0010 #include "messagedisplayhelper_p.h"
0011 #include "messagedisplay.h"
0012 
0013 #include "displaycalendar.h"
0014 #include "functions.h"
0015 #include "kalarmapp.h"
0016 #include "mainwindow.h"
0017 #include "resourcescalendar.h"
0018 #include "resources/resources.h"
0019 #include "lib/messagebox.h"
0020 #include "lib/pushbutton.h"
0021 #include "lib/synchtimer.h"
0022 #include "screensaver.h" // DBUS-generated
0023 #include "kalarm_debug.h"
0024 
0025 #ifdef HAVE_TEXT_TO_SPEECH_SUPPORT
0026 #include <TextEditTextToSpeech/TextToSpeech>
0027 #endif
0028 
0029 #include <KLocalizedString>
0030 #include <KConfig>
0031 #include <KIO/StatJob>
0032 #include <KIO/StoredTransferJob>
0033 #include <KJobWidgets>
0034 #include <KNotification>
0035 #include <phonon/MediaObject>
0036 #include <phonon/AudioOutput>
0037 #include <phonon/VolumeFaderEffect>
0038 
0039 #include <QByteArray>
0040 #include <QLocale>
0041 #include <QMimeDatabase>
0042 #include <QRegularExpression>
0043 #include <QTemporaryFile>
0044 #include <QTextBrowser>
0045 #include <QThread>
0046 #include <QTimeZone>
0047 #include <QTimer>
0048 #include <QUrl>
0049 
0050 using namespace KAlarmCal;
0051 
0052 namespace
0053 {
0054 const char FDO_SCREENSAVER_SERVICE[] = "org.freedesktop.ScreenSaver";
0055 const char FDO_SCREENSAVER_PATH[]    = "/org/freedesktop/ScreenSaver";
0056 }
0057 
0058 // Error message bit masks
0059 enum
0060 {
0061     ErrMsg_Speak     = 0x01,
0062     ErrMsg_AudioFile = 0x02
0063 };
0064 
0065 QList<MessageDisplayHelper*>   MessageDisplayHelper::mInstanceList;
0066 QHash<EventId, unsigned>       MessageDisplayHelper::mErrorMessages;
0067 // There can only be one audio thread at a time: trying to play multiple
0068 // sound files simultaneously would result in a cacophony, and besides
0069 // that, Phonon currently crashes...
0070 QPointer<QThread>     MessageDisplayHelper::mAudioThread;
0071 QPointer<AudioPlayer> MessageDisplayHelper::mAudioPlayer;
0072 MessageDisplayHelper* MessageDisplayHelper::mAudioOwner = nullptr;
0073 
0074 /******************************************************************************
0075 * Construct the message display handler for the specified alarm.
0076 * Other alarms in the supplied event may have been updated by the caller, so
0077 * the whole event needs to be stored for updating the calendar file when it is
0078 * displayed.
0079 */
0080 MessageDisplayHelper::MessageDisplayHelper(MessageDisplay* parent, const KAEvent& event, const KAAlarm& alarm, int flags)
0081     : mParent(parent)
0082     , mMessage(event.cleanText())
0083     , mFont(event.font())
0084     , mBgColour(event.bgColour())
0085     , mFgColour(event.fgColour())
0086     , mEventId(event)
0087     , mAudioFile(event.audioFile())
0088     , mVolume(event.soundVolume())
0089     , mFadeVolume(event.fadeVolume())
0090     , mFadeSeconds(qMin(event.fadeSeconds(), 86400))
0091     , mDefaultDeferMinutes(event.deferDefaultMinutes())
0092     , mAlarmType(alarm.type())
0093     , mAction(event.actionSubType())
0094     , mEmailId(event.emailId())
0095     , mCommandError(event.commandError())
0096     , mAudioRepeatPause(event.repeatSoundPause())
0097     , mConfirmAck(event.confirmAck())
0098     , mNoDefer(true)
0099     , mInvalid(false)
0100     , mEvent(event)
0101     , mOriginalEvent(event)
0102     , mResource(Resources::resourceForEvent(mEventId.eventId()))
0103     , mAlwaysHide(flags & MessageDisplay::AlwaysHide)
0104     , mNoPostAction(alarm.type() & KAAlarm::Type::Reminder)
0105     , mBeep(event.beep())
0106     , mSpeak(event.speak())
0107     , mNoRecordCmdError(flags & MessageDisplay::NoRecordCmdError)
0108     , mRescheduleEvent(!(flags & MessageDisplay::NoReschedule))
0109 {
0110     qCDebug(KALARM_LOG) << "MessageDisplayHelper():" << mEventId;
0111     if (alarm.type() & KAAlarm::Type::Reminder)
0112     {
0113         if (event.reminderMinutes() < 0)
0114         {
0115             event.previousOccurrence(alarm.dateTime(false).effectiveKDateTime(), mDateTime, false);
0116             if (!mDateTime.isValid()  &&  event.repeatAtLogin())
0117                 mDateTime = alarm.dateTime().addSecs(event.reminderMinutes() * 60);
0118         }
0119         else
0120             mDateTime = event.mainDateTime(true);
0121     }
0122     else
0123         mDateTime = alarm.dateTime(true);
0124     if (!(flags & (MessageDisplay::NoInitView | MessageDisplay::AlwaysHide)))
0125     {
0126         const bool readonly = KAlarm::eventReadOnly(mEventId.eventId());
0127         mShowEdit = !mEventId.isEmpty()  &&  !readonly;
0128         mNoDefer = readonly || (flags & MessageDisplay::NoDefer) || alarm.repeatAtLogin();
0129     }
0130 
0131     mInstanceList.append(this);
0132     if (event.autoClose())
0133         mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event.lateCancel() * 60);
0134 }
0135 
0136 /******************************************************************************
0137 * Construct the message display handler for a specified error message.
0138 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
0139 * that the option is specific to 'event'.
0140 */
0141 MessageDisplayHelper::MessageDisplayHelper(MessageDisplay* parent, const KAEvent& event, const DateTime& alarmDateTime,
0142                                            const QStringList& errmsgs, const QString& dontShowAgain)
0143     : mParent(parent)
0144     , mMessage(event.cleanText())
0145     , mDateTime(alarmDateTime)
0146     , mEventId(event)
0147     , mAlarmType(KAAlarm::Type::Main)
0148     , mAction(event.actionSubType())
0149     , mEmailId(-1)
0150     , mCommandError(KAEvent::CmdErr::None)
0151     , mErrorMsgs(errmsgs)
0152     , mDontShowAgain(dontShowAgain)
0153     , mConfirmAck(false)
0154     , mShowEdit(false)
0155     , mNoDefer(true)
0156     , mInvalid(false)
0157     , mEvent(event)
0158     , mOriginalEvent(event)
0159     , mErrorWindow(true)
0160     , mNoPostAction(true)
0161 {
0162     qCDebug(KALARM_LOG) << "MessageDisplayHelper(errmsg)";
0163     mInstanceList.append(this);
0164 }
0165 
0166 /******************************************************************************
0167 * Construct the message display handler for restoration by session management.
0168 * The handler is initialised by readProperties().
0169 */
0170 MessageDisplayHelper::MessageDisplayHelper(MessageDisplay* parent)
0171     : mParent(parent)
0172 {
0173     qCDebug(KALARM_LOG) << "MessageDisplayHelper(): restore";
0174     mInstanceList.append(this);
0175 }
0176 
0177 /******************************************************************************
0178 * Destructor. Perform any post-alarm actions before tidying up.
0179 */
0180 MessageDisplayHelper::~MessageDisplayHelper()
0181 {
0182     qCDebug(KALARM_LOG) << "~MessageDisplayHelper()" << mEventId;
0183     if (mAudioOwner == this  &&  !mAudioPlayer.isNull())
0184         mAudioPlayer->stop();   // mAudioPlayer will delete once stopped
0185     if (mAudioOwner == this)
0186         mAudioOwner = nullptr;
0187     // If the audio thread is destroyed while still running, it will crash.
0188     // So remove this instance as its parent to prevent its deletion as a child.
0189     if (mAudioThread.data())
0190         mAudioThread->setParent(nullptr);
0191     mErrorMessages.remove(mEventId);
0192     mInstanceList.removeAll(this);
0193     delete mTempFile;
0194     if (!mNoPostAction  &&  !mEvent.postAction().isEmpty())
0195         theApp()->alarmCompleted(mEvent);
0196 }
0197 
0198 /******************************************************************************
0199 * Obtain the texts to show in the displayed alarm.
0200 */
0201 void MessageDisplayHelper::initTexts()
0202 {
0203     const bool reminder = (!mErrorWindow  &&  (mAlarmType & KAAlarm::Type::Reminder));
0204     mTexts.title = (mAlarmType & KAAlarm::Type::Reminder) ? i18nc("@title:window", "Reminder")
0205                                                           : i18nc("@title:window", "Message");
0206 
0207     // Show the alarm date/time, together with a reminder text where appropriate.
0208     if (mDateTime.isValid())
0209     {
0210         // Alarm date/time: display time zone if not local time zone.
0211         mTexts.time = mTexts.timeFull = dateTimeToDisplay();
0212         if (reminder)
0213         {
0214             // Reminder.
0215             // Create a label "time\nReminder" by inserting the time at the
0216             // start of the translated string, allowing for possible HTML tags
0217             // enclosing "Reminder".
0218             QString s = i18nc("@info", "Reminder");
0219             static const QRegularExpression re(QStringLiteral("^(<[^>]+>)*"));  // search for HTML tag "<...>"
0220             const QRegularExpressionMatch match = re.match(s);
0221             // Prefix the time, plus a newline, to "Reminder", inside any HTML tags.
0222             s.insert(match.capturedEnd(0), mTexts.time + QLatin1String("<br/>"));
0223             mTexts.timeFull = s;
0224         }
0225     }
0226 
0227     if (!mErrorWindow)
0228     {
0229         // It's a normal alarm message display
0230         switch (mAction)
0231         {
0232             case KAEvent::SubAction::File:
0233             {
0234                 // Display the file name
0235                 mTexts.fileName = mMessage;
0236 
0237                 // Display contents of file
0238                 const QUrl url = QUrl::fromUserInput(mMessage, QString(), QUrl::AssumeLocalFile);
0239 
0240                 auto statJob = KIO::stat(url, KIO::StatJob::SourceSide, KIO::StatBasic, KIO::HideProgressInfo);
0241                 const bool exists = statJob->exec();
0242                 const bool isDir = statJob->statResult().isDir();
0243 
0244                 bool opened = false;
0245                 if (exists && !isDir)
0246                 {
0247                     auto job = KIO::storedGet(url);
0248                     KJobWidgets::setWindow(job, MainWindow::mainMainWindow());
0249                     if (job->exec())
0250                     {
0251                         opened = true;
0252                         const QByteArray data = job->data();
0253 
0254                         QMimeDatabase db;
0255                         QMimeType mime = db.mimeTypeForUrl(url);
0256                         if (mime.name() == QLatin1String("application/octet-stream"))
0257                             mime = db.mimeTypeForData(mTempFile);
0258                         mTexts.fileType = File::fileType(mime);
0259                         switch (mTexts.fileType)
0260                         {
0261                             case File::Type::Image:
0262                             case File::Type::TextFormatted:
0263                                 delete mTempFile;
0264                                 mTempFile = new QTemporaryFile;
0265                                 mTempFile->open();
0266                                 mTempFile->write(data);
0267                                 break;
0268                             default:
0269                                 break;
0270                         }
0271 
0272                         switch (mTexts.fileType)
0273                         {
0274                             case File::Type::Image:
0275                                 mTexts.message = QLatin1String(R"(<div align="center"><img src=")") + mTempFile->fileName() + QLatin1String(R"("></div>)");
0276                                 mTempFile->close();   // keep the file available to be displayed
0277                                 break;
0278                             case File::Type::TextFormatted:
0279                             {
0280                                 QTextBrowser browser;
0281                                 browser.setSource(QUrl::fromLocalFile(mTempFile->fileName()));
0282                                 mTexts.message = browser.toHtml();
0283                                 delete mTempFile;
0284                                 mTempFile = nullptr;
0285                                 break;
0286                             }
0287                             default:
0288                                 mTexts.message = QString::fromUtf8(data);
0289                                 break;
0290                         }
0291                     }
0292                 }
0293 
0294                 if (!exists || isDir || !opened)
0295                 {
0296                     mErrorMsgs += isDir ? i18nc("@info", "File is a folder") : exists ? i18nc("@info", "Failed to open file") : i18nc("@info", "File not found");
0297                 }
0298                 break;
0299             }
0300             case KAEvent::SubAction::Message:
0301                 mTexts.message = mMessage;
0302                 break;
0303 
0304             case KAEvent::SubAction::Command:
0305                 theApp()->execCommandAlarm(mEvent, mEvent.alarm(mAlarmType), mNoRecordCmdError,
0306                                            this, SLOT(readProcessOutput(ShellProcess*)), "commandCompleted");
0307                 break;
0308 
0309             case KAEvent::SubAction::Email:
0310             default:
0311                 break;
0312         }
0313 
0314         if (reminder  &&  mEvent.reminderMinutes() > 0)
0315         {
0316             // Advance reminder: show remaining time until the actual alarm
0317             if (mDateTime.isDateOnly()  ||  KADateTime::currentLocalDate().daysTo(mDateTime.date()) > 0)
0318             {
0319                 setRemainingTextDay(false);
0320                 MidnightTimer::connect(this, SLOT(slotSetRemainingTextDay()));    // update every day
0321             }
0322             else
0323             {
0324                 setRemainingTextMinute(false);
0325                 MinuteTimer::connect(this, SLOT(slotSetRemainingTextMinute()));   // update every minute
0326             }
0327         }
0328     }
0329     else
0330     {
0331         // It's an error message
0332         switch (mAction)
0333         {
0334             case KAEvent::SubAction::Email:
0335             {
0336                 // Display the email addresses and subject.
0337                 mTexts.errorEmail[0] = i18nc("@info Email addressee", "To:");
0338                 mTexts.errorEmail[1] = mEvent.emailAddresses(QStringLiteral("\n"));
0339                 mTexts.errorEmail[2] = i18nc("@info Email subject", "Subject:");
0340                 mTexts.errorEmail[3] = mEvent.emailSubject();
0341                 break;
0342             }
0343             case KAEvent::SubAction::Command:
0344             case KAEvent::SubAction::File:
0345             case KAEvent::SubAction::Message:
0346             default:
0347                 // Just display the error message strings
0348                 break;
0349         }
0350     }
0351 
0352     if (!mErrorMsgs.isEmpty())
0353         mTexts.title = i18nc("@title:window", "Error");
0354 
0355     mInitialised = true;   // the alarm's texts have been created
0356 }
0357 
0358 /******************************************************************************
0359 * Return the number of message displays, optionally excluding always-hidden ones.
0360 */
0361 int MessageDisplayHelper::instanceCount(bool excludeAlwaysHidden)
0362 {
0363     int count = mInstanceList.count();
0364     if (excludeAlwaysHidden)
0365     {
0366         for (const MessageDisplayHelper* h : std::as_const(mInstanceList))
0367         {
0368             if (h->mAlwaysHide)
0369                 --count;
0370         }
0371     }
0372     return count;
0373 }
0374 
0375 /******************************************************************************
0376 * Check whether to display an error message.
0377 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
0378 * that the option is specific to 'event'.
0379 */
0380 bool MessageDisplayHelper::shouldShowError(const KAEvent& event, const QStringList& errmsgs, const QString& dontShowAgain)
0381 {
0382     if (!dontShowAgain.isEmpty()
0383     &&  KAlarm::dontShowErrors(EventId(event), dontShowAgain))
0384         return false;
0385 
0386     // Don't pile up duplicate error messages for the same alarm
0387     EventId eid(event);
0388     for (const MessageDisplayHelper* h : std::as_const(mInstanceList))
0389     {
0390         if (h->mErrorWindow  &&  h->mEventId == eid
0391         &&  h->mErrorMsgs == errmsgs  &&  h->mDontShowAgain == dontShowAgain)
0392             return false;
0393     }
0394     return true;
0395 }
0396 
0397 /******************************************************************************
0398 * Convert a reminder display into a normal alarm display.
0399 */
0400 bool MessageDisplayHelper::cancelReminder(const KAEvent& event, const KAAlarm& alarm)
0401 {
0402     if (!mInitialised)
0403         return false;
0404     mDateTime = alarm.dateTime(true);
0405     mNoPostAction = false;
0406     mAlarmType = alarm.type();
0407     if (event.autoClose())
0408         mCloseTime = alarm.dateTime().effectiveKDateTime().toUtc().qDateTime().addSecs(event.lateCancel() * 60);
0409     mTexts.title = i18nc("@title:window", "Message");
0410     mTexts.time = mTexts.timeFull = dateTimeToDisplay();
0411     mTexts.remainingTime.clear();
0412     MidnightTimer::disconnect(this, SLOT(slotSetRemainingTextDay()));
0413     MinuteTimer::disconnect(this, SLOT(slotSetRemainingTextMinute()));
0414     Q_EMIT textsChanged(DisplayTexts::Title | DisplayTexts::Time | DisplayTexts::TimeFull | DisplayTexts::RemainingTime);
0415     return true;
0416 }
0417 
0418 /******************************************************************************
0419 * Update the alarm's trigger time. No textsChanged() signal is emitted.
0420 */
0421 bool MessageDisplayHelper::updateDateTime(const KAEvent& event, const KAAlarm& alarm)
0422 {
0423     mDateTime = (alarm.type() & KAAlarm::Type::Reminder) ? event.mainDateTime(true) : alarm.dateTime(true);
0424     if (!mDateTime.isValid())
0425         return false;
0426     mTexts.time = mTexts.timeFull = dateTimeToDisplay();
0427     return true;
0428 }
0429 
0430 /******************************************************************************
0431 * Get the trigger time to display.
0432 */
0433 QString MessageDisplayHelper::dateTimeToDisplay() const
0434 {
0435     QString tm;
0436     if (mDateTime.isValid())
0437     {
0438         QLocale locale;
0439         if (mDateTime.isDateOnly())
0440             tm = locale.toString(mDateTime.date(), QLocale::ShortFormat);
0441         else
0442         {
0443             bool showZone = false;
0444             if (mDateTime.timeType() == KADateTime::UTC
0445             ||  (mDateTime.timeType() == KADateTime::TimeZone && !mDateTime.isLocalZone()))
0446             {
0447                 // Display time zone abbreviation if it's different from the local
0448                 // zone. Note that the iCalendar time zone might represent the local
0449                 // time zone in a slightly different way from the system time zone,
0450                 // so the zone comparison above might not produce the desired result.
0451                 const QString tz = mDateTime.kDateTime().toString(QStringLiteral("%Z"));
0452                 KADateTime local = mDateTime.kDateTime();
0453                 local.setTimeSpec(KADateTime::Spec::LocalZone());
0454                 showZone = (local.toString(QStringLiteral("%Z")) != tz);
0455             }
0456             const QDateTime dt = mDateTime.qDateTime();
0457             tm = locale.toString(dt, QLocale::ShortFormat);
0458             if (showZone)
0459                 tm += QLatin1Char(' ') + mDateTime.timeZone().displayName(dt, QTimeZone::ShortName, locale);
0460         }
0461     }
0462     return tm;
0463 }
0464 
0465 /******************************************************************************
0466 * Set the remaining time text in a reminder display.
0467 * Called at the start of every day (at the user-defined start-of-day time).
0468 */
0469 void MessageDisplayHelper::setRemainingTextDay(bool notify)
0470 {
0471     const int days = KADateTime::currentLocalDate().daysTo(mDateTime.date());
0472     if (days <= 0  &&  !mDateTime.isDateOnly())
0473     {
0474         // The alarm is due today, so start refreshing every minute
0475         MidnightTimer::disconnect(this, SLOT(slotSetRemainingTextDay()));
0476         setRemainingTextMinute(notify);
0477         MinuteTimer::connect(this, SLOT(slotSetRemainingTextMinute()));   // update every minute
0478     }
0479     else
0480     {
0481         if (days <= 0)
0482             mTexts.remainingTime = i18nc("@info", "Today");
0483         else if (days % 7)
0484             mTexts.remainingTime = i18ncp("@info", "Tomorrow", "in %1 days' time", days);
0485         else
0486             mTexts.remainingTime = i18ncp("@info", "in 1 week's time", "in %1 weeks' time", days/7);
0487         if (notify)
0488             Q_EMIT textsChanged(DisplayTexts::RemainingTime);
0489     }
0490 }
0491 
0492 /******************************************************************************
0493 * Set the remaining time text in a reminder display.
0494 * Called on every minute boundary.
0495 */
0496 void MessageDisplayHelper::setRemainingTextMinute(bool notify)
0497 {
0498     const int mins = (KADateTime::currentUtcDateTime().secsTo(mDateTime.effectiveKDateTime()) + 59) / 60;
0499     if (mins < 60)
0500         mTexts.remainingTime = i18ncp("@info", "in 1 minute's time", "in %1 minutes' time", (mins > 0 ? mins : 0));
0501     else if (mins % 60 == 0)
0502         mTexts.remainingTime = i18ncp("@info", "in 1 hour's time", "in %1 hours' time", mins/60);
0503     else
0504     {
0505         QString hourText = i18ncp("@item:intext inserted into 'in ... %1 minute's time' below", "1 hour", "%1 hours", mins/60);
0506         mTexts.remainingTime = i18ncp("@info '%2' is the previous message '1 hour'/'%1 hours'", "in %2 1 minute's time", "in %2 %1 minutes' time", mins%60, hourText);
0507     }
0508     if (notify)
0509         Q_EMIT textsChanged(DisplayTexts::RemainingTime);
0510 }
0511 
0512 /******************************************************************************
0513 * Called when output is available from the command which is providing the text
0514 * for this display. Add the output.
0515 */
0516 void MessageDisplayHelper::readProcessOutput(ShellProcess* proc)
0517 {
0518     const QByteArray data = proc->readAll();
0519     if (!data.isEmpty())
0520     {
0521         mCommandOutput.clear();
0522 
0523         // Strip any trailing newline, to avoid showing trailing blank line
0524         // in message display.
0525         QString newText;
0526         if (mTexts.newLine)
0527             newText = QStringLiteral("\n");
0528         mTexts.newLine = data.endsWith('\n');
0529         newText += QString::fromLocal8Bit(data.data(), data.length() - (mTexts.newLine ? 1 : 0));
0530         mTexts.message += newText;
0531         Q_EMIT textsChanged(DisplayTexts::MessageAppend, newText);
0532     }
0533 }
0534 
0535 /******************************************************************************
0536 * Called when the command which is providing the text for this display has
0537 * completed. Check whether the command succeeded, even partially.
0538 */
0539 void MessageDisplayHelper::commandCompleted(ShellProcess::Status status)
0540 {
0541     bool failed;
0542     switch (status)
0543     {
0544         case ShellProcess::Status::Success:
0545         case ShellProcess::Status::Died:
0546             failed = false;
0547             break;
0548 
0549         case ShellProcess::Status::Unauthorised:
0550         case ShellProcess::Status::NotFound:
0551         case ShellProcess::Status::StartFail:
0552         case ShellProcess::Status::Inactive:
0553         default:
0554             failed = true;
0555             break;
0556     }
0557     Q_EMIT commandExited(!failed);
0558 }
0559 
0560 /******************************************************************************
0561 * Save settings to the session managed config file, for restoration
0562 * when the program is restored.
0563 */
0564 bool MessageDisplayHelper::saveProperties(KConfigGroup& config)
0565 {
0566     if (!mErrorWindow  &&  !mAlwaysHide)
0567     {
0568         config.writeEntry("EventID", mEventId.eventId());
0569         config.writeEntry("CollectionID", mResource.id());
0570         config.writeEntry("AlarmType", static_cast<int>(mAlarmType));
0571         if (mAlarmType == KAAlarm::Type::Invalid)
0572             qCCritical(KALARM_LOG) << "MessageDisplayHelper::saveProperties: Invalid alarm: id=" << mEventId << ", alarm count=" << mEvent.alarmCount();
0573         config.writeEntry("Message", mMessage);
0574         config.writeEntry("Type", static_cast<int>(mAction));
0575         config.writeEntry("Font", mFont);
0576         config.writeEntry("BgColour", mBgColour);
0577         config.writeEntry("FgColour", mFgColour);
0578         config.writeEntry("ConfirmAck", mConfirmAck);
0579         if (mDateTime.isValid())
0580         {
0581             config.writeEntry("Time", mDateTime.effectiveDateTime());
0582             config.writeEntry("DateOnly", mDateTime.isDateOnly());
0583             QByteArray zone;
0584             if (mDateTime.isUtc())
0585                 zone = "UTC";
0586             else if (mDateTime.isOffsetFromUtc())
0587             {
0588                 int offset = mDateTime.utcOffset();
0589                 if (offset >= 0)
0590                     zone = '+' + QByteArray::number(offset);
0591                 else
0592                     zone = QByteArray::number(offset);
0593             }
0594             else if (mDateTime.timeType() == KADateTime::TimeZone)
0595             {
0596                 const QTimeZone tz = mDateTime.timeZone();
0597                 if (tz.isValid())
0598                     zone = tz.id();
0599             }
0600             config.writeEntry("TimeZone", zone);
0601         }
0602         if (mCloseTime.isValid())
0603             config.writeEntry("Expiry", mCloseTime);
0604         if (mAudioRepeatPause >= 0  &&  mSilenceButton  &&  mSilenceButton->isEnabled())
0605         {
0606             // Only need to restart sound file playing if it's being repeated
0607             config.writePathEntry("AudioFile", mAudioFile);
0608             config.writeEntry("Volume", static_cast<int>(mVolume * 100));
0609             config.writeEntry("AudioPause", mAudioRepeatPause);
0610         }
0611         config.writeEntry("Speak", mSpeak);
0612         config.writeEntry("DeferMins", mDefaultDeferMinutes);
0613         config.writeEntry("NoDefer", mNoDefer);
0614         config.writeEntry("NoPostAction", mNoPostAction);
0615         config.writeEntry("EmailId", mEmailId);
0616         config.writeEntry("CmdErr", static_cast<int>(mCommandError));
0617         config.writeEntry("DontShowAgain", mDontShowAgain);
0618         return true;
0619     }
0620     else
0621     {
0622         config.writeEntry("Invalid", true);
0623         return false;
0624     }
0625 }
0626 
0627 /******************************************************************************
0628 * Read settings from the session managed config file.
0629 * This function is automatically called whenever the app is being restored.
0630 * Read in whatever was saved in saveProperties().
0631 * Reply = true if the parent display needs to initialise its display.
0632 */
0633 bool MessageDisplayHelper::readProperties(const KConfigGroup& config)
0634 {
0635     return readPropertyValues(config)
0636        &&  processPropertyValues();
0637 }
0638 
0639 /******************************************************************************
0640 * Read settings from the session managed config file.
0641 * This function is automatically called whenever the app is being restored.
0642 * Read in whatever was saved in saveProperties().
0643 * Reply = true if the parent display needs to initialise its display.
0644 */
0645 bool MessageDisplayHelper::readPropertyValues(const KConfigGroup& config)
0646 {
0647     const QString eventId = config.readEntry("EventID");
0648     const ResourceId resourceId = config.readEntry("CollectionID", ResourceId(-1));
0649     mInvalid             = config.readEntry("Invalid", false);
0650     mAlarmType           = static_cast<KAAlarm::Type>(config.readEntry("AlarmType", 0));
0651     if (mAlarmType == KAAlarm::Type::Invalid)
0652     {
0653         mInvalid = true;
0654         qCCritical(KALARM_LOG) << "MessageDisplayHelper::readProperties: Invalid alarm: id=" << eventId;
0655     }
0656     mMessage             = config.readEntry("Message");
0657     mAction              = static_cast<KAEvent::SubAction>(config.readEntry("Type", 0));
0658     mFont                = config.readEntry("Font", QFont());
0659     mBgColour            = config.readEntry("BgColour", QColor(Qt::white));
0660     mFgColour            = config.readEntry("FgColour", QColor(Qt::black));
0661     mConfirmAck          = config.readEntry("ConfirmAck", false);
0662     QDateTime invalidDateTime;
0663     QDateTime dt         = config.readEntry("Time", invalidDateTime);
0664     const QByteArray zoneId = config.readEntry("TimeZone").toLatin1();
0665     KADateTime::Spec timeSpec;
0666     if (zoneId.isEmpty())
0667         timeSpec = KADateTime::LocalZone;
0668     else if (zoneId == "UTC")
0669         timeSpec = KADateTime::UTC;
0670     else if (zoneId.startsWith('+')  ||  zoneId.startsWith('-'))
0671         timeSpec.setType(KADateTime::OffsetFromUTC, zoneId.toInt());
0672     else
0673         timeSpec = QTimeZone(zoneId);
0674     mDateTime = KADateTime(dt.date(), dt.time(), timeSpec);
0675     const bool dateOnly  = config.readEntry("DateOnly", false);
0676     if (dateOnly)
0677         mDateTime.setDateOnly(true);
0678     mCloseTime           = config.readEntry("Expiry", invalidDateTime);
0679     mCloseTime.setTimeZone(QTimeZone::utc());
0680     mAudioFile           = config.readPathEntry("AudioFile", QString());
0681     mVolume              = static_cast<float>(config.readEntry("Volume", 0)) / 100;
0682     mFadeVolume          = -1;
0683     mFadeSeconds         = 0;
0684     if (!mAudioFile.isEmpty())   // audio file URL was only saved if it repeats
0685         mAudioRepeatPause = config.readEntry("AudioPause", 0);
0686     mBeep                = false;   // don't beep after restart (similar to not playing non-repeated sound file)
0687     mSpeak               = config.readEntry("Speak", false);
0688     mDefaultDeferMinutes = config.readEntry("DeferMins", 0);
0689     mNoDefer             = config.readEntry("NoDefer", false);
0690     mNoPostAction        = config.readEntry("NoPostAction", true);
0691     mEmailId             = config.readEntry("EmailId", KAEvent::EmailId(0));
0692     mCommandError        = KAEvent::CmdErr(config.readEntry("CmdErr", static_cast<int>(KAEvent::CmdErr::None)));
0693     mDontShowAgain       = config.readEntry("DontShowAgain", QString());
0694     mShowEdit            = false;
0695     // Temporarily initialise mResource and mEventId - they will be set by redisplayAlarm()
0696     mResource            = Resources::resource(resourceId);
0697     mEventId             = EventId(resourceId, eventId);
0698     if (mAlarmType == KAAlarm::Type::Invalid)
0699         return false;
0700     qCDebug(KALARM_LOG) << "MessageDisplayHelper::readProperties:" << eventId;
0701     return true;
0702 }
0703 
0704 /******************************************************************************
0705 * Recreate the event from the calendar file (if possible).
0706 */
0707 bool MessageDisplayHelper::processPropertyValues()
0708 {
0709     if (!mEventId.eventId().isEmpty())
0710     {
0711         // Close any other display for this alarm which has already been restored by redisplayAlarms()
0712         if (!Resources::allCreated())
0713         {
0714             connect(Resources::instance(), &Resources::resourcesCreated,
0715                                      this, &MessageDisplayHelper::showRestoredAlarm);
0716             return false;
0717         }
0718         redisplayAlarm();
0719     }
0720     return true;
0721 }
0722 
0723 /******************************************************************************
0724 * Fetch the restored alarm from the calendar and redisplay it in this display.
0725 */
0726 void MessageDisplayHelper::showRestoredAlarm()
0727 {
0728     qCDebug(KALARM_LOG) << "MessageDisplayHelper::showRestoredAlarm:" << mEventId;
0729     redisplayAlarm();
0730     mParent->setUpDisplay();
0731     mParent->showDisplay();
0732 }
0733 
0734 /******************************************************************************
0735 * Fetch the restored alarm from the calendar and redisplay it in this display.
0736 */
0737 void MessageDisplayHelper::redisplayAlarm()
0738 {
0739     mResource = Resources::resourceForEvent(mEventId.eventId());
0740     mEventId.setResourceId(mResource.id());
0741     qCDebug(KALARM_LOG) << "MessageDisplayHelper::redisplayAlarm:" << mEventId;
0742     // Delete any already existing display for the same event
0743     MessageDisplay* duplicate = findEvent(mEventId, mParent);
0744     if (duplicate)
0745     {
0746         qCDebug(KALARM_LOG) << "MessageDisplayHelper::redisplayAlarm: Deleting duplicate display:" << mEventId;
0747         delete duplicate;
0748     }
0749 
0750     const KAEvent event = ResourcesCalendar::event(mEventId);
0751     if (event.isValid())
0752     {
0753         mEvent = event;
0754         mShowEdit = true;
0755     }
0756     else
0757     {
0758         // It's not in the active calendar, so try the displaying or archive calendars
0759         mParent->retrieveEvent(mEventId, mEvent, mResource, mShowEdit, mNoDefer);
0760         mNoDefer = !mNoDefer;
0761     }
0762 }
0763 
0764 /******************************************************************************
0765 * Called when an alarm is currently being displayed, to store a copy of the
0766 * alarm in the displaying calendar, and to reschedule it for its next repetition.
0767 * If no repetitions remain, cancel it.
0768 */
0769 bool MessageDisplayHelper::alarmShowing(KAEvent& event)
0770 {
0771     qCDebug(KALARM_LOG) << "MessageDisplayHelper::alarmShowing:" << event.id() << "," << KAAlarm::debugType(mAlarmType);
0772     const KAAlarm alarm = event.alarm(mAlarmType);
0773     if (!alarm.isValid())
0774     {
0775         qCCritical(KALARM_LOG) << "MessageDisplayHelper::alarmShowing: Alarm type not found:" << event.id() << ":" << mAlarmType;
0776         return false;
0777     }
0778     if (!mAlwaysHide)
0779     {
0780         // Copy the alarm to the displaying calendar in case of a crash, etc.
0781         KAEvent dispEvent;
0782         const ResourceId id = Resources::resourceForEvent(event.id()).id();
0783         dispEvent.setDisplaying(event, mAlarmType, id,
0784                                 mDateTime.effectiveKDateTime(), mShowEdit, !mNoDefer);
0785         if (DisplayCalendar::open())
0786         {
0787             DisplayCalendar::deleteEvent(dispEvent.id());   // in case it already exists
0788             DisplayCalendar::addEvent(dispEvent);
0789             DisplayCalendar::save();
0790         }
0791     }
0792     theApp()->rescheduleAlarm(event, alarm);
0793     return true;
0794 }
0795 
0796 /******************************************************************************
0797 * Returns the existing message display (if any) which is showing the event with
0798 * the specified ID.
0799 */
0800 MessageDisplay* MessageDisplayHelper::findEvent(const EventId& eventId, MessageDisplay* exclude)
0801 {
0802     if (!eventId.isEmpty())
0803     {
0804         for (const MessageDisplayHelper* h : std::as_const(mInstanceList))
0805         {
0806             if (h->mParent != exclude  &&  h->mEventId == eventId  &&  !h->mErrorWindow)
0807                 return h->mParent;
0808         }
0809     }
0810     return nullptr;
0811 }
0812 
0813 /******************************************************************************
0814 * Beep and play the audio file, as appropriate.
0815 */
0816 void MessageDisplayHelper::playAudio()
0817 {
0818     if (mBeep)
0819     {
0820         // Beep using two methods, in case the sound card/speakers are switched off or not working
0821         QApplication::beep();      // beep through the internal speaker
0822         KNotification::beep();     // beep through the sound card & speakers
0823     }
0824     if (!mAudioFile.isEmpty())
0825     {
0826         if (!mVolume  &&  mFadeVolume <= 0)
0827             return;    // ensure zero volume doesn't play anything
0828         startAudio();    // play the audio file
0829     }
0830     else if (mSpeak)
0831     {
0832         // The message is to be spoken. In case of error messages,
0833         // call it on a timer to allow the display to be shown first.
0834         QTimer::singleShot(0, this, &MessageDisplayHelper::slotSpeak);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
0835     }
0836 }
0837 
0838 /******************************************************************************
0839 * Speak the message.
0840 * Called asynchronously to avoid delaying the display of the message.
0841 */
0842 void MessageDisplayHelper::slotSpeak()
0843 {
0844 #ifdef HAVE_TEXT_TO_SPEECH_SUPPORT
0845     TextEditTextToSpeech::TextToSpeech* tts = TextEditTextToSpeech::TextToSpeech::self();
0846     if (!tts->isReady())
0847     {
0848         KAMessageBox::detailedError(MainWindow::mainMainWindow(), i18nc("@info", "Unable to speak message"), i18nc("@info", "Text-to-speech subsystem is not available"));
0849         clearErrorMessage(ErrMsg_Speak);
0850         return;
0851     }
0852 
0853     tts->say(mMessage);
0854 #endif
0855 }
0856 
0857 /******************************************************************************
0858 * Called when another display's audio thread has been destructed.
0859 * Start playing this display's audio file. Because initialising the sound system
0860 * and loading the file may take some time, it is called in a separate thread to
0861 * allow the display to show first.
0862 */
0863 void MessageDisplayHelper::startAudio()
0864 {
0865     if (mAudioThread)
0866     {
0867         // An audio file is already playing for another message display, so
0868         // wait until it has finished.
0869         connect(mAudioThread.data(), &QObject::destroyed, this, &MessageDisplayHelper::audioTerminating);
0870     }
0871     else
0872     {
0873         qCDebug(KALARM_LOG) << "MessageDisplayHelper::startAudio:" << QThread::currentThread();
0874         mAudioOwner = this;
0875 
0876         // Create a thread for the audio player to run in, and create the audio
0877         // player as a worker to run in the thread created inside QThread.
0878         QThread* audioThread = new QThread(this);
0879         mAudioThread = audioThread;    // set the QPointer
0880         AudioPlayer* audioPlayer = new AudioPlayer(mAudioFile, mVolume, mFadeVolume, mFadeSeconds, mAudioRepeatPause);
0881         mAudioPlayer = audioPlayer;    // set the QPointer
0882         audioPlayer->moveToThread(audioThread);
0883         connect(audioThread, &QThread::started, audioPlayer, &AudioPlayer::execute);
0884         connect(audioPlayer, &AudioPlayer::destroyed, audioThread, &QThread::quit);
0885         connect(audioThread, &QThread::finished, audioThread, &QThread::deleteLater);
0886         connect(audioThread, &QThread::destroyed, audioThread, []() { qCDebug(KALARM_LOG) << "MessageDisplayHelper: Audio thread deleted"; });
0887         connect(audioThread, &QThread::destroyed, this, [this]() { if (mAudioOwner == parent()) mAudioOwner = nullptr; });
0888 
0889         // Set up connections not in the thread-worker relationship.
0890         connect(audioPlayer, &AudioPlayer::readyToPlay, this, &MessageDisplayHelper::playReady);
0891         connect(audioThread, &QThread::finished, this, &MessageDisplayHelper::playFinished);
0892         if (mSilenceButton)
0893             connect(mSilenceButton, &QAbstractButton::clicked, this, &MessageDisplayHelper::stopAudioPlay);
0894 
0895         // Notify after creating mAudioPlayer, so that isAudioPlaying() will
0896         // return the correct value.
0897         theApp()->notifyAudioPlaying(true);
0898         audioThread->start();
0899     }
0900 }
0901 
0902 /******************************************************************************
0903 * Return whether audio playback is currently active.
0904 */
0905 bool MessageDisplayHelper::isAudioPlaying()
0906 {
0907     return mAudioPlayer;
0908 }
0909 
0910 /******************************************************************************
0911 * Stop audio playback.
0912 */
0913 void MessageDisplayHelper::stopAudio()
0914 {
0915     qCDebug(KALARM_LOG) << "MessageDisplayHelper::stopAudio";
0916     if (mAudioPlayer)
0917         mAudioPlayer->stop();
0918 }
0919 
0920 /******************************************************************************
0921 * Ensure that the screen wakes from sleep, in case the window manager doesn't
0922 * do this when the window is displayed.
0923 */
0924 void MessageDisplayHelper::wakeScreen()
0925 {
0926     qCDebug(KALARM_LOG) << "MessageDisplayHelper::wakeScreen";
0927     // Note that this freedesktop D-Bus call to wake the screen may not work on
0928     // all systems. It is known to work on X11.
0929     QDBusConnection conn = QDBusConnection::sessionBus();
0930     if (conn.interface()->isServiceRegistered(QString::fromLatin1(FDO_SCREENSAVER_SERVICE)))
0931     {
0932         OrgFreedesktopScreenSaverInterface ssiface(
0933                 QString::fromLatin1(FDO_SCREENSAVER_SERVICE),
0934                 QString::fromLatin1(FDO_SCREENSAVER_PATH),
0935                 conn);
0936         ssiface.SimulateUserActivity();
0937     }
0938 }
0939 
0940 /******************************************************************************
0941 * Called when the audio file is ready to start playing.
0942 */
0943 void MessageDisplayHelper::playReady()
0944 {
0945     if (mSilenceButton)
0946         mSilenceButton->setEnabled(true);
0947 }
0948 
0949 /******************************************************************************
0950 * Called when another display's audio thread is being destructed.
0951 * Wait until the destructor has finished.
0952 */
0953 void MessageDisplayHelper::audioTerminating()
0954 {
0955     QTimer::singleShot(0, this, &MessageDisplayHelper::startAudio);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
0956 }
0957 
0958 /******************************************************************************
0959 * Called when the audio file thread finishes.
0960 */
0961 void MessageDisplayHelper::playFinished()
0962 {
0963     if (mSilenceButton)
0964         mSilenceButton->setEnabled(false);
0965     if (mAudioPlayer)   // mAudioPlayer can actually be null here!
0966     {
0967         const QString errmsg = mAudioPlayer->error();
0968         if (!errmsg.isEmpty()  &&  !haveErrorMessage(ErrMsg_AudioFile))
0969         {
0970             KAMessageBox::error(mParent->displayParent(), errmsg);
0971             clearErrorMessage(ErrMsg_AudioFile);
0972         }
0973     }
0974     delete mAudioThread.data();
0975     if (mAlwaysHide)
0976         mParent->closeDisplay();
0977 }
0978 
0979 /******************************************************************************
0980 * Constructor for audio player.
0981 */
0982 AudioPlayer::AudioPlayer(const QString& audioFile, float volume, float fadeVolume, int fadeSeconds, int repeatPause)
0983     : QObject()
0984     , mFile(audioFile)
0985     , mVolume(volume)
0986     , mFadeVolume(fadeVolume)
0987     , mFadeSeconds(fadeSeconds)
0988     , mRepeatPause(repeatPause)
0989 {
0990 }
0991 
0992 /******************************************************************************
0993 * Destructor for audio player.
0994 * Note that this destructor may be executed in the parent thread.
0995 */
0996 AudioPlayer::~AudioPlayer()
0997 {
0998     qCDebug(KALARM_LOG) << "MessageDisplayHelper::~AudioPlayer";
0999     mMutex.lock();
1000     mPath.disconnect();
1001     if (mFader)
1002     {
1003         mPath.removeEffect(mFader);
1004         delete mFader;
1005         mFader = nullptr;
1006     }
1007     delete mAudioObject;
1008     mAudioObject = nullptr;
1009     mMutex.unlock();
1010     // Notify after deleting mAudioPlayer, so that isAudioPlaying() will
1011     // return the correct value.
1012     QTimer::singleShot(0, theApp(), &KAlarmApp::notifyAudioStopped);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1013 }
1014 
1015 /******************************************************************************
1016 * Kick off playing the audio file.
1017 */
1018 void AudioPlayer::execute()
1019 {
1020     mMutex.lock();
1021     if (mAudioObject)
1022     {
1023         mMutex.unlock();
1024         return;
1025     }
1026     qCDebug(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::execute:" << QThread::currentThread() << mFile;
1027     const QString audioFile = mFile;
1028     const QUrl url = QUrl::fromUserInput(mFile);
1029     mFile = url.isLocalFile() ? url.toLocalFile() : url.toString();
1030     Phonon::MediaSource source(url);
1031     if (source.type() == Phonon::MediaSource::Invalid)
1032     {
1033         mError = xi18nc("@info", "Cannot open audio file: <filename>%1</filename>", audioFile);
1034         mMutex.unlock();
1035         qCCritical(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::execute: Open failure:" << audioFile;
1036         return;
1037     }
1038     mAudioObject = new Phonon::MediaObject(this);
1039     mAudioObject->setCurrentSource(source);
1040     mAudioObject->setTransitionTime(100);   // workaround to prevent clipping of end of files in Xine backend
1041     mAudioOutput = new Phonon::AudioOutput(Phonon::NotificationCategory, this);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1042     mPath = Phonon::createPath(mAudioObject, mAudioOutput);
1043     if (mVolume >= 0  ||  mFadeVolume >= 0)
1044     {
1045         if (mVolume >= 0)
1046             mAudioOutput->setVolume(mVolume);
1047         if (mFadeVolume >= 0  &&  mFadeSeconds > 0)
1048         {
1049             mFader = new Phonon::VolumeFaderEffect(this);
1050             if (!mFader->isValid())
1051             {
1052                 // The current Phonon backend doesn't support fading.
1053                 qCWarning(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer: Current Phonon backend does not support fading";
1054                 delete mFader;
1055                 mFader = nullptr;
1056             }
1057             else
1058             {
1059                 if (mVolume < 0)
1060                     mVolume = mAudioOutput->volume();
1061                 mPath.insertEffect(mFader);
1062             }
1063         }
1064     }
1065     connect(mAudioObject, &Phonon::MediaObject::stateChanged, this, &AudioPlayer::playStateChanged, Qt::DirectConnection);
1066     connect(mAudioObject, &Phonon::MediaObject::finished, this, &AudioPlayer::checkAudioPlay, Qt::DirectConnection);
1067     mPlayedOnce = false;
1068     mPausing    = false;
1069     mMutex.unlock();
1070     Q_EMIT readyToPlay();
1071     checkAudioPlay();
1072 }
1073 
1074 /******************************************************************************
1075 * Called when the audio file has loaded and is ready to play, or when play
1076 * has completed.
1077 * If it is ready to play, start playing it (for the first time or repeated).
1078 * If play has not yet completed, wait a bit longer.
1079 */
1080 void AudioPlayer::checkAudioPlay()
1081 {
1082     mMutex.lock();
1083     if (!mAudioObject)
1084     {
1085         mMutex.unlock();
1086         return;
1087     }
1088     if (mPausing)
1089         mPausing = false;
1090     else
1091     {
1092         // The file has loaded and is ready to play, or play has completed
1093         if (mPlayedOnce)
1094         {
1095             if (mRepeatPause < 0)
1096             {
1097                 // Play has completed
1098                 mMutex.unlock();
1099                 stop();
1100                 return;
1101             }
1102             if (mRepeatPause > 0)
1103             {
1104                 // Pause before playing the file again
1105                 mPausing = true;
1106                 QTimer::singleShot(mRepeatPause * 1000, this, &AudioPlayer::checkAudioPlay);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1107                 mMutex.unlock();
1108                 return;
1109             }
1110         }
1111         mPlayedOnce = true;
1112     }
1113 
1114     // Start playing the file, either for the first time or again
1115     qCDebug(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::checkAudioPlay: start";
1116     if (mVolume >= 0)
1117         mAudioOutput->setVolume(mVolume);
1118     if (mFader)
1119     {
1120         mFader->setVolume(mFadeVolume);
1121         mFader->fadeTo(mVolume, mFadeSeconds * 1000);
1122     }
1123     mAudioObject->play();
1124     mMutex.unlock();
1125 }
1126 
1127 /******************************************************************************
1128 * Called when the playback object changes state.
1129 * If an error has occurred, quit and return the error to the caller.
1130 */
1131 void AudioPlayer::playStateChanged(Phonon::State newState)
1132 {
1133     qCDebug(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::playStateChanged:" << newState;
1134     switch (newState)
1135     {
1136         case Phonon::StoppedState:
1137             mMutex.lock();
1138             if (mStopping)
1139             {
1140                 mPath.disconnect();
1141                 if (mFader)
1142                 {
1143                     // Delete fader from the audio thread, in case it is still active.
1144                     // because its timers can't be stopped from another thread.
1145                     mPath.removeEffect(mFader);
1146                     delete mFader;
1147                     mFader = nullptr;
1148                 }
1149                 deleteLater();
1150             }
1151             mMutex.unlock();
1152             break;
1153 
1154         case Phonon::ErrorState:
1155         {
1156             QMutexLocker locker(&mMutex);
1157             const QString err = mAudioObject->errorString();
1158             if (!err.isEmpty())
1159             {
1160                 qCCritical(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::playStateChanged: Play failure:" << mFile << ":" << err;
1161                 mError = xi18nc("@info", "<para>Error playing audio file: <filename>%1</filename></para><para>%2</para>", mFile, err);
1162                 exit(1);
1163             }
1164             break;
1165         }
1166 
1167         default:
1168             break;
1169     }
1170 }
1171 
1172 /******************************************************************************
1173 * Called when play completes, the Silence button is clicked, or the display is
1174 * closed, to terminate audio access.
1175 */
1176 void AudioPlayer::stop()
1177 {
1178     qCDebug(KALARM_LOG) << "MessageDisplayHelper::AudioPlayer::stop";
1179     mMutex.lock();
1180     mStopping = true;
1181     if (mAudioObject)
1182     {
1183         if (mAudioObject->state() != Phonon::StoppedState)
1184         {
1185             mAudioObject->stop();
1186             mMutex.unlock();
1187             return;
1188         }
1189     }
1190     mMutex.unlock();
1191     deleteLater();
1192 }
1193 
1194 QString AudioPlayer::error() const
1195 {
1196     QMutexLocker locker(&mMutex);
1197     return mError;
1198 }
1199 
1200 /******************************************************************************
1201 * Display the alarm.
1202 * Reply = true if the alarm should be shown, false if not.
1203 */
1204 bool MessageDisplayHelper::activateAutoClose()
1205 {
1206     if (mCloseTime.isValid())
1207     {
1208         // Set a timer to auto-close the display.
1209         int delay = QDateTime::currentDateTimeUtc().secsTo(mCloseTime);
1210         if (delay < 0)
1211             delay = 0;
1212         QTimer::singleShot(delay * 1000, this, &MessageDisplayHelper::autoCloseNow);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1213         if (!delay)
1214             return false;    // don't show the alarm if auto-closing is already due
1215     }
1216     return true;
1217 }
1218 
1219 /******************************************************************************
1220 * Called when the display has been shown properly (in its correct position),
1221 * to play sounds and reschedule the event.
1222 */
1223 void MessageDisplayHelper::displayComplete(bool audio)
1224 {
1225     delete mTempFile;
1226     mTempFile = nullptr;
1227     if (audio)
1228         playAudio();
1229     if (mRescheduleEvent)
1230         alarmShowing(mEvent);
1231 }
1232 
1233 /******************************************************************************
1234 * To be called when a close event is received.
1235 * Only quits the application if there is no system tray icon displayed.
1236 */
1237 bool MessageDisplayHelper::closeEvent()
1238 {
1239     // Don't prompt or delete the alarm from the display calendar if the session is closing
1240     if (!mErrorWindow  &&  !qApp->isSavingSession())
1241     {
1242         if (mConfirmAck  &&  !mParent->confirmAcknowledgement())
1243             return false;
1244 
1245         if (!mEventId.isEmpty())
1246         {
1247             // Delete from the display calendar
1248             KAlarm::deleteDisplayEvent(CalEvent::uid(mEventId.eventId(), CalEvent::DISPLAYING));
1249         }
1250     }
1251     return true;
1252 }
1253 
1254 /******************************************************************************
1255 * Create an alarm edit dialog.
1256 *
1257 * NOTE: The alarm edit dialog is made a child of the main window, not of
1258 *       displayParent(), so that if displayParent() closes before the dialog
1259 *       (e.g. on auto-close), KAlarm doesn't crash. The dialog is set non-modal
1260 *       so that the main window is unaffected, but modal mode is simulated so
1261 *       that displayParent() is inactive while the dialog is open.
1262 */
1263 EditAlarmDlg* MessageDisplayHelper::createEdit()
1264 {
1265     qCDebug(KALARM_LOG) << "MessageDisplayHelper::createEdit";
1266     mEditDlg = EditAlarmDlg::create(false, mOriginalEvent, false, MainWindow::mainMainWindow(), EditAlarmDlg::RES_IGNORE);
1267     if (mEditDlg)
1268     {
1269         mEditDlg->setAttribute(Qt::WA_NativeWindow, true);
1270         connect(mEditDlg, &QDialog::accepted, this, &MessageDisplayHelper::editCloseOk);
1271         connect(mEditDlg, &QDialog::rejected, this, &MessageDisplayHelper::editCloseCancel);
1272         connect(mEditDlg, &QObject::destroyed, this, &MessageDisplayHelper::editCloseCancel);
1273     }
1274     return mEditDlg;
1275 }
1276 
1277 /******************************************************************************
1278 * Execute the alarm edit dialog.
1279 */
1280 void MessageDisplayHelper::executeEdit()
1281 {
1282     MainWindow::mainMainWindow()->editAlarm(mEditDlg, mOriginalEvent);
1283 }
1284 
1285 /******************************************************************************
1286 * Called when OK is clicked in the alarm edit dialog invoked by the Edit button.
1287 * Closes the display.
1288 */
1289 void MessageDisplayHelper::editCloseOk()
1290 {
1291     mEditDlg = nullptr;
1292     mNoCloseConfirm = true;   // allow window to close without confirmation prompt
1293     mParent->closeDisplay();
1294 }
1295 
1296 /******************************************************************************
1297 * Called when Cancel is clicked in the alarm edit dialog invoked by the Edit
1298 * button, or when the dialog is deleted.
1299 */
1300 void MessageDisplayHelper::editCloseCancel()
1301 {
1302     mEditDlg = nullptr;
1303     mParent->editDlgCancelled();
1304 }
1305 
1306 /******************************************************************************
1307 * Set up to disable the defer button when the deferral limit is reached.
1308 */
1309 void MessageDisplayHelper::setDeferralLimit(const KAEvent& event)
1310 {
1311     mDeferLimit = event.deferralLimit().effectiveKDateTime().toUtc().qDateTime();
1312     MidnightTimer::connect(this, SLOT(checkDeferralLimit()));   // check every day
1313     mDisableDeferral = false;
1314     checkDeferralLimit();
1315 }
1316 
1317 /******************************************************************************
1318 * Check whether the deferral limit has been reached.
1319 * If so, disable the Defer button.
1320 * N.B. Ideally, just a single QTimer::singleShot() call would be made to disable
1321 *      the defer button at the correct time. But for a 32-bit integer, the
1322 *      milliseconds parameter overflows in about 25 days, so instead a daily
1323 *      check is done until the day when the deferral limit is reached, followed
1324 *      by a non-overflowing QTimer::singleShot() call.
1325 */
1326 void MessageDisplayHelper::checkDeferralLimit()
1327 {
1328     if (!mParent->isDeferButtonEnabled()  ||  !mDeferLimit.isValid())
1329         return;
1330     int n = KADateTime::currentLocalDate().daysTo(KADateTime(mDeferLimit, KADateTime::LocalZone).date());
1331     if (n > 0)
1332         return;
1333     MidnightTimer::disconnect(this, SLOT(checkDeferralLimit()));
1334     if (n == 0)
1335     {
1336         // The deferral limit will be reached today
1337         n = QDateTime::currentDateTimeUtc().secsTo(mDeferLimit);
1338         if (n > 0)
1339         {
1340             QTimer::singleShot(n * 1000, this, &MessageDisplayHelper::checkDeferralLimit);   //NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
1341             return;
1342         }
1343     }
1344     mParent->enableDeferButton(false);
1345     mDisableDeferral = true;
1346 }
1347 
1348 /******************************************************************************
1349 * Check whether the specified error message is already displayed for this
1350 * alarm, and note that it will now be displayed.
1351 * Reply = true if message is already displayed.
1352 */
1353 bool MessageDisplayHelper::haveErrorMessage(unsigned msg) const
1354 {
1355     if (!mErrorMessages.contains(mEventId))
1356         mErrorMessages.insert(mEventId, 0);
1357     unsigned& message = mErrorMessages[mEventId];
1358     const bool result = (message & msg);
1359     message |= msg;
1360     return result;
1361 }
1362 
1363 void MessageDisplayHelper::clearErrorMessage(unsigned msg) const
1364 {
1365     if (mErrorMessages.contains(mEventId))
1366     {
1367         unsigned& message = mErrorMessages[mEventId];
1368         if (message == msg)
1369             mErrorMessages.remove(mEventId);
1370         else
1371             message &= ~msg;
1372     }
1373 }
1374 
1375 #include "moc_messagedisplayhelper_p.cpp"
1376 #include "moc_messagedisplayhelper.cpp"
1377 
1378 // vim: et sw=4: