From 7b1f10d4d514285017928b79316ff5e061f16e86 Mon Sep 17 00:00:00 2001 From: Yoram Bar-Haim Date: Wed, 28 Dec 2022 01:06:22 +0000 Subject: [PATCH] Add support for phone side telephony plugin on Linux. ## Summary Add support for Phone side (like in the Android and SailfishOS apps) to telephony plugin. this requires ModemManager to work. Call state notifications are currently supported, more capabilities are possible. Tested on PinePhone but should probably work on every linux Phone with ModemManager. to test, pair kdeconnect on your linux phone to desktop, and call your phone. you should get notification on the desktop, telling you that phone call is incoming. --- .kde-ci.yml | 4 + plugins/CMakeLists.txt | 4 + plugins/mmtelephony/CMakeLists.txt | 21 +++ plugins/mmtelephony/README | 13 ++ .../mmtelephony/kdeconnect_mmtelephony.json | 28 ++++ plugins/mmtelephony/mmtelephonyplugin.cpp | 135 ++++++++++++++++++ plugins/mmtelephony/mmtelephonyplugin.h | 57 ++++++++ 7 files changed, 262 insertions(+) create mode 100644 plugins/mmtelephony/CMakeLists.txt create mode 100644 plugins/mmtelephony/README create mode 100644 plugins/mmtelephony/kdeconnect_mmtelephony.json create mode 100644 plugins/mmtelephony/mmtelephonyplugin.cpp create mode 100644 plugins/mmtelephony/mmtelephonyplugin.h diff --git a/.kde-ci.yml b/.kde-ci.yml index d6e56e1d0..aed6522c5 100644 --- a/.kde-ci.yml +++ b/.kde-ci.yml @@ -27,3 +27,7 @@ Dependencies: 'frameworks/kpackage': '@stable' 'libraries/pulseaudio-qt': '@stable' 'libraries/plasma-wayland-protocols': '@stable' + +- 'on': ['Linux'] + 'require': + 'frameworks/modemmanager-qt': '@stable' diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 01abe99dd..dda19398b 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -38,6 +38,10 @@ if(NOT SAILFISHOS) add_subdirectory(screensaver-inhibit) add_subdirectory(virtualmonitor) + if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") + add_subdirectory(mmtelephony) + endif() + if(NOT WIN32) add_subdirectory(sendnotifications) endif() diff --git a/plugins/mmtelephony/CMakeLists.txt b/plugins/mmtelephony/CMakeLists.txt new file mode 100644 index 000000000..1fd9afe1e --- /dev/null +++ b/plugins/mmtelephony/CMakeLists.txt @@ -0,0 +1,21 @@ +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS ModemManagerQt) +set(debug_file_SRCS) +ecm_qt_declare_logging_category( + debug_file_SRCS HEADER plugin_mmtelephony_debug.h + IDENTIFIER KDECONNECT_PLUGIN_MMTELEPHONY CATEGORY_NAME kdeconnect.plugin.mmtelephony + DEFAULT_SEVERITY Warning + EXPORT kdeconnect-kde DESCRIPTION "kdeconnect (plugin mmtelephony)") + +set(kdeconnect_mmtelephony_SRCS + mmtelephonyplugin.cpp + ${debug_file_SRCS} +) + +kdeconnect_add_plugin(kdeconnect_mmtelephony SOURCES ${kdeconnect_mmtelephony_SRCS}) + +target_link_libraries(kdeconnect_mmtelephony + kdeconnectcore + Qt${QT_MAJOR_VERSION}::DBus + KF5::ModemManagerQt + KF5::I18n +) diff --git a/plugins/mmtelephony/README b/plugins/mmtelephony/README new file mode 100644 index 000000000..495f2356e --- /dev/null +++ b/plugins/mmtelephony/README @@ -0,0 +1,13 @@ + +This plugin will display a notification each time a package with type +"kdeconnect.telephony" is received. The type of notification will change +depending on the contents of the field "event" (string). + +Valid contents for "event" are: "ringing", "talking", "missedCall" and "sms". + +If the incoming package contains a "phoneNumber" string field, the notification +will also display it. Note that "phoneNumber" can be a contact name instead +of an actual phone number. + +If the incoming package contains "isCancel" set to true, the package is ignored. + diff --git a/plugins/mmtelephony/kdeconnect_mmtelephony.json b/plugins/mmtelephony/kdeconnect_mmtelephony.json new file mode 100644 index 000000000..b9dc49ffe --- /dev/null +++ b/plugins/mmtelephony/kdeconnect_mmtelephony.json @@ -0,0 +1,28 @@ +{ + "KPlugin": { + "Authors": [ + { + "Email": "bhyoram@protonmail.com", + "Name": "Yoram Bar-Haim" + } + ], + "Description": "Show notifications for incoming calls", + "EnabledByDefault": true, + "Icon": "call-start", + "Id": "kdeconnect_mmtelephony", + "License": "GPL", + "Name": "ModemManager Telephony integration", + "ServiceTypes": [ + "KdeConnect/Plugin" + ], + "Version": "0.1", + "Website": "https://kdeconnect.kde.org/" + }, + "X-KdeConnect-OutgoingPacketType": [ + "kdeconnect.telephony" + ], + "X-KdeConnect-SupportedPacketType": [ + "kdeconnect.telephony", + "kdeconnect.telephony.request_mute" + ] +} diff --git a/plugins/mmtelephony/mmtelephonyplugin.cpp b/plugins/mmtelephony/mmtelephonyplugin.cpp new file mode 100644 index 000000000..9f8833ddb --- /dev/null +++ b/plugins/mmtelephony/mmtelephonyplugin.cpp @@ -0,0 +1,135 @@ +/** + * SPDX-FileCopyrightText: 2019 Richard Liebscher + * SPDX-FileCopyrightText: 2022 Yoram Bar Haim + * + * SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-KDE-Accepted-GPL + */ + +#include "mmtelephonyplugin.h" + +#include +#include +#include + +#include "plugin_mmtelephony_debug.h" +#include + +K_PLUGIN_CLASS_WITH_JSON(MMTelephonyPlugin, "kdeconnect_mmtelephony.json") + +static const QString PACKET_TYPE_TELEPHONY = QStringLiteral("kdeconnect.telephony"); +static const QString PACKET_TYPE_TELEPHONY_REQUEST_MUTE = QStringLiteral("kdeconnect.telephony.request_mute"); + +QSharedPointer _voiceInterface(const QSharedPointer modemDevice) +{ + return modemDevice->interface(ModemManager::ModemDevice::VoiceInterface).objectCast(); +} + +MMTelephonyPlugin::MMTelephonyPlugin(QObject *parent, const QVariantList &args) + : KdeConnectPlugin(parent, args) +{ + connect(ModemManager::notifier(), &ModemManager::Notifier::modemAdded, this, &MMTelephonyPlugin::onModemAdded); +} + +bool MMTelephonyPlugin::receivePacket(const NetworkPacket &np) +{ + if (np.get(QStringLiteral("event")) == QLatin1String("mute")) { + // TODO: mute code + return true; + } + + return true; +} + +void MMTelephonyPlugin::onModemAdded(const QString &path) +{ + auto modemDevice = ModemManager::findModemDevice(path); + QSharedPointer vcm = _voiceInterface(modemDevice); + auto voice = vcm.get(); + connect(voice, &ModemManager::ModemVoice::callAdded, this, [this, voice](const QString &uni) { + auto call = voice->findCall(uni); + onCallAdded(call); + }); + connect(voice, &ModemManager::ModemVoice::callDeleted, this, [this, voice](const QString &uni) { + auto call = voice->findCall(uni); + onCallRemoved(call); + }); +} + +void MMTelephonyPlugin::onModemRemoved(const QString &path) +{ + Q_UNUSED(path); +} + +void MMTelephonyPlugin::onCallAdded(ModemManager::Call::Ptr call) +{ + qCDebug(KDECONNECT_PLUGIN_MMTELEPHONY) << "Call added" << call->number(); + + connect(call.get(), &ModemManager::Call::stateChanged, this, [=](MMCallState newState, MMCallState oldState, MMCallStateReason reason) { + onCallStateChanged(call.get(), newState, oldState, reason); + }); +} + +void MMTelephonyPlugin::onCallRemoved(ModemManager::Call::Ptr call) +{ + qCDebug(KDECONNECT_PLUGIN_MMTELEPHONY) << "Call removed" << call.get()->number(); +} + +QString MMTelephonyPlugin::stateName(MMCallState state) +{ + QString event; + switch (state) { + case MMCallState::MM_CALL_STATE_RINGING_IN: + event = QStringLiteral("ringing"); + break; + case MMCallState::MM_CALL_STATE_ACTIVE: + event = QStringLiteral("talking"); + break; + case MMCallState::MM_CALL_STATE_TERMINATED: + event = QStringLiteral("disconnected"); + break; + case MMCallState::MM_CALL_STATE_UNKNOWN: + default: + event = QStringLiteral("Unknown"); + } + return event; +} + +void MMTelephonyPlugin::onCallStateChanged(ModemManager::Call *call, MMCallState newState, MMCallState oldState, MMCallStateReason reason) +{ + Q_UNUSED(reason); + auto event = stateName(newState); + + qCDebug(KDECONNECT_PLUGIN_MMTELEPHONY) << "Call state changed" << call->uni() << event; + if (newState != MMCallState::MM_CALL_STATE_TERMINATED) + sendMMTelephonyPacket(call, event); + else + sendCancelMMTelephonyPacket(call, stateName(oldState)); +} + +void MMTelephonyPlugin::sendMMTelephonyPacket(ModemManager::Call *call, const QString &state) +{ + QString phoneNumber = call->number(); + + qCDebug(KDECONNECT_PLUGIN_MMTELEPHONY) << "Phone number is" << phoneNumber; + NetworkPacket np{PACKET_TYPE_TELEPHONY, + { + {QStringLiteral("event"), state}, + {QStringLiteral("phoneNumber"), phoneNumber}, + {QStringLiteral("contactName"), phoneNumber}, + }}; + sendPacket(np); +} + +void MMTelephonyPlugin::sendCancelMMTelephonyPacket(ModemManager::Call *call, const QString &lastState) +{ + QString phoneNumber = call->number(); + + NetworkPacket np{PACKET_TYPE_TELEPHONY, + {{QStringLiteral("event"), lastState}, + {QStringLiteral("phoneNumber"), phoneNumber}, + {QStringLiteral("contactName"), phoneNumber}, + {QStringLiteral("isCancel"), true}}}; + sendPacket(np); +} + +#include "mmtelephonyplugin.moc" diff --git a/plugins/mmtelephony/mmtelephonyplugin.h b/plugins/mmtelephony/mmtelephonyplugin.h new file mode 100644 index 000000000..9b148d37f --- /dev/null +++ b/plugins/mmtelephony/mmtelephonyplugin.h @@ -0,0 +1,57 @@ +/** + * SPDX-FileCopyrightText: 2013 Albert Vaca + * SPDX-FileCopyrightText: 2018 Simon Redman + * SPDX-FileCopyrightText: 2019 Richard Liebscher + * SPDX-FileCopyrightText: 2022 Yoram Bar Haim + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#ifndef TELEPHONYPLUGIN_H +#define TELEPHONYPLUGIN_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +class QDBusPendingCallWatcher; +class QDBusInterface; +class QDBusError; +class QDBusObjectPath; +class QDBusVariant; + +class MMTelephonyPlugin : public KdeConnectPlugin +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kdeconnect.device.telephony") + +public: + explicit MMTelephonyPlugin(QObject *parent, const QVariantList &args); + ~MMTelephonyPlugin() override = default; + + bool receivePacket(const NetworkPacket &np) override; + void connected() override + { + } + +private: + void onCallAdded(ModemManager::Call::Ptr call); + void onCallRemoved(ModemManager::Call::Ptr call); + void onModemAdded(const QString &path); + void onModemRemoved(const QString &path); + void onCallStateChanged(ModemManager::Call *call, MMCallState newState, MMCallState oldState, MMCallStateReason reason); + void sendMMTelephonyPacket(ModemManager::Call *call, const QString &state); + void sendCancelMMTelephonyPacket(ModemManager::Call *call, const QString &lastState); + static QString stateName(MMCallState state); +}; + +#endif