1 /*
2  *  messagenotification.cpp  -  displays an alarm message in a system notification
3  *  Program:  kalarm
4  *  SPDX-FileCopyrightText: 2020-2022 David Jarvie <djarvie@kde.org>
5  *
6  *  SPDX-License-Identifier: GPL-2.0-or-later
7  */
8 
9 #include "messagenotification.h"
10 #include "messagedisplayhelper.h"
11 
12 #include "kalarmapp.h"
13 #include "mainwindow.h"
14 #include "preferences.h"
15 #include "resourcescalendar.h"
16 #include "lib/file.h"
17 #include "kalarm_debug.h"
18 
19 #include <KAboutData>
20 #include <KLocalizedString>
21 #include <KIdleTime>
22 #ifdef RESTORE_NOTIFICATIONS
23 #include <KConfigGroup>
24 #include <KConfigGui>
25 #endif
26 
27 #include <QSessionManager>
28 
29 using namespace KAlarmCal;
30 
31 //clazy:excludeall=non-pod-global-static
32 
33 namespace
34 {
35 
36 // Notification eventIds: these are the IDs contained in the '[Event/ID]'
37 // entries in kalarm.notifyrc.
38 const QString MessageId = QStringLiteral("Message");
39 const QString BeepId    = QStringLiteral("MessageBeep");
40 const QString SpeakId   = QStringLiteral("MessageSpeak");
41 const QString ErrorId   = QStringLiteral("MessageError");
42 
43 // Flags for the notification
44 const KNotification::NotificationFlags NFLAGS = KNotification::RaiseWidgetOnActivation;
45 
46 const QString NL = QStringLiteral("\n");
47 const QString SP = QStringLiteral(" ");
48 
getNotifyEventId(const KAEvent & event)49 inline QString getNotifyEventId(const KAEvent& event)
50 {
51     return event.beep() ? BeepId : event.speak() ? SpeakId : MessageId;
52 }
53 
54 } // namespace
55 
56 
57 /*=============================================================================
58 * Helper class to save all message notifications' properties on session
59 * shutdown, to enable them to be recreated on the next startup.
60 *
61 * NOTE: When a notification has closed, there is currently no way to know
62 *       whether it has been closed by the user or has timed out. There is also
63 *       no way to know when a notification in the notification history is
64 *       closed by the user. So notifications are not restored on startup, since
65 *       that might re-raise notifications which the user has already closed.
66 *       If this changes in the future, notifications could be restored on
67 *       startup, in the same way as alarm windows are restored.
68 */
69 class MNSessionManager : public QObject
70 {
71     Q_OBJECT
72 public:
MNSessionManager()73     MNSessionManager()
74     {
75 #ifdef RESTORE_NOTIFICATIONS
76         connect(qApp, &QGuiApplication::saveStateRequest, this, &MNSessionManager::saveState);
77 #endif
78     }
~MNSessionManager()79     ~MNSessionManager() override {}
80 
create()81     static void create()
82     {
83         if (!mInstance)
84             mInstance = new MNSessionManager;
85     }
86 
87 private Q_SLOTS:
88 #ifdef RESTORE_NOTIFICATIONS
89     /******************************************************************************
90     * Called by the session manager to request the application to save its state.
91     */
saveState(QSessionManager & sm)92     void saveState(QSessionManager& sm)
93     {
94         KConfigGui::setSessionConfig(sm.sessionId(), sm.sessionKey());
95         KConfig* config = KConfigGui::sessionConfig();
96         // Save each MessageNotification's data.
97         int n = 1;
98         for (MessageNotification* notif : std::as_const(MessageNotification::mNotificationList))
99         {
100             const QByteArray group = "Notification_" + QByteArray::number(++n);
101             KConfigGroup cg(config, group.constData());
102             notif->saveProperties(cg);
103         }
104         KConfigGroup cg(config, "Number");
105         cg.writeEntry("NumberOfNotifications", MessageNotification::mNotificationList.count());
106     }
107 #endif
108 
109 private:
110     static MNSessionManager* mInstance;
111 };
112 
113 MNSessionManager* MNSessionManager::mInstance = nullptr;
114 
115 
116 QVector<MessageNotification*> MessageNotification::mNotificationList;
117 
118 /******************************************************************************
119 * Restore MessageNotification instances saved at session shutdown.
120 */
sessionRestore()121 void MessageNotification::sessionRestore()
122 {
123 #ifdef RESTORE_NOTIFICATIONS
124     KConfig* config = KConfigGui::sessionConfig();
125     if (config)
126     {
127         KConfigGroup cg(config, "Number");
128         const int count = cg.readEntry("NumberOfNotifications", 0);
129         for (int n = 1;  n <= count;  ++n)
130         {
131             const QByteArray group = "Notification_" + QByteArray::number(n);
132             cg = KConfigGroup(config, group.constData());
133             // Have to initialise the MessageNotification instance with its
134             // eventId already known. So first create a helper, then read
135             // its properties, and finally create the MessageNotification.
136             MessageDisplayHelper* helper = new MessageDisplayHelper(nullptr);
137             if (!helper->readPropertyValues(cg))
138                 delete helper;
139             else
140             {
141                 const QString notifyId = cg.readEntry("NotifyId");
142                 new MessageNotification(notifyId, helper);
143             }
144         }
145     }
146 #endif
147 }
148 
149 /******************************************************************************
150 * Construct the message notification for the specified alarm.
151 * Other alarms in the supplied event may have been updated by the caller, so
152 * the whole event needs to be stored for updating the calendar file when it is
153 * displayed.
154 */
MessageNotification(const KAEvent & event,const KAAlarm & alarm,int flags)155 MessageNotification::MessageNotification(const KAEvent& event, const KAAlarm& alarm, int flags)
156     : KNotification(getNotifyEventId(event), NFLAGS)
157     , MessageDisplay(event, alarm, flags)
158 {
159     qCDebug(KALARM_LOG) << "MessageNotification():" << mEventId();
160     MNSessionManager::create();
161     setWidget(MainWindow::mainMainWindow());
162     if (!(flags & NoInitView))
163         MessageNotification::setUpDisplay();    // avoid calling virtual method from constructor
164 
165     connect(this, qOverload<unsigned int>(&KNotification::activated), this, &MessageNotification::buttonActivated);
166     connect(this, &KNotification::closed, this, &MessageNotification::slotClosed);
167     connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageNotification::textsChanged);
168     connect(mHelper, &MessageDisplayHelper::commandExited, this, &MessageNotification::commandCompleted);
169 
170     mNotificationList.append(this);
171 }
172 
173 /******************************************************************************
174 * Construct the message notification for a specified error message.
175 * If 'dontShowAgain' is non-null, a "Don't show again" option is displayed. Note
176 * that the option is specific to 'event'.
177 */
MessageNotification(const KAEvent & event,const DateTime & alarmDateTime,const QStringList & errmsgs,const QString & dontShowAgain)178 MessageNotification::MessageNotification(const KAEvent& event, const DateTime& alarmDateTime,
179                        const QStringList& errmsgs, const QString& dontShowAgain)
180     : KNotification(ErrorId, NFLAGS)
181     , MessageDisplay(event, alarmDateTime, errmsgs, dontShowAgain)
182 {
183     qCDebug(KALARM_LOG) << "MessageNotification(errmsg)";
184     MNSessionManager::create();
185     setWidget(MainWindow::mainMainWindow());
186     MessageNotification::setUpDisplay();    // avoid calling virtual method from constructor
187 
188     connect(this, qOverload<unsigned int>(&KNotification::activated), this, &MessageNotification::buttonActivated);
189     connect(this, &KNotification::closed, this, &MessageNotification::slotClosed);
190     connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageNotification::textsChanged);
191 
192     mNotificationList.append(this);
193 }
194 
195 /******************************************************************************
196 * Construct the message notification from the properties contained in the
197 * supplied helper.
198 * Ownership of the helper is taken by the new instance.
199 */
MessageNotification(const QString & eventId,MessageDisplayHelper * helper)200 MessageNotification::MessageNotification(const QString& eventId, MessageDisplayHelper* helper)
201     : KNotification(eventId, NFLAGS)
202     , MessageDisplay(helper)
203 {
204     qCDebug(KALARM_LOG) << "MessageNotification(helper):" << mEventId();
205     MNSessionManager::create();
206     setWidget(MainWindow::mainMainWindow());
207 
208     connect(this, qOverload<unsigned int>(&KNotification::activated), this, &MessageNotification::buttonActivated);
209     connect(this, &KNotification::closed, this, &MessageNotification::slotClosed);
210     connect(mHelper, &MessageDisplayHelper::textsChanged, this, &MessageNotification::textsChanged);
211     connect(mHelper, &MessageDisplayHelper::commandExited, this, &MessageNotification::commandCompleted);
212 
213     mNotificationList.append(this);
214     helper->processPropertyValues();
215 }
216 
217 /******************************************************************************
218 * Destructor. Perform any post-alarm actions before tidying up.
219 */
~MessageNotification()220 MessageNotification::~MessageNotification()
221 {
222     qCDebug(KALARM_LOG) << "~MessageNotification" << mEventId();
223     close();
224     mNotificationList.removeAll(this);
225 }
226 
227 /******************************************************************************
228 * Construct the message notification.
229 */
setUpDisplay()230 void MessageNotification::setUpDisplay()
231 {
232     mHelper->initTexts();
233     MessageDisplayHelper::DisplayTexts texts = mHelper->texts();
234 
235     setNotificationTitle(texts.title);
236 
237     // Show the alarm date/time. Any reminder indication is shown in the
238     // notification title.
239     // Alarm date/time: display time zone if not local time zone.
240     mTimeText = texts.time;
241 
242     mMessageText.clear();
243     if (!mErrorWindow())
244     {
245         // It's a normal alarm message notification
246         switch (mAction())
247         {
248             case KAEvent::FILE:
249                 // Display the file name
250                 mMessageText = texts.fileName + NL;
251 
252                 if (mErrorMsgs().isEmpty())
253                 {
254                     // Display contents of file
255                     switch (texts.fileType)
256                     {
257                         case File::Image:
258                             break;   // can't display an image
259                         case File::TextFormatted:
260                         default:
261                             mMessageText += texts.message;
262                             break;
263                     }
264                 }
265                 break;
266 
267             case KAEvent::MESSAGE:
268                 mMessageText = texts.message;
269                 break;
270 
271             case KAEvent::COMMAND:
272                 mMessageText = texts.message;
273                 mCommandInhibit = true;
274                 break;
275 
276             case KAEvent::EMAIL:
277             default:
278                 break;
279         }
280 
281         if (!texts.remainingTime.isEmpty())
282         {
283             // Advance reminder: show remaining time until the actual alarm
284             mRemainingText = texts.remainingTime;
285         }
286     }
287     else
288     {
289         // It's an error message
290         switch (mAction())
291         {
292             case KAEvent::EMAIL:
293             {
294                 // Display the email addresses and subject.
295                 mMessageText = texts.errorEmail[0] + SP + texts.errorEmail[1] + NL
296                              + texts.errorEmail[2] + SP + texts.errorEmail[3] + NL;
297                 break;
298             }
299             case KAEvent::COMMAND:
300             case KAEvent::FILE:
301             case KAEvent::MESSAGE:
302             default:
303                 // Just display the error message strings
304                 break;
305         }
306     }
307 
308     if (!mErrorMsgs().isEmpty())
309     {
310         setIconName(QStringLiteral("dialog-error"));
311         mMessageText += mErrorMsgs().join(NL);
312         mCommandInhibit = false;
313     }
314 
315     setNotificationText();
316 
317     mEnableEdit = mShowEdit();
318     if (!mNoDefer())
319     {
320         mEnableDefer = true;
321         mHelper->setDeferralLimit(mEvent());  // ensure that button is disabled when alarm can't be deferred any more
322     }
323     setNotificationButtons();
324 
325     mInitialised = true;   // the notification's widgets have been created
326 }
327 
328 /******************************************************************************
329 * Return the number of message notifications.
330 */
notificationCount()331 int MessageNotification::notificationCount()
332 {
333     return mNotificationList.count();
334 }
335 
336 /******************************************************************************
337 * Returns the widget to act as parent for error messages, etc.
338 */
displayParent()339 QWidget* MessageNotification::displayParent()
340 {
341     return widget();
342 }
343 
closeDisplay()344 void MessageNotification::closeDisplay()
345 {
346     close();
347 }
348 
349 /******************************************************************************
350 * Display the notification.
351 * Output any required audio notification, and reschedule or delete the event
352 * from the calendar file.
353 */
showDisplay()354 void MessageNotification::showDisplay()
355 {
356     if (mInitialised  &&  mHelper->activateAutoClose())
357     {
358         if (!mCommandInhibit  &&  !mShown)
359         {
360             qCDebug(KALARM_LOG) << "MessageNotification::showDisplay: sendEvent";
361             sendEvent();
362             mShown = true;
363             // Ensure that the screen wakes from sleep, in case the window manager
364             // doesn't do this when the notification is displayed.
365             KIdleTime::instance()->simulateUserActivity();
366         }
367         if (!mDisplayComplete  &&  !mErrorWindow()  &&  mAlarmType() != KAAlarm::INVALID_ALARM)
368             mHelper->displayComplete(false);   // reschedule
369         mDisplayComplete = true;
370     }
371 }
372 
raiseDisplay()373 void MessageNotification::raiseDisplay()
374 {
375 }
376 
377 /******************************************************************************
378 * Raise the alarm notification, re-output any required audio notification, and
379 * reschedule the alarm in the calendar file.
380 */
repeat(const KAAlarm & alarm)381 void MessageNotification::repeat(const KAAlarm& alarm)
382 {
383     if (!mInitialised)
384         return;
385     if (mEventId().isEmpty())
386         return;
387     KAEvent event = ResourcesCalendar::event(mEventId());
388     if (event.isValid())
389     {
390         mAlarmType() = alarm.type();    // store new alarm type for use if it is later deferred
391         if (mHelper->alarmShowing(event))
392             ResourcesCalendar::updateEvent(event);
393     }
394 }
395 
hasDefer() const396 bool MessageNotification::hasDefer() const
397 {
398     return mEnableDefer;
399 }
400 
401 /******************************************************************************
402 * Show the Defer button when it was previously hidden.
403 */
showDefer()404 void MessageNotification::showDefer()
405 {
406     if (!mEnableDefer)
407     {
408         mNoDefer() = false;
409         mEnableDefer = true;
410         setNotificationButtons();
411         mHelper->setDeferralLimit(mEvent());    // remove button when alarm can't be deferred any more
412     }
413 }
414 
415 /******************************************************************************
416 * Convert a reminder notification into a normal alarm notification.
417 */
cancelReminder(const KAEvent & event,const KAAlarm & alarm)418 void MessageNotification::cancelReminder(const KAEvent& event, const KAAlarm& alarm)
419 {
420     if (mHelper->cancelReminder(event, alarm))
421     {
422         const MessageDisplayHelper::DisplayTexts& texts = mHelper->texts();
423         setNotificationTitle(texts.title);
424         mTimeText = texts.time;
425         mRemainingText.clear();
426         setNotificationText();
427         showDefer();
428     }
429 }
430 
431 /******************************************************************************
432 * Update and show the alarm's trigger time.
433 */
showDateTime(const KAEvent & event,const KAAlarm & alarm)434 void MessageNotification::showDateTime(const KAEvent& event, const KAAlarm& alarm)
435 {
436     if (mHelper->updateDateTime(event, alarm))
437     {
438         mTimeText = mHelper->texts().time;
439         setNotificationText();
440     }
441 }
442 
443 /******************************************************************************
444 * Called when the texts to display have changed.
445 */
textsChanged(MessageDisplayHelper::DisplayTexts::TextIds ids,const QString & change)446 void MessageNotification::textsChanged(MessageDisplayHelper::DisplayTexts::TextIds ids, const QString& change)
447 {
448     const MessageDisplayHelper::DisplayTexts& texts = mHelper->texts();
449 
450     if (ids & MessageDisplayHelper::DisplayTexts::Title)
451         setNotificationTitle(texts.title);
452 
453     bool textChanged = false;
454     if (ids & MessageDisplayHelper::DisplayTexts::Time)
455     {
456         mTimeText = texts.time;
457         textChanged = true;
458     }
459 
460     if (ids & MessageDisplayHelper::DisplayTexts::RemainingTime)
461     {
462         mRemainingText = texts.remainingTime;
463         textChanged = true;
464     }
465 
466     if (ids & MessageDisplayHelper::DisplayTexts::MessageAppend)
467     {
468         // More output is available from the command which is providing the text
469         // for this notification. Add the output, but don't show the notification
470         // until all output has been received. This is a workaround for
471         // notification texts not being reliably updated by setText().
472         mMessageText += change;
473         return;
474     }
475 
476     if (textChanged)
477         setNotificationText();
478 
479     // Update the notification. Note that this does nothing if no changes have occurred.
480     update();
481 }
482 
483 /******************************************************************************
484 * Called when the command providing the alarm message text has exited.
485 * Because setText() doesn't reliably update the text in the notification,
486 * command output notifications are not displayed until all the text is
487 * available to display.
488 * 'success' is true if the command did not fail completely.
489 */
commandCompleted(bool success)490 void MessageNotification::commandCompleted(bool success)
491 {
492     qCDebug(KALARM_LOG) << "MessageNotification::commandCompleted:" << success;
493     if (!success)
494     {
495         // The command failed completely. KAlarmApp will output an error
496         // message, so don't display the empty notification.
497         deleteLater();
498     }
499     else
500     {
501         // The command may have produced some output, so display that, although
502         // if an error occurred, KAlarmApp might display an error message as
503         // well.
504         setNotificationText();
505         mCommandInhibit = false;
506         showDisplay();
507     }
508 }
509 
510 /******************************************************************************
511 * Set the notification's title.
512 */
setNotificationTitle(const QString & text)513 void MessageNotification::setNotificationTitle(const QString& text)
514 {
515     setTitle(mErrorMsgs().isEmpty() ? QString() : text);
516 }
517 
518 /******************************************************************************
519 * Set the notification's text by combining the text portions.
520 */
setNotificationText()521 void MessageNotification::setNotificationText()
522 {
523     setText(mMessageText + NL + mTimeText + NL + QStringLiteral("<i>") + mRemainingText + QStringLiteral("</i>"));
524 update();
525 }
526 
527 /******************************************************************************
528 * Set the notification's action buttons.
529 */
setNotificationButtons()530 void MessageNotification::setNotificationButtons()
531 {
532     mEditButtonIndex = -1;
533     mDeferButtonIndex = -1;
534     QStringList buttons;
535     if (mEnableEdit)
536     {
537         mEditButtonIndex = 0;
538         buttons += i18nc("@action:button", "Edit");
539     }
540     if (mEnableDefer)
541     {
542         mDeferButtonIndex = buttons.count();
543         buttons += i18nc("@action:button", "Defer");
544     }
545     setActions(buttons);
546     setDefaultAction(KAboutData::applicationData().displayName());
547 }
548 
isDeferButtonEnabled() const549 bool MessageNotification::isDeferButtonEnabled() const
550 {
551     return mEnableDefer;
552 }
553 
enableDeferButton(bool enable)554 void MessageNotification::enableDeferButton(bool enable)
555 {
556     mEnableDefer = enable;
557     setNotificationButtons();
558 }
559 
enableEditButton(bool enable)560 void MessageNotification::enableEditButton(bool enable)
561 {
562     mEnableEdit = enable;
563     setNotificationButtons();
564 }
565 
566 /******************************************************************************
567 * Save settings to the session managed config file, for restoration
568 * when the program is restored.
569 */
saveProperties(KConfigGroup & config)570 void MessageNotification::saveProperties(KConfigGroup& config)
571 {
572     Q_UNUSED(config)
573 #ifdef RESTORE_NOTIFICATIONS
574     if (mDisplayComplete  &&  mHelper->saveProperties(config))
575         config.writeEntry("NotifyId", eventId());
576 #endif
577 }
578 
579 /******************************************************************************
580 * Called when a button in the notification has been pressed.
581 * Button indexes start at 1.
582 */
buttonActivated(unsigned int index)583 void MessageNotification::buttonActivated(unsigned int index)
584 {
585     int i = static_cast<int>(index);
586     if (i == 0)
587     {
588         displayMainWindow();
589     }
590     else if (i == mEditButtonIndex + 1)
591     {
592         if (mHelper->createEdit())
593             mHelper->executeEdit();
594     }
595     else if (i == mDeferButtonIndex + 1)
596     {
597         DeferDlgData* data = createDeferDlg(this, true);
598         executeDeferDlg(data);
599     }
600 }
601 
602 /******************************************************************************
603 * Called when the notification has closed, either by user action of by timeout.
604 * Note that when a notification has timed out, it shows in the notification
605 * history, but there is no way to know if the user closes it there.
606 * Only quits the application if there is no system tray icon displayed.
607 */
slotClosed()608 void MessageNotification::slotClosed()
609 {
610     qCDebug(KALARM_LOG) << "MessageNotification::slotClosed";
611     mHelper->closeEvent();
612 }
613 
614 #include "messagenotification.moc"
615 
616 // vim: et sw=4:
617