From a504dbf9653041f1c5bf54237d380e10542044e8 Mon Sep 17 00:00:00 2001 From: Claudio Cambra Date: Fri, 24 Jun 2022 11:49:38 +0200 Subject: [PATCH] Close call notifications when the call has been joined by the user, or the call has ended Signed-off-by: Claudio Cambra --- src/gui/CMakeLists.txt | 2 + src/gui/callstatechecker.cpp | 184 ++++++++++++++++++++++++ src/gui/callstatechecker.h | 70 +++++++++ src/gui/systray.cpp | 5 +- src/gui/systray.h | 2 +- src/gui/tray/CallNotificationDialog.qml | 29 +++- src/gui/tray/usermodel.cpp | 2 +- src/libsync/account.cpp | 5 - src/libsync/account.h | 5 +- 9 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 src/gui/callstatechecker.cpp create mode 100644 src/gui/callstatechecker.h diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 0f0d4c9e50d56..2390d354845df 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -72,6 +72,8 @@ set(client_SRCS application.cpp invalidfilenamedialog.h invalidfilenamedialog.cpp + callstatechecker.h + callstatechecker.cpp conflictdialog.h conflictdialog.cpp conflictsolver.h diff --git a/src/gui/callstatechecker.cpp b/src/gui/callstatechecker.cpp new file mode 100644 index 0000000000000..83795251f2725 --- /dev/null +++ b/src/gui/callstatechecker.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include +#include +#include + +#include "callstatechecker.h" +#include "account.h" + +namespace OCC { + +Q_LOGGING_CATEGORY(lcCallStateChecker, "nextcloud.gui.callstatechecker", QtInfoMsg) + +constexpr int successStatusCode = 200; + +CallStateChecker::CallStateChecker(QObject *parent) + : QObject(parent) +{ + setup(); +} + +void CallStateChecker::setup() +{ + _notificationTimer.setSingleShot(true); + _notificationTimer.setInterval(60 * 1000); + connect(&_notificationTimer, &QTimer::timeout, this, &CallStateChecker::slotNotificationTimerElapsed); + + _statusCheckTimer.setInterval(2 * 1000); + connect(&_statusCheckTimer, &QTimer::timeout, this, &CallStateChecker::slotStatusCheckTimerElapsed); +} + +QString CallStateChecker::token() const +{ + return _token; +} + +void CallStateChecker::setToken(const QString &token) +{ + _token = token; + Q_EMIT tokenChanged(); + reset(); +} + +AccountState* CallStateChecker::accountState() const +{ + return _accountState; +} + +void CallStateChecker::setAccountState(AccountState *state) +{ + _accountState = state; + Q_EMIT accountStateChanged(); + reset(); +} + +bool CallStateChecker::checking() const +{ + return _checking; +} + +void CallStateChecker::setChecking(const bool checking) +{ + if(checking) { + qCInfo(lcCallStateChecker) << "Starting to check state of call with token:" << _token; + _notificationTimer.start(); + _statusCheckTimer.start(); + } else { + qCInfo(lcCallStateChecker) << "Stopping checking of call state for call with token:" << _token; + _notificationTimer.stop(); + _statusCheckTimer.stop(); + _stateCheckJob.clear(); + } + + _checking = checking; + Q_EMIT checkingChanged(); +} + +void CallStateChecker::reset() +{ + qCInfo(lcCallStateChecker, "Resetting call check"); + setChecking(false); + setChecking(true); +} + +void CallStateChecker::slotNotificationTimerElapsed() +{ + qCInfo(lcCallStateChecker) << "Notification timer elapsed, stopping call checking of call with token:" << _token; + setChecking(false); + Q_EMIT stopNotifying(); +} + +void CallStateChecker::slotStatusCheckTimerElapsed() +{ + // Don't run check if another check is still ongoing + if (_stateCheckJob) { + return; + } + + startCallStateCheck(); +} + +bool CallStateChecker::isAccountServerVersion22OrLater() const +{ + if(!_accountState || !_accountState->account()) { + return false; + } + + const auto accountNcVersion = _accountState->account()->serverVersionInt(); + constexpr auto ncVersion22 = OCC::Account::makeServerVersion(22, 0, 0); + + return accountNcVersion >= ncVersion22; +} + +void CallStateChecker::startCallStateCheck() +{ + // check connectivity and credentials + if (!(_accountState && _accountState->isConnected() && + _accountState->account() && _accountState->account()->credentials() && + _accountState->account()->credentials()->ready())) { + qCInfo(lcCallStateChecker, "Could not connect, can't check call state."); + return; + } + + // Check for token + if(_token.isEmpty()) { + qCInfo(lcCallStateChecker, "No call token set, can't check without it."); + return; + } + + qCInfo(lcCallStateChecker) << "Checking state of call with token: " << _token; + + const auto spreedPath = QStringLiteral("ocs/v2.php/apps/spreed/"); + const auto callApiPath = isAccountServerVersion22OrLater() ? QStringLiteral("api/v4/call/") : QStringLiteral("api/v1/call/"); + const QString callPath = spreedPath + callApiPath + _token; // Make sure it's a QString and not a QStringBuilder + + _stateCheckJob = new JsonApiJob(_accountState->account(), callPath, this); + connect(_stateCheckJob.data(), &JsonApiJob::jsonReceived, this, &CallStateChecker::slotCallStateReceived); + + _stateCheckJob->setVerb(JsonApiJob::Verb::Get); + _stateCheckJob->start(); +} + +void CallStateChecker::slotCallStateReceived(const QJsonDocument &json, const int statusCode) +{ + if (statusCode != successStatusCode) { + qCInfo(lcCallStateChecker) << "Failed to retrieve call state data. Server returned status code: " << statusCode; + return; + } + + const auto participantsJsonArray = json.object().value("ocs").toObject().value("data").toArray(); + + if (participantsJsonArray.empty()) { + qCInfo(lcCallStateChecker, "Call has no participants and has therefore been abandoned."); + Q_EMIT stopNotifying(); + setChecking(false); + return; + } + + for (const auto &participant : participantsJsonArray) { + const auto participantDataObject = participant.toObject(); + const auto participantId = isAccountServerVersion22OrLater() ? participantDataObject.value("actorId").toString() : participantDataObject.value("userId").toString(); + + if (participantId == _accountState->account()->davUser()) { + qCInfo(lcCallStateChecker, "Found own account ID in participants list, meaning call has been joined."); + Q_EMIT stopNotifying(); + setChecking(false); + return; + } + } +} + +} diff --git a/src/gui/callstatechecker.h b/src/gui/callstatechecker.h new file mode 100644 index 0000000000000..630099630e050 --- /dev/null +++ b/src/gui/callstatechecker.h @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2022 by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +#include +#include + +#include "networkjobs.h" +#include "accountstate.h" + +namespace OCC { + +class CallStateChecker : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString token READ token WRITE setToken NOTIFY tokenChanged) + Q_PROPERTY(AccountState* accountState READ accountState WRITE setAccountState NOTIFY accountStateChanged) + Q_PROPERTY(bool checking READ checking WRITE setChecking NOTIFY checkingChanged) + +public: + explicit CallStateChecker(QObject *parent = nullptr); + + QString token() const; + AccountState* accountState() const; + bool checking() const; + +signals: + void tokenChanged(); + void accountStateChanged(); + void checkingChanged(); + + void stopNotifying(); + +public slots: + void setToken(const QString &token); + void setAccountState(OCC::AccountState *accountState); + void setChecking(const bool checking); + +private slots: + void slotStatusCheckTimerElapsed(); + void slotNotificationTimerElapsed(); + void slotCallStateReceived(const QJsonDocument &json, const int statusCode); + void reset(); + +private: + void setup(); + void startCallStateCheck(); + bool isAccountServerVersion22OrLater() const; + + AccountState *_accountState = nullptr; + QString _token; + QTimer _statusCheckTimer; // How often we check the status of the call + QTimer _notificationTimer; // How long we present the call notification for + QPointer _stateCheckJob; + bool _checking = false; +}; + +} diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 128937cde5bd6..d3e4251bb466d 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -23,6 +23,7 @@ #include "tray/trayimageprovider.h" #include "configfile.h" #include "accessmanager.h" +#include "callstatechecker.h" #include #include @@ -98,6 +99,7 @@ Systray::Systray() ); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "WheelHandler"); + qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "CallStateChecker"); #if defined(Q_OS_MACOS) && defined(BUILD_OWNCLOUD_OSX_BUNDLE) setUserNotificationCenterDelegate(); @@ -183,7 +185,7 @@ void Systray::setupContextMenu() }); } -void Systray::createCallDialog(const Activity &callNotification) +void Systray::createCallDialog(const Activity &callNotification, const AccountStatePtr accountState) { qCDebug(lcSystray) << "Starting a new call dialog for notification with id: " << callNotification._id << "with text: " << callNotification._subject; @@ -208,6 +210,7 @@ void Systray::createCallDialog(const Activity &callNotification) } const QVariantMap initialProperties{ + {"accountState", QVariant::fromValue(accountState.data())}, {"talkNotificationData", talkNotificationData}, {"links", links}, {"subject", callNotification._subject}, diff --git a/src/gui/systray.h b/src/gui/systray.h index 18f01cd937aea..a1ecdc4a17b81 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -85,7 +85,7 @@ class Systray bool isOpen(); QString windowTitle() const; bool useNormalWindow() const; - void createCallDialog(const Activity &callNotification); + void createCallDialog(const Activity &callNotification, const AccountStatePtr accountState); Q_INVOKABLE void pauseResumeSync(); Q_INVOKABLE bool syncIsPaused(); diff --git a/src/gui/tray/CallNotificationDialog.qml b/src/gui/tray/CallNotificationDialog.qml index 1e48487f466bd..23d9e2bc8659e 100644 --- a/src/gui/tray/CallNotificationDialog.qml +++ b/src/gui/tray/CallNotificationDialog.qml @@ -1,3 +1,18 @@ +/* + * Copyright (C) 2022 by Camila Ayres + * Copyright (C) 2022 by Claudio Cambra + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + import QtQuick 2.15 import QtQuick.Window 2.15 import Style 1.0 @@ -20,6 +35,7 @@ Window { readonly property string deleteIcon: svgImage.arg("delete") // We set talkNotificationData, subject, and links properties in C++ + property var accountState: ({}) property var talkNotificationData: ({}) property string subject: "" property var links: [] @@ -29,6 +45,7 @@ Window { readonly property bool usingUserAvatar: root.talkNotificationData.userAvatar !== "" function closeNotification() { + callStateChecker.checking = false; ringSound.stop(); root.close(); } @@ -45,14 +62,22 @@ Window { root.requestActivate(); ringSound.play(); - } + callStateChecker.checking = true; + } + + CallStateChecker { + id: callStateChecker + token: root.talkNotificationData.conversationToken + accountState: root.accountState + + onStopNotifying: root.closeNotification() + } Audio { id: ringSound source: root.ringtonePath loops: 9 // about 45 seconds of audio playing audioRole: Audio.RingtoneRole - onStopped: root.closeNotification() } Rectangle { diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp index f02a2f8c71ac2..081ccb452c6f3 100644 --- a/src/gui/tray/usermodel.cpp +++ b/src/gui/tray/usermodel.cpp @@ -143,7 +143,7 @@ void User::slotBuildIncomingCallDialogs(const ActivityList &list) if(systray) { for(const auto &activity : list) { - systray->createCallDialog(activity); + systray->createCallDialog(activity, _account); } } } diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index eab683dd63830..fa874386bb412 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -657,11 +657,6 @@ int Account::serverVersionInt() const components.value(2).toInt()); } -int Account::makeServerVersion(int majorVersion, int minorVersion, int patchVersion) -{ - return (majorVersion << 16) + (minorVersion << 8) + patchVersion; -} - bool Account::serverVersionUnsupported() const { if (serverVersionInt() == 0) { diff --git a/src/libsync/account.h b/src/libsync/account.h index 876fa94b9bc0a..4c1429f0a9ae1 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -223,7 +223,10 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject */ int serverVersionInt() const; - static int makeServerVersion(int majorVersion, int minorVersion, int patchVersion); + static constexpr int makeServerVersion(const int majorVersion, const int minorVersion, const int patchVersion) { + return (majorVersion << 16) + (minorVersion << 8) + patchVersion; + }; + void setServerVersion(const QString &version); /** Whether the server is too old.