File indexing completed on 2024-11-17 05:15:19

0001 /*
0002  * Copyright 2020 Han Young <hanyoung@protonmail.com>
0003  * Copyright 2020-2022 Devin Lin <devin@kde.org>
0004  *
0005  * SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "alarm.h"
0009 
0010 #include "alarmadaptor.h"
0011 #include "kclockdsettings.h"
0012 #include "utilities.h"
0013 
0014 #include <KConfigGroup>
0015 #include <KSharedConfig>
0016 
0017 #include <QDBusConnection>
0018 #include <QDBusMessage>
0019 #include <QDateTime>
0020 #include <QJsonDocument>
0021 #include <QJsonObject>
0022 
0023 // alarm from json (loaded from storage)
0024 Alarm::Alarm(const QString &serialized, AlarmModel *parent)
0025     : QObject{parent}
0026 {
0027     if (!serialized.isEmpty()) {
0028         QJsonDocument doc = QJsonDocument::fromJson(serialized.toUtf8());
0029         QJsonObject obj = doc.object();
0030 
0031         m_uuid = QUuid::fromString(obj[QStringLiteral("uuid")].toString());
0032         m_name = obj[QStringLiteral("name")].toString();
0033         m_enabled = obj[QStringLiteral("enabled")].toBool();
0034         m_hours = obj[QStringLiteral("hours")].toInt();
0035         m_minutes = obj[QStringLiteral("minutes")].toInt();
0036         m_daysOfWeek = obj[QStringLiteral("daysOfWeek")].toInt();
0037         m_audioPath = QUrl::fromLocalFile(obj[QStringLiteral("audioPath")].toString());
0038         m_ringDuration = obj[QStringLiteral("ringDuration")].toInt(5);
0039         m_snoozeDuration = obj[QStringLiteral("snoozeDuration")].toInt(5);
0040         m_snoozedLength = obj[QStringLiteral("snoozedLength")].toInt();
0041     }
0042 
0043     init(parent);
0044 }
0045 
0046 Alarm::Alarm(const QString &name, int hours, int minutes, int daysOfWeek, QString audioPath, int ringDuration, int snoozeDuration, AlarmModel *parent)
0047     : QObject{parent}
0048     , m_uuid{QUuid::createUuid()}
0049     , m_name{name}
0050     , m_enabled{true} // default the alarm to be on
0051     , m_hours{hours}
0052     , m_minutes{minutes}
0053     , m_daysOfWeek{daysOfWeek}
0054     , m_audioPath{QUrl::fromLocalFile(audioPath.replace(QStringLiteral("file://"), QString()))}
0055     , m_ringDuration{ringDuration}
0056     , m_snoozeDuration{snoozeDuration}
0057 {
0058     init(parent);
0059 }
0060 
0061 Alarm::~Alarm()
0062 {
0063     // ensure alarm doesn't continue ringing after deletion
0064     if (m_ringing) {
0065         AlarmPlayer::instance().stop();
0066     }
0067 }
0068 
0069 void Alarm::init(AlarmModel *parent)
0070 {
0071     // setup notification
0072     m_notification->setIconName(QStringLiteral("kclock"));
0073     m_notification->setTitle(name() == QString() ? i18n("Alarm") : name());
0074     m_notification->setText(QLocale::system().toString(QTime::currentTime(), QLocale::ShortFormat));
0075     m_notification->setAutoDelete(false); // don't auto-delete when closing
0076 
0077     auto defaultAction = m_notification->addDefaultAction(i18n("View"));
0078     connect(defaultAction, &KNotificationAction::activated, this, &Alarm::dismiss);
0079 
0080     auto dismissAction = m_notification->addAction(i18n("Dismiss"));
0081     connect(dismissAction, &KNotificationAction::activated, this, &Alarm::dismiss);
0082 
0083     auto snoozeAction = m_notification->addAction(i18n("Snooze"));
0084     connect(snoozeAction, &KNotificationAction::activated, this, &Alarm::snooze);
0085 
0086     connect(m_notification, &KNotification::closed, this, &Alarm::dismiss);
0087 
0088     // setup signals
0089     connect(this, &Alarm::rescheduleRequested, this, &Alarm::save);
0090     connect(this, &Alarm::rescheduleRequested, this, &Alarm::calculateNextRingTime);
0091     calculateNextRingTime();
0092 
0093     if (parent) {
0094         // must be after calculateNextTime signal, to ensure the ring time is calculated before the alarm is scheduled
0095         connect(this, &Alarm::rescheduleRequested, parent, &AlarmModel::scheduleAlarm);
0096     }
0097 
0098     // setup dbus
0099     new AlarmAdaptor(this);
0100     QDBusConnection::sessionBus().registerObject(QStringLiteral("/Alarms/") + m_uuid.toString(QUuid::Id128), this);
0101     connect(this, &QObject::destroyed, [this] {
0102         QDBusConnection::sessionBus().unregisterObject(QStringLiteral("/Alarms/") + m_uuid.toString(QUuid::Id128), QDBusConnection::UnregisterNode);
0103     });
0104 
0105     // setup ringing length timer
0106     connect(m_ringTimer, &QTimer::timeout, this, []() {
0107         AlarmPlayer::instance().stop();
0108     });
0109 }
0110 
0111 // alarm to json
0112 QString Alarm::serialize()
0113 {
0114     QJsonObject obj;
0115     obj[QStringLiteral("uuid")] = m_uuid.toString();
0116     obj[QStringLiteral("name")] = m_name;
0117     obj[QStringLiteral("enabled")] = m_enabled;
0118     obj[QStringLiteral("hours")] = m_hours;
0119     obj[QStringLiteral("minutes")] = m_minutes;
0120     obj[QStringLiteral("daysOfWeek")] = m_daysOfWeek;
0121     obj[QStringLiteral("audioPath")] = m_audioPath.toLocalFile();
0122     obj[QStringLiteral("ringDuration")] = m_ringDuration;
0123     obj[QStringLiteral("snoozeDuration")] = m_snoozeDuration;
0124     obj[QStringLiteral("snoozedLength")] = m_snoozedLength;
0125     QByteArray data = QJsonDocument(obj).toJson(QJsonDocument::Compact);
0126     return QString::fromStdString(data.toStdString());
0127 }
0128 
0129 QString Alarm::uuid() const
0130 {
0131     return m_uuid.toString();
0132 }
0133 
0134 QString Alarm::name() const
0135 {
0136     return m_name;
0137 }
0138 
0139 void Alarm::setName(const QString &name)
0140 {
0141     if (name != m_name) {
0142         m_name = name;
0143         Q_EMIT nameChanged();
0144     }
0145 }
0146 
0147 bool Alarm::enabled() const
0148 {
0149     return m_enabled;
0150 }
0151 
0152 void Alarm::setEnabled(bool enabled)
0153 {
0154     if (m_enabled != enabled) {
0155         setSnoozedLength(0); // reset snooze value
0156         setNextRingTime(0); // reset next ring time
0157         m_enabled = enabled;
0158         Q_EMIT enabledChanged();
0159         Q_EMIT rescheduleRequested(); // notify the AlarmModel to reschedule
0160     }
0161 }
0162 
0163 int Alarm::hours() const
0164 {
0165     return m_hours;
0166 }
0167 
0168 void Alarm::setHours(int hours)
0169 {
0170     if (hours != m_hours) {
0171         m_hours = hours;
0172         Q_EMIT hoursChanged();
0173         Q_EMIT rescheduleRequested();
0174     }
0175 }
0176 
0177 int Alarm::minutes() const
0178 {
0179     return m_minutes;
0180 }
0181 
0182 void Alarm::setMinutes(int minutes)
0183 {
0184     if (minutes != m_minutes) {
0185         m_minutes = minutes;
0186         Q_EMIT minutesChanged();
0187         Q_EMIT rescheduleRequested();
0188     }
0189 }
0190 
0191 int Alarm::daysOfWeek() const
0192 {
0193     return m_daysOfWeek;
0194 }
0195 
0196 void Alarm::setDaysOfWeek(int daysOfWeek)
0197 {
0198     if (daysOfWeek != m_daysOfWeek) {
0199         m_daysOfWeek = daysOfWeek;
0200         Q_EMIT daysOfWeekChanged();
0201         Q_EMIT rescheduleRequested();
0202     }
0203 }
0204 
0205 QString Alarm::audioPath() const
0206 {
0207     return m_audioPath.toLocalFile();
0208 }
0209 
0210 void Alarm::setAudioPath(QString path)
0211 {
0212     if (m_audioPath.path() != path) {
0213         path = path.replace(QStringLiteral("file://"), QString());
0214         m_audioPath = QUrl::fromLocalFile(path);
0215         Q_EMIT audioPathChanged();
0216     }
0217 }
0218 
0219 int Alarm::ringDuration() const
0220 {
0221     return m_ringDuration;
0222 }
0223 
0224 void Alarm::setRingDuration(int ringDuration)
0225 {
0226     if (ringDuration != m_ringDuration) {
0227         m_ringDuration = ringDuration;
0228         Q_EMIT ringDurationChanged();
0229     }
0230 }
0231 
0232 int Alarm::snoozeDuration() const
0233 {
0234     return m_snoozeDuration;
0235 }
0236 
0237 void Alarm::setSnoozeDuration(int snoozeDuration)
0238 {
0239     if (snoozeDuration != m_snoozeDuration) {
0240         m_snoozeDuration = snoozeDuration;
0241         Q_EMIT snoozeDurationChanged();
0242     }
0243 }
0244 
0245 int Alarm::snoozedLength() const
0246 {
0247     return m_snoozedLength;
0248 }
0249 
0250 void Alarm::setSnoozedLength(int snoozedLength)
0251 {
0252     if (snoozedLength != m_snoozedLength) {
0253         m_snoozedLength = snoozedLength;
0254         Q_EMIT snoozedLengthChanged();
0255     }
0256 }
0257 
0258 bool Alarm::ringing() const
0259 {
0260     return m_ringing;
0261 }
0262 
0263 void Alarm::setRinging(bool ringing)
0264 {
0265     if (ringing != m_ringing) {
0266         m_ringing = ringing;
0267         Q_EMIT ringingChanged();
0268     }
0269 }
0270 
0271 void Alarm::save()
0272 {
0273     // save this alarm to the config
0274     auto config = KSharedConfig::openConfig();
0275     KConfigGroup group = config->group(ALARM_CFG_GROUP);
0276     group.writeEntry(m_uuid.toString(), this->serialize());
0277     group.sync();
0278 }
0279 
0280 void Alarm::ring()
0281 {
0282     if (!m_enabled) {
0283         return;
0284     }
0285 
0286     qDebug() << "Ringing alarm" << m_name << "and sending notification...";
0287 
0288     // save ring time
0289     if (m_snoozedLength == 0) {
0290         m_originalRingTime = m_nextRingTime;
0291     }
0292 
0293     // reset snoozed length (if snooze happens, this will get set again)
0294     setSnoozedLength(0);
0295 
0296     // send notification
0297     m_notification->setText(QLocale::system().toString(QTime::currentTime(), QLocale::ShortFormat));
0298     m_notification->sendEvent();
0299 
0300     // wake up device
0301     Utilities::wakeupNow();
0302 
0303     // pause playing mpris media sources
0304     Utilities::pauseMprisSources();
0305 
0306     // play sound (it will loop)
0307     qDebug() << "Alarm sound: " << m_audioPath;
0308     AlarmPlayer::instance().setSource(m_audioPath);
0309     AlarmPlayer::instance().play();
0310     setRinging(true);
0311 
0312     // stop ringing after duration
0313     m_ringTimer->start(m_ringDuration * 60 * 1000);
0314 }
0315 
0316 void Alarm::dismiss()
0317 {
0318     qDebug() << "Alarm" << m_name << "dismissed";
0319 
0320     // ignore if the snooze button was pressed and dismiss is still called
0321     if (!m_justSnoozed && daysOfWeek() == 0) {
0322         // disable alarm if set to run once
0323         setEnabled(false);
0324     }
0325     m_justSnoozed = false;
0326 
0327     AlarmPlayer::instance().stop();
0328     Utilities::resumeMprisSources();
0329 
0330     setRinging(false);
0331     m_ringTimer->stop();
0332     m_notification->close();
0333     Q_EMIT rescheduleRequested();
0334 
0335     Utilities::instance().decfActiveCount();
0336 }
0337 
0338 void Alarm::snooze()
0339 {
0340     qDebug() << "Alarm snoozed (" << m_snoozeDuration << " minutes)";
0341     AlarmPlayer::instance().stop();
0342     Utilities::resumeMprisSources();
0343 
0344     setRinging(false);
0345     m_ringTimer->stop();
0346     m_notification->close();
0347     m_justSnoozed = true;
0348 
0349     // add snooze amount, and enable alarm
0350     setSnoozedLength((QDateTime::currentSecsSinceEpoch() + 60 * m_snoozeDuration) - m_originalRingTime);
0351     m_enabled = true; // can't use setEnabled, since it will reset snoozedLength
0352     Q_EMIT enabledChanged();
0353     Q_EMIT rescheduleRequested();
0354 
0355     Utilities::instance().decfActiveCount();
0356 }
0357 
0358 void Alarm::calculateNextRingTime()
0359 {
0360     if (!this->m_enabled) { // if not enabled, means this would never ring
0361         setNextRingTime(0);
0362         return;
0363     }
0364 
0365     // get the time that the alarm will ring on the day
0366     QTime alarmTime = QTime(m_hours, m_minutes, 0).addSecs(m_snoozedLength);
0367     QDateTime date = QDateTime::currentDateTime();
0368 
0369     if (this->m_daysOfWeek == 0) { // alarm does not repeat (no days of the week are specified)
0370         // either the alarm occurs today or tomorrow
0371         bool alarmOccursToday = alarmTime >= date.time();
0372         setNextRingTime(QDateTime(date.date().addDays(alarmOccursToday ? 0 : 1), alarmTime).toSecsSinceEpoch());
0373 
0374     } else { // repeat alarm
0375         bool first = true;
0376 
0377         // keeping looping forward a single day until the day of week is accepted
0378         while (((this->m_daysOfWeek & (1 << (date.date().dayOfWeek() - 1))) == 0) // check day
0379                || (first && (alarmTime < date.time()))) // check time if the current day is accepted (keep looping forward if alarmTime has passed)
0380         {
0381             date = date.addDays(1); // go forward a day
0382             first = false;
0383         }
0384 
0385         setNextRingTime(QDateTime(date.date(), alarmTime).toSecsSinceEpoch());
0386     }
0387 }
0388 
0389 void Alarm::setNextRingTime(quint64 nextRingTime)
0390 {
0391     if (m_nextRingTime != nextRingTime) {
0392         m_nextRingTime = nextRingTime;
0393         Q_EMIT nextRingTimeChanged();
0394     }
0395 }
0396 
0397 quint64 Alarm::nextRingTime()
0398 {
0399     // day changed, re-calculate
0400     if (this->m_nextRingTime <= static_cast<unsigned long long>(QDateTime::currentSecsSinceEpoch())) {
0401         calculateNextRingTime();
0402     }
0403     return m_nextRingTime;
0404 }