Commit f395fd2c authored by Volker Krause's avatar Volker Krause
Browse files

Move the reminder daemon from Kalendar here

We'll use that as shared infrastructure going forward, replacing the
application-specific implementations.
parent 810f5ada
Pipeline #146258 passed with stage
in 1 minute and 17 seconds
......@@ -7,6 +7,8 @@ Dependencies:
'frameworks/extra-cmake-modules': '@latest'
'frameworks/kcontacts' : '@latest'
'frameworks/kcalendarcore' : '@latest'
'frameworks/knotifications' : '@latest'
'frameworks/kdbusaddons' : '@latest'
'pim/akonadi' : '@same'
'pim/akonadi-contacts' : '@same'
'pim/kcalutils' : '@same'
......
......@@ -57,6 +57,8 @@ find_package(KF5WidgetsAddons ${KF5_MIN_VERSION} CONFIG REQUIRED)
find_package(KF5XmlGui ${KF5_MIN_VERSION} CONFIG REQUIRED)
find_package(KF5KIO ${KF5_MIN_VERSION} CONFIG REQUIRED)
find_package(KF5Codecs ${KF5_MIN_VERSION} CONFIG REQUIRED)
find_package(KF5DBusAddons ${KF5_MIN_VERSION} CONFIG REQUIRED)
find_package(KF5Notifications ${KF5_MIN_VERSION} CONFIG REQUIRED)
find_package(KF5MailTransportAkonadi ${MAILTRANSPORT_LIB_VERSION} CONFIG REQUIRED)
find_package(KF5IdentityManagement ${IDENTITYMANAGEMENT_LIB_VERSION} CONFIG REQUIRED)
......@@ -83,6 +85,7 @@ endif()
add_subdirectory(src)
add_subdirectory(serializers)
add_subdirectory(reminder-daemon)
if (BUILD_TESTING)
add_subdirectory(autotests)
endif ()
......
# SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
#
# SPDX-License-Identifier: BSD-2-Clause
add_executable(kalendarac)
add_definitions(-DTRANSLATION_DOMAIN=\"kalendarac\")
qt_add_dbus_interface(kalendarac_SRCS org.kde.calendar.Calendar.xml calendarinterface)
target_sources(kalendarac PRIVATE
kalendaralarmclient.cpp
alarmnotification.cpp
kalendaralarmclient.h
alarmnotification.h
kalendaracmain.cpp
${kalendarac_SRCS}
)
target_include_directories(kalendarac PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(kalendarac
KF5::CoreAddons
KF5::ConfigCore
KF5::CalendarCore
KF5::DBusAddons
KF5::AkonadiCore
KF5::AkonadiCalendar
KF5::KIOGui
KF5::I18n
KF5::Notifications
Qt::Core
Qt::DBus
)
ecm_qt_declare_logging_category(kalendarac
HEADER logging.h
IDENTIFIER Log
CATEGORY_NAME org.kde.kalendarac
DESCRIPTION "Reminder daemon"
EXPORT REMINDER_DAEMON
)
if (COMPILE_WITH_UNITY_CMAKE_SUPPORT)
set_target_properties(kalendarac PROPERTIES UNITY_BUILD ON)
endif()
install(TARGETS
kalendarac ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}
)
install(FILES
org.kde.kalendarac.desktop
DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}
)
install(FILES kalendarac.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR})
set(SERV_EXEC ${KDE_INSTALL_FULL_BINDIR}/kalendarac)
configure_file(org.kde.kalendarac.service.in ${CMAKE_CURRENT_BINARY_DIR}/org.kde.kalendarac.service)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/org.kde.kalendarac.service DESTINATION ${KDE_INSTALL_FULL_DBUSSERVICEDIR})
ecm_qt_install_logging_categories(
EXPORT REMINDER_DAEMON
FILE org_kde_kalendarac.categories
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
)
#! /usr/bin/env bash
# SPDX-FileCopyrightText: 2020 Tobias Fella <fella@posteo.de>
# SPDX-License-Identifier: CC0-1.0
$XGETTEXT `find -name \*.cpp -o -name \*.qml -o -name \*.js` -o $podir/kalendarac.pot
/*
* SPDX-FileCopyrightText: 2019 Dimitris Kardarakos <dimkard@posteo.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#include "alarmnotification.h"
#include "kalendaralarmclient.h"
#include <KLocalizedString>
#include <QDebug>
#include <QDesktopServices>
#include <QRegularExpression>
#include <QUrlQuery>
AlarmNotification::AlarmNotification(const QString &uid)
: m_uid{uid}
, m_remind_at{QDateTime()}
{
}
AlarmNotification::~AlarmNotification()
{
// don't delete immediately, in case we end up here as a result
// of a signal from m_notification itself
m_notification->deleteLater();
}
void AlarmNotification::send(KalendarAlarmClient *client, const KCalendarCore::Incidence::Ptr &incidence)
{
const QDateTime startTime = m_occurrence.isValid() ? m_occurrence : incidence->dtStart();
const bool notificationExists = m_notification;
if (!notificationExists) {
m_notification = new KNotification(QStringLiteral("alarm"));
m_notification->setFlags(KNotification::Persistent);
// dismiss both with the explicit action and just closing the notification
// there is no signal for explicit closing though, we only can observe that
// indirectly from not having received a different signal before closed()
QObject::connect(m_notification, &KNotification::closed, client, [this, client]() {
client->dismiss(this);
});
QObject::connect(m_notification, &KNotification::defaultActivated, client, [this, client, startTime]() {
client->showIncidence(uid(), startTime, m_notification->xdgActivationToken());
});
QObject::connect(m_notification, &KNotification::action1Activated, client, [this, client]() {
client->suspend(this);
QObject::disconnect(m_notification, &KNotification::closed, client, nullptr);
});
QObject::connect(m_notification, &KNotification::action3Activated, client, [this]() {
QDesktopServices::openUrl(m_contextAction);
});
}
// change the content unconditionally, that will also update already existing notifications
m_notification->setTitle(incidence->summary());
m_notification->setText(m_text);
m_notification->setDefaultAction(i18n("View"));
if (!m_text.isEmpty() && m_text != incidence->summary()) { // MS Teams sometimes repeats the summary as the alarm text, we don't need that
m_notification->setText(m_text);
} else if (incidence->type() == KCalendarCore::Incidence::TypeTodo && !incidence->dtStart().isValid()) {
const auto todo = incidence.staticCast<KCalendarCore::Todo>();
m_notification->setText(i18n("Task due at %1", QLocale().toString(todo->dtDue().time(), QLocale::NarrowFormat)));
} else if (!incidence->allDay()) {
const QString incidenceType = incidence->type() == KCalendarCore::Incidence::TypeTodo ? i18n("Task") : i18n("Event");
const int startOffset = qRound(QDateTime::currentDateTime().secsTo(startTime) / 60.0);
if (startOffset > 0 && startOffset < 60) {
m_notification->setText(i18ncp("Event starts in 5 minutes", "%2 starts in %1 minute", "%2 starts in %1 minutes", startOffset, incidenceType));
} else {
m_notification->setText(
i18nc("Event starts at 10:00", "%1 starts at %2", incidenceType, QLocale().toString(startTime.time(), QLocale::NarrowFormat)));
}
}
m_notification->setIconName(incidence->type() == KCalendarCore::Incidence::TypeTodo ? QStringLiteral("view-task")
: QStringLiteral("view-calendar-upcoming"));
QStringList actions = {i18n("Remind in 5 mins"), i18n("Dismiss")};
const auto contextAction = determineContextAction(incidence);
if (!contextAction.isEmpty()) {
actions.push_back(contextAction);
}
m_notification->setActions(actions);
if (!notificationExists) {
m_notification->sendEvent();
}
}
QString AlarmNotification::uid() const
{
return m_uid;
}
QString AlarmNotification::text() const
{
return m_text;
}
void AlarmNotification::setText(const QString &alarmText)
{
m_text = alarmText;
}
QDateTime AlarmNotification::occurrence() const
{
return m_occurrence;
}
void AlarmNotification::setOccurrence(const QDateTime &occurrence)
{
m_occurrence = occurrence;
}
QDateTime AlarmNotification::remindAt() const
{
return m_remind_at;
}
void AlarmNotification::setRemindAt(const QDateTime &remindAtDt)
{
m_remind_at = remindAtDt;
}
bool AlarmNotification::hasValidContextAction() const
{
return m_contextAction.isValid() && (m_contextAction.scheme() == QLatin1String("https") || m_contextAction.scheme() == QLatin1String("geo"));
}
QString AlarmNotification::determineContextAction(const KCalendarCore::Incidence::Ptr &incidence)
{
// look for possible (meeting) URLs
m_contextAction = incidence->url();
if (!hasValidContextAction()) {
m_contextAction = QUrl(incidence->location());
}
if (!hasValidContextAction()) {
m_contextAction = QUrl(incidence->customProperty("MICROSOFT", "SKYPETEAMSMEETINGURL"));
}
if (!hasValidContextAction()) {
static QRegularExpression urlFinder(QStringLiteral(R"(https://[^\s>]*)"));
const auto match = urlFinder.match(incidence->description());
if (match.hasMatch()) {
m_contextAction = QUrl(match.captured());
}
}
if (hasValidContextAction()) {
return i18n("Open URL");
}
// navigate to location
if (incidence->hasGeo()) {
m_contextAction.clear();
m_contextAction.setScheme(QStringLiteral("geo"));
m_contextAction.setPath(QString::number(incidence->geoLatitude()) + QLatin1Char(',') + QString::number(incidence->geoLongitude()));
} else if (!incidence->location().isEmpty()) {
m_contextAction.clear();
m_contextAction.setScheme(QStringLiteral("geo"));
m_contextAction.setPath(QStringLiteral("0,0"));
QUrlQuery query;
query.addQueryItem(QStringLiteral("q"), incidence->location());
m_contextAction.setQuery(query);
}
if (hasValidContextAction()) {
return i18n("Map");
}
return QString();
}
/*
* SPDX-FileCopyrightText: 2019 Dimitris Kardarakos <dimkard@posteo.net>
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once
#include <KCalendarCore/Incidence>
#include <KNotification>
#include <QDateTime>
#include <QPointer>
#include <QUrl>
class KalendarAlarmClient;
/**
* @brief The alarm notification that should be displayed. It is a wrapper of a KNotification enhanced with alarm properties, like uid and remind time
*
*/
class AlarmNotification
{
public:
explicit AlarmNotification(const QString &uid);
~AlarmNotification();
/**
* @brief Sends the notification to be displayed
*/
void send(KalendarAlarmClient *client, const KCalendarCore::Incidence::Ptr &incidence);
/**
* @return The uid of the Incidence of the alarm of the notification
*/
QString uid() const;
/**
* @brief The text of the notification that should be displayed
*/
QString text() const;
/**
* @brief Sets the to-be-displayed text of the notification
*/
void setText(const QString &alarmText);
/** Occurrence time in case of recurring incidences. */
QDateTime occurrence() const;
void setOccurrence(const QDateTime &occurrence);
/**
* @return In case of a suspended notification, the time that the notification should be displayed. Otherwise, it is empty.
*/
QDateTime remindAt() const;
/**
* @brief Sets the time that should be displayed a suspended notification
*/
void setRemindAt(const QDateTime &remindAtDt);
private:
bool hasValidContextAction() const;
QString determineContextAction(const KCalendarCore::Incidence::Ptr &incidence);
QPointer<KNotification> m_notification;
QString m_uid;
QString m_text;
QDateTime m_occurrence;
QDateTime m_remind_at;
QUrl m_contextAction;
};
[Global]
IconName=appointment-reminder
DesktopEntry=org.kde.kalendar
Comment=Reminders about calendar events and tasks
Comment[ar]=مذكر بأحداث التقويم والمهام
Comment[az]=Təqvim hadisələri və tapşırıqları haqqında xxatırlatmalar
Comment[ca]=Recordatoris dels esdeveniments i les tasques del calendari
Comment[en_GB]=Reminders about calendar events and tasks
Comment[es]=Recordatorios de eventos de calendario y tareas
Comment[fr]=Rappels concernant les tâches et évènements de l'agenda
Comment[hu]=Emlékeztető naptáreseményekről és feladatokról
Comment[it]=Promemoria relativi a eventi e attività dei calendari
Comment[nl]=Herinneringen over agenda-afspraken en taken
Comment[sl]=Opomniki na dogodke in opravila
Comment[sv]=Påminnelser om kalenderhändelser och uppgifter
Comment[tr]=Takvim etkinlikleri ve görevleri hakkında hatırlatıcılar
Comment[uk]=Нагадування щодо подій та завдань з календаря
Comment[x-test]=xxReminders about calendar events and tasksxx
Name=Calendar Reminders
Name[ar]=عميل تذكير
Name[az]=Təqvim xatırlatmaları
Name[ca]=Recordatoris del calendari
Name[en_GB]=Calendar Reminders
Name[es]=Recordatorios de calendario
Name[fr]=Rappels d'agenda
Name[hu]=Naptáremlékeztetők
Name[it]=Promemoria dei calendari
Name[nl]=Agendaherinneringen
Name[sl]=Opomniki koledarja
Name[sv]=Kalenderpåminnelser
Name[tr]=Takvim Hatırlatıcıları
Name[uk]=Нагадування календаря
Name[x-test]=xxCalendar Remindersxx
[Event/alarm]
Name=Alarm
Name[ar]=المنبه
Name[az]=Səsli xatırlatma
Name[ca]=Alarma
Name[ca@valencia]=Alarma
Name[cs]=Alarm
Name[en_GB]=Alarm
Name[es]=Alarma
Name[fi]=Hälytys
Name[fr]=Alarme
Name[hu]=Riasztás
Name[it]=Avviso
Name[nl]=Alarm
Name[pl]=Alarm
Name[pt]=Alarme
Name[pt_BR]=Alarme
Name[sl]=Alarm
Name[sv]=Alarm
Name[tr]=Çalar Saat
Name[uk]=Нагадування
Name[x-test]=xxAlarmxx
Contexts=uid
Comment=Alarm Notification
Comment[ar]=إشعار المنبه
Comment[az]=Səsli xatırlatma bildirişi
Comment[ca]=Notificació de l'alarma
Comment[ca@valencia]=Notificació de l'alarma
Comment[cs]=Oznámení upomínky
Comment[en_GB]=Alarm Notification
Comment[es]=Notificación de la alarma
Comment[fi]=Hälytysilmoitus
Comment[fr]=Notification d'alarme
Comment[hu]=Riasztásértesítés
Comment[it]=Notifica dell'avviso
Comment[nl]=Alarmmelding
Comment[pl]=Powiadomienie o alarmie
Comment[pt]=Notificação do Alarme
Comment[pt_BR]=Notificação de alarme
Comment[sl]=Obvestilo o alarmu
Comment[sv]=Alarmunderrättelse
Comment[tr]=Çalar Saat Bildirimi
Comment[uk]=Сповіщення для нагадування
Comment[x-test]=xxAlarm Notificationxx
Action=Popup|Sound
Sound=freedesktop/stereo/bell.oga
# SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
# SPDX-License-Identifier: CC0-1.0
// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "kalendaralarmclient.h"
#include <KAboutData>
#include <KDBusService>
#include <KLocalizedString>
#include <QCommandLineParser>
#include <QGuiApplication>
#include <akonadi-calendar_version.h>
int main(int argc, char **argv)
{
QGuiApplication app(argc, argv);
app.setQuitOnLastWindowClosed(false);
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
app.setAttribute(Qt::AA_UseHighDpiPixmaps, true);
#endif
KAboutData aboutData(
// The program name used internally.
QStringLiteral("kalendarac"),
// A displayable program name string.
i18nc("@title", "Reminders"),
QStringLiteral(AKONADICALENDAR_VERSION_STRING),
// Short description of what the app does.
i18n("Calendar Reminder Service"),
// The license this code is released under.
KAboutLicense::GPL,
// Copyright Statement.
i18n("(c) KDE Community 2021-2022"));
aboutData.addAuthor(i18nc("@info:credit", "Carl Schwan"),
i18nc("@info:credit", "Maintainer"),
QStringLiteral("carl@carlschwan.eu"),
QStringLiteral("https://carlschwan.eu"));
aboutData.addAuthor(i18nc("@info:credit", "Clau Cambra"),
i18nc("@info:credit", "Maintainer"),
QStringLiteral("claudio.cambra@gmail.com"),
QStringLiteral("https://claudiocambra.com"));
KAboutData::setApplicationData(aboutData);
QCommandLineParser parser;
KAboutData::setApplicationData(aboutData);
aboutData.setupCommandLine(&parser);
parser.process(app);
aboutData.processCommandLine(&parser);
KDBusService service(KDBusService::Unique);
KalendarAlarmClient client;
return app.exec();
}
// SPDX-FileCopyrightText: 2021 Claudio Cambra <claudio.cambra@gmail.com>
// SPDX-License-Identifier: LGPL-2.1-or-later
#include "kalendaralarmclient.h"
#include "alarmnotification.h"
#include "calendarinterface.h"
#include "logging.h"
#include <akonadi-calendar_version.h>
#include <KIO/ApplicationLauncherJob>
#include <KCheckableProxyModel>
#include <KConfigGroup>
#include <KLocalizedString>
#include <KSharedConfig>
#include <QDateTime>
#include <QFileInfo>
using namespace KCalendarCore;
KalendarAlarmClient::KalendarAlarmClient(QObject *parent)
: QObject(parent)
{
mCheckTimer.setSingleShot(true);
mCheckTimer.setTimerType(Qt::VeryCoarseTimer);
// Check if Akonadi is already configured
const QString akonadiConfigFile = Akonadi::ServerManager::serverConfigFilePath(Akonadi::ServerManager::ReadWrite);
if (QFileInfo::exists(akonadiConfigFile)) {
// Akonadi is configured, create ETM and friends, which will start Akonadi
// if its not running yet
setupAkonadi();
} else {
// Akonadi has not been set up yet, wait for someone else to start it,
// so that we don't unnecessarily slow session start up
connect(Akonadi::ServerManager::self(), &Akonadi::ServerManager::stateChanged, this, [this](Akonadi::ServerManager::State state) {
if (state == Akonadi::ServerManager::Running) {
setupAkonadi();
}
});
}
KConfigGroup alarmGroup(KSharedConfig::openConfig(), "Alarms");
mLastChecked = alarmGroup.readEntry("CalendarsLastChecked", QDateTime::currentDateTime().addDays(-9));
restoreSuspendedFromConfig();
}
KalendarAlarmClient::~KalendarAlarmClient() = default;
void KalendarAlarmClient::setupAkonadi()
{
const QStringList mimeTypes{Event::eventMimeType(), Todo::todoMimeType()};
mCalendar = Akonadi::ETMCalendar::Ptr(new Akonadi::ETMCalendar(mimeTypes));
mCalendar->setObjectName(QStringLiteral("KalendarAC's calendar"));
mETM = mCalendar->entityTreeModel();
connect(&mCheckTimer, &QTimer::timeout, this, &KalendarAlarmClient::checkAlarms);
connect(mETM, &Akonadi::EntityTreeModel::collectionPopulated, this, &KalendarAlarmClient::deferredInit);
connect(mETM, &Akonadi::EntityTreeModel::collectionTreeFetched, this, &KalendarAlarmClient::deferredInit);
checkAlarms();
}
void checkAllItems(KCheckableProxyModel *model, const QModelIndex &parent = QModelIndex())
{
const int rowCount = model->rowCount(parent);
for (int row = 0; row < rowCount; ++row) {
QModelIndex index = model->index(row, 0, parent);
model->setData(index, Qt::Checked, Qt::CheckStateRole);
if (model->rowCount(index) > 0) {
checkAllItems(model, index);
}
}
}
void KalendarAlarmClient::deferredInit()
{
if (!collectionsAvailable()) {
return;
}
qCDebug(Log) << "Performing delayed initialization.";
KCheckableProxyModel *checkableModel = mCalendar->checkableProxyModel();
checkAllItems(checkableModel);
// Now that everything is set up, a first check for reminders can be performed.
checkAlarms();
}
void KalendarAlarmClient::restoreSuspendedFromConfig()
{
qCDebug(Log) << "Restore suspended alarms from config";
const KConfigGroup suspendedGroup(KSharedConfig::openConfig(), "Suspended");
const auto suspendedAlarms = suspendedGroup.groupList();
for (const auto &s : suspendedAlarms) {
const KConfigGroup suspendedAlarm(&suspendedGroup, s);
const QString uid = suspendedAlarm.readEntry("UID");
const QString txt = suspendedAlarm.readEntry("Text");
const QDateTime occurrence = suspendedAlarm.readEntry("Occurrence", QDateTime());
const QDateTime remindAt = suspendedAlarm.readEntry("RemindAt", QDateTime());
qCDebug(Log) << "restoreSuspendedFromConfig: Restoring alarm" << uid << "," << txt << "," << remindAt;
if (!uid.isEmpty() && remindAt.isValid()) {
addNotification(uid, txt, occurrence, remindAt);
}
}
}
void KalendarAlarmClient::dismiss(AlarmNotification *notification)
{
qCDebug(Log) << "Alarm" << notification->uid() << "dismissed";
removeNotification(notification);
m_notifications.remove(notification->uid());
delete notification;
}
void KalendarAlarmClient::suspend(AlarmNotification *notification)