diff --git a/plugins/notifications/CMakeLists.txt b/plugins/notifications/CMakeLists.txt index 3d27b89fe..511d0413d 100644 --- a/plugins/notifications/CMakeLists.txt +++ b/plugins/notifications/CMakeLists.txt @@ -5,8 +5,9 @@ target_link_libraries(kdeconnect_notifications kdeconnectcore Qt::DBus Qt::Widgets - KF6::Notifications + KF6::GuiAddons KF6::I18n + KF6::Notifications KF6::WindowSystem ) diff --git a/plugins/notifications/notificationsplugin.cpp b/plugins/notifications/notificationsplugin.cpp index c52556d87..8000face1 100644 --- a/plugins/notifications/notificationsplugin.cpp +++ b/plugins/notifications/notificationsplugin.cpp @@ -11,11 +11,15 @@ #include #include +#include + #if !defined(Q_OS_WIN) && !defined(Q_OS_MAC) #include #include #endif +#include + K_PLUGIN_CLASS_WITH_JSON(NotificationsPlugin, "kdeconnect_notifications.json") void NotificationsPlugin::connected() @@ -175,6 +179,32 @@ void NotificationsPlugin::sendAction(const QString &key, const QString &action) np.set(QStringLiteral("key"), key); np.set(QStringLiteral("action"), action); sendPacket(np); + + copyAuthCodeIfPresent(action); +} + +void NotificationsPlugin::copyAuthCodeIfPresent(const QString &action) +{ + // The auth code we receive has invisible characters in it for some reason. + // (U+2063 INVISIBLE SEPARATOR between each digit). + // Remove them if present before continuing. + QString sanitizedAction = action; + sanitizedAction.remove(QChar(0x2063)); + + // Match blocks of digits, 4-10 digits long. This should match auth codes + // in any language without relying on the action text having a specific + // keyword in it such as "Copy" in English. + QRegularExpression authCodeRegex(QStringLiteral("\\b(\\d{4,10})\\b")); + QRegularExpressionMatch match = authCodeRegex.match(sanitizedAction); + + if (!match.hasMatch()) { + return; + } + + QString text = match.captured(1); + auto mimeData = new QMimeData; + mimeData->setText(text); + KSystemClipboard::instance()->setMimeData(mimeData, QClipboard::Clipboard); } QString NotificationsPlugin::newId() diff --git a/plugins/notifications/notificationsplugin.h b/plugins/notifications/notificationsplugin.h index 02e814854..7afff6170 100644 --- a/plugins/notifications/notificationsplugin.h +++ b/plugins/notifications/notificationsplugin.h @@ -43,6 +43,14 @@ Q_SIGNALS: Q_SCRIPTABLE void allNotificationsRemoved(); private: + /** + * Check if the action is to copy an auth code, if so add the code to the desktop clipboard. + * + * This is necessary since access to the Android clipboard has become more restricted for apps + * that are not the foreground app since Android 10. + */ + void copyAuthCodeIfPresent(const QString &action); + void removeNotification(const QString &internalId); QString newId(); // Generates successive identifiers to use as public ids void notificationReady(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 69e195538..8818a3878 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,8 +5,10 @@ set(kdeconnect_libraries kdeconnectinterfaces kdeconnectsmshelper kdeconnectversion + KF6::GuiAddons KF6::I18n KF6::KIOWidgets + KF6::Notifications Qt::DBus Qt::Network KF6::People @@ -14,6 +16,7 @@ set(kdeconnect_libraries Qt::Test ) +ecm_add_test(notificationstest.cpp LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(pluginloadtest.cpp LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(sendfiletest.cpp LINK_LIBRARIES ${kdeconnect_libraries}) ecm_add_test(smshelpertest.cpp LINK_LIBRARIES ${kdeconnect_libraries}) diff --git a/tests/notificationstest.cpp b/tests/notificationstest.cpp new file mode 100644 index 000000000..207e07ae0 --- /dev/null +++ b/tests/notificationstest.cpp @@ -0,0 +1,93 @@ +/** + * SPDX-FileCopyrightText: 2024 Kristen McWilliam + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "kdeconnectplugin.h" +#include "testdaemon.h" + +#include + +#include + +/** + * Test the NotificationsPlugin class + */ +class NotificationsPluginTest : public QObject +{ + Q_OBJECT + +public: + NotificationsPluginTest() + { + QStandardPaths::setTestModeEnabled(true); + m_daemon = new TestDaemon; + } + +private: + TestDaemon *m_daemon; + +private Q_SLOTS: + + /** + * If the user selects the action to copy an auth code, the auth code should + * be copied to the desktop clipboard. + */ + void testAuthCodeIsCopied() + { + Device *device = nullptr; + const QList devicesList = m_daemon->devicesList(); + + for (Device *id : devicesList) { + if (id->isReachable()) { + if (!id->isPaired()) + id->requestPairing(); + device = id; + } + } + + if (device == nullptr) { + QFAIL("Unable to determine device"); + } + + QCOMPARE(device->isReachable(), true); + QCOMPARE(device->isPaired(), true); + + KdeConnectPlugin *plugin = device->plugin(QStringLiteral("kdeconnect_notifications")); + QVERIFY(plugin); + + const QString key = QStringLiteral("0|com.google.android.apps.messaging|2|com.google.android.apps.messaging:incoming_message:23|10129"); + + // Note that the auth code we receive to work with has invisible + // characters in it for some reason. (U+2063 INVISIBLE SEPARATOR between + // each digit) + // + // `action` here is equal to `Copy \"6⁣7⁣1⁣7⁣3⁣3⁣\"`, but with the invisible + // characters written out explicitly to make it easier to read, and to + // prevent confusion when reading the test code. + const QString action = QStringLiteral("Copy \"6\u20637\u20631\u20637\u20633\u20633\""); + + const QString expectedClipboardContents = QStringLiteral("671733"); + + // Verify that the clipboard does not contain the auth code already + const QString originalClipboardContents = KSystemClipboard::instance()->text(QClipboard::Clipboard); + QVERIFY(originalClipboardContents != expectedClipboardContents); + + // Send the action + plugin->metaObject()->invokeMethod(plugin, "sendAction", Q_ARG(QString, key), Q_ARG(QString, action)); + + // Verify that the clipboard now contains the auth code + const QString updatedClipboardContents = KSystemClipboard::instance()->text(QClipboard::Clipboard); + QCOMPARE(updatedClipboardContents, expectedClipboardContents); + + // Set the clipboard back to its original state + auto mimeData = new QMimeData; + mimeData->setText(originalClipboardContents); + KSystemClipboard::instance()->setMimeData(mimeData, QClipboard::Clipboard); + } +}; + +QTEST_MAIN(NotificationsPluginTest); + +#include "notificationstest.moc"