File indexing completed on 2024-04-21 04:56:51

0001 /**
0002  * SPDX-FileCopyrightText: 2013 Albert Vaca <albertvaka@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "notification.h"
0008 #include "plugin_notifications_debug.h"
0009 
0010 #include <KLocalizedString>
0011 #include <KNotification>
0012 #include <KNotificationReplyAction>
0013 #include <QFile>
0014 #include <QIcon>
0015 #include <QJsonArray>
0016 #include <QPixmap>
0017 #include <QString>
0018 #include <QUrl>
0019 #include <QtGlobal>
0020 #include <knotifications_version.h>
0021 
0022 #include <core/filetransferjob.h>
0023 #include <core/notificationserverinfo.h>
0024 
0025 QMap<QString, FileTransferJob *> Notification::s_downloadsInProgress;
0026 
0027 Notification::Notification(const NetworkPacket &np, const Device *device, QObject *parent)
0028     : QObject(parent)
0029     , m_imagesDir()
0030     , m_device(device)
0031 {
0032     // Make a own directory for each user so no one can see each others icons
0033     QString username;
0034 #ifdef Q_OS_WIN
0035     username = QString::fromLatin1(qgetenv("USERNAME"));
0036 #else
0037     username = QString::fromLatin1(qgetenv("USER"));
0038 #endif
0039 
0040     m_imagesDir.setPath(QDir::temp().absoluteFilePath(QStringLiteral("kdeconnect_") + username));
0041     m_imagesDir.mkpath(m_imagesDir.absolutePath());
0042     QFile(m_imagesDir.absolutePath()).setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner);
0043     m_ready = false;
0044 
0045     parseNetworkPacket(np);
0046     createKNotification(np);
0047 
0048 #if QT_VERSION_MAJOR == 5
0049     connect(m_notification, &KNotification::activated, this, [this](unsigned int actionIndex) {
0050         // Notification action indices start at 1
0051         Q_EMIT actionTriggered(m_internalId, m_actions[actionIndex - 1]);
0052     });
0053 #endif
0054 }
0055 
0056 void Notification::dismiss()
0057 {
0058     if (m_dismissable) {
0059         Q_EMIT dismissRequested(m_internalId);
0060     }
0061 }
0062 
0063 void Notification::show()
0064 {
0065     m_ready = true;
0066     Q_EMIT ready();
0067     if (!m_silent) {
0068         m_notification->sendEvent();
0069     }
0070 }
0071 
0072 void Notification::update(const NetworkPacket &np)
0073 {
0074     parseNetworkPacket(np);
0075     createKNotification(np);
0076 }
0077 
0078 void Notification::createKNotification(const NetworkPacket &np)
0079 {
0080     if (!m_notification) {
0081         m_notification = new KNotification(QStringLiteral("notification"), KNotification::CloseOnTimeout, this);
0082         m_notification->setComponentName(QStringLiteral("kdeconnect"));
0083         m_notification->setHint(QStringLiteral("resident"),
0084                                 true); // This means the notification won't be deleted automatically, but only with KNotifications 5.81
0085     }
0086 
0087     QString escapedTitle = m_title.toHtmlEscaped();
0088     // notification title text does not have markup, but in some cases below it is used in body text so we escape it
0089     QString escapedText = m_text.toHtmlEscaped();
0090     QString escapedTicker = m_ticker.toHtmlEscaped();
0091 
0092     if (NotificationServerInfo::instance().supportedHints().testFlag(NotificationServerInfo::X_KDE_DISPLAY_APPNAME)) {
0093         m_notification->setTitle(m_title);
0094         m_notification->setText(escapedText);
0095         m_notification->setHint(QStringLiteral("x-kde-display-appname"), m_appName.toHtmlEscaped());
0096     } else {
0097         m_notification->setTitle(m_appName);
0098 
0099         if (m_title.isEmpty() && m_text.isEmpty()) {
0100             m_notification->setText(escapedTicker);
0101         } else if (m_appName == m_title) {
0102             m_notification->setText(escapedText);
0103         } else if (m_title.isEmpty()) {
0104             m_notification->setText(escapedText);
0105         } else if (m_text.isEmpty()) {
0106             m_notification->setText(escapedTitle);
0107         } else {
0108             m_notification->setText(escapedTitle + QStringLiteral(": ") + escapedText);
0109         }
0110     }
0111 
0112     m_notification->setHint(QStringLiteral("x-kde-origin-name"), m_device->name());
0113 
0114     if (!m_requestReplyId.isEmpty()) {
0115         auto replyAction = std::make_unique<KNotificationReplyAction>(i18nc("@action:button", "Reply"));
0116         replyAction->setPlaceholderText(i18nc("@info:placeholder", "Reply to %1...", m_appName));
0117         replyAction->setFallbackBehavior(KNotificationReplyAction::FallbackBehavior::UseRegularAction);
0118         QObject::connect(replyAction.get(), &KNotificationReplyAction::replied, this, &Notification::replied);
0119         QObject::connect(replyAction.get(), &KNotificationReplyAction::activated, this, &Notification::reply);
0120         m_notification->setReplyAction(std::move(replyAction));
0121     }
0122 
0123 #if QT_VERSION_MAJOR == 6
0124     m_notification->clearActions();
0125     for (const QString &actionId : std::as_const(m_actions)) {
0126         KNotificationAction *action = m_notification->addAction(actionId);
0127 
0128         connect(action, &KNotificationAction::activated, this, [this, actionId] {
0129             Q_EMIT actionTriggered(m_internalId, actionId);
0130         });
0131     }
0132 #else
0133     m_notification->setActions(m_actions);
0134 #endif
0135 
0136     m_hasIcon = m_hasIcon && !m_payloadHash.isEmpty();
0137 
0138     if (!m_hasIcon) {
0139         show();
0140     } else {
0141         m_iconPath = m_imagesDir.absoluteFilePath(m_payloadHash);
0142         loadIcon(np);
0143     }
0144 }
0145 
0146 void Notification::loadIcon(const NetworkPacket &np)
0147 {
0148     m_ready = false;
0149 
0150     if (QFileInfo::exists(m_iconPath)) {
0151         applyIcon();
0152         show();
0153     } else {
0154         FileTransferJob *fileTransferJob = s_downloadsInProgress.value(m_iconPath);
0155         if (!fileTransferJob) {
0156             fileTransferJob = np.createPayloadTransferJob(QUrl::fromLocalFile(m_iconPath));
0157             fileTransferJob->start();
0158             s_downloadsInProgress[m_iconPath] = fileTransferJob;
0159         }
0160 
0161         connect(fileTransferJob, &FileTransferJob::result, this, [this, fileTransferJob] {
0162             s_downloadsInProgress.remove(m_iconPath);
0163             if (fileTransferJob->error()) {
0164                 qCDebug(KDECONNECT_PLUGIN_NOTIFICATIONS) << "Error in FileTransferJob: " << fileTransferJob->errorString();
0165             } else {
0166                 applyIcon();
0167             }
0168             show();
0169         });
0170     }
0171 }
0172 
0173 void Notification::applyIcon()
0174 {
0175     QPixmap icon(m_iconPath, "PNG");
0176     m_notification->setPixmap(icon);
0177 }
0178 
0179 void Notification::reply()
0180 {
0181     Q_EMIT replyRequested();
0182 }
0183 
0184 void Notification::sendReply(const QString &message)
0185 {
0186     Q_EMIT replied(message);
0187 }
0188 
0189 void Notification::parseNetworkPacket(const NetworkPacket &np)
0190 {
0191     m_internalId = np.get<QString>(QStringLiteral("id"));
0192     m_appName = np.get<QString>(QStringLiteral("appName"));
0193     m_ticker = np.get<QString>(QStringLiteral("ticker"));
0194     m_title = np.get<QString>(QStringLiteral("title"));
0195     m_text = np.get<QString>(QStringLiteral("text"));
0196     m_dismissable = np.get<bool>(QStringLiteral("isClearable"));
0197     m_hasIcon = np.hasPayload();
0198     m_silent = np.get<bool>(QStringLiteral("silent"));
0199     m_payloadHash = np.get<QString>(QStringLiteral("payloadHash"));
0200     m_requestReplyId = np.get<QString>(QStringLiteral("requestReplyId"), QString());
0201 
0202     m_actions.clear();
0203 
0204     const auto actions = np.get<QJsonArray>(QStringLiteral("actions"));
0205     for (const QJsonValue &value : actions) {
0206         m_actions.append(value.toString());
0207     }
0208 }
0209 
0210 #include "moc_notification.cpp"