Bläddra i källkod

Close call notifications when the call has been joined by the user, or the call has ended

Signed-off-by: Claudio Cambra <claudio.cambra@gmail.com>
Claudio Cambra 3 år sedan
förälder
incheckning
a504dbf965

+ 2 - 0
src/gui/CMakeLists.txt

@@ -72,6 +72,8 @@ set(client_SRCS
     application.cpp
     application.cpp
     invalidfilenamedialog.h
     invalidfilenamedialog.h
     invalidfilenamedialog.cpp
     invalidfilenamedialog.cpp
+    callstatechecker.h
+    callstatechecker.cpp
     conflictdialog.h
     conflictdialog.h
     conflictdialog.cpp
     conflictdialog.cpp
     conflictsolver.h
     conflictsolver.h

+ 184 - 0
src/gui/callstatechecker.cpp

@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
+ *
+ * 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 <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+
+#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;
+        }
+    }
+}
+
+}

+ 70 - 0
src/gui/callstatechecker.h

@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
+ *
+ * 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 <QObject>
+#include <QTimer>
+
+#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<JsonApiJob> _stateCheckJob;
+    bool _checking = false;
+};
+
+}

+ 4 - 1
src/gui/systray.cpp

@@ -23,6 +23,7 @@
 #include "tray/trayimageprovider.h"
 #include "tray/trayimageprovider.h"
 #include "configfile.h"
 #include "configfile.h"
 #include "accessmanager.h"
 #include "accessmanager.h"
+#include "callstatechecker.h"
 
 
 #include <QCursor>
 #include <QCursor>
 #include <QGuiApplication>
 #include <QGuiApplication>
@@ -98,6 +99,7 @@ Systray::Systray()
     );
     );
 
 
     qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler");
     qmlRegisterType<WheelHandler>("com.nextcloud.desktopclient", 1, 0, "WheelHandler");
+    qmlRegisterType<CallStateChecker>("com.nextcloud.desktopclient", 1, 0, "CallStateChecker");
 
 
 #if defined(Q_OS_MACOS) && defined(BUILD_OWNCLOUD_OSX_BUNDLE)
 #if defined(Q_OS_MACOS) && defined(BUILD_OWNCLOUD_OSX_BUNDLE)
     setUserNotificationCenterDelegate();
     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;
     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{
         const QVariantMap initialProperties{
+            {"accountState", QVariant::fromValue(accountState.data())},
             {"talkNotificationData", talkNotificationData},
             {"talkNotificationData", talkNotificationData},
             {"links", links},
             {"links", links},
             {"subject", callNotification._subject},
             {"subject", callNotification._subject},

+ 1 - 1
src/gui/systray.h

@@ -85,7 +85,7 @@ public:
     bool isOpen();
     bool isOpen();
     QString windowTitle() const;
     QString windowTitle() const;
     bool useNormalWindow() const;
     bool useNormalWindow() const;
-    void createCallDialog(const Activity &callNotification);
+    void createCallDialog(const Activity &callNotification, const AccountStatePtr accountState);
 
 
     Q_INVOKABLE void pauseResumeSync();
     Q_INVOKABLE void pauseResumeSync();
     Q_INVOKABLE bool syncIsPaused();
     Q_INVOKABLE bool syncIsPaused();

+ 27 - 2
src/gui/tray/CallNotificationDialog.qml

@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2022 by Camila Ayres <camila@nextcloud.com>
+ * Copyright (C) 2022 by Claudio Cambra <claudio.cambra@nextcloud.com>
+ *
+ * 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 2.15
 import QtQuick.Window 2.15
 import QtQuick.Window 2.15
 import Style 1.0
 import Style 1.0
@@ -20,6 +35,7 @@ Window {
     readonly property string deleteIcon: svgImage.arg("delete")
     readonly property string deleteIcon: svgImage.arg("delete")
 
 
     // We set talkNotificationData, subject, and links properties in C++
     // We set talkNotificationData, subject, and links properties in C++
+    property var accountState: ({})
     property var talkNotificationData: ({})
     property var talkNotificationData: ({})
     property string subject: ""
     property string subject: ""
     property var links: []
     property var links: []
@@ -29,6 +45,7 @@ Window {
     readonly property bool usingUserAvatar: root.talkNotificationData.userAvatar !== ""
     readonly property bool usingUserAvatar: root.talkNotificationData.userAvatar !== ""
 
 
     function closeNotification() {
     function closeNotification() {
+        callStateChecker.checking = false;
         ringSound.stop();
         ringSound.stop();
         root.close();
         root.close();
     }
     }
@@ -45,14 +62,22 @@ Window {
         root.requestActivate();
         root.requestActivate();
 
 
         ringSound.play();
         ringSound.play();
-     }
+        callStateChecker.checking = true;
+    }
+
+    CallStateChecker {
+        id: callStateChecker
+        token: root.talkNotificationData.conversationToken
+        accountState: root.accountState
+
+        onStopNotifying: root.closeNotification()
+    }
 
 
     Audio {
     Audio {
         id: ringSound
         id: ringSound
         source: root.ringtonePath
         source: root.ringtonePath
         loops: 9 // about 45 seconds of audio playing
         loops: 9 // about 45 seconds of audio playing
         audioRole: Audio.RingtoneRole
         audioRole: Audio.RingtoneRole
-        onStopped: root.closeNotification()
     }
     }
 
 
     Rectangle {
     Rectangle {

+ 1 - 1
src/gui/tray/usermodel.cpp

@@ -143,7 +143,7 @@ void User::slotBuildIncomingCallDialogs(const ActivityList &list)
 
 
     if(systray) {
     if(systray) {
         for(const auto &activity : list) {
         for(const auto &activity : list) {
-            systray->createCallDialog(activity);
+            systray->createCallDialog(activity, _account);
         }
         }
     }
     }
 }
 }

+ 0 - 5
src/libsync/account.cpp

@@ -657,11 +657,6 @@ int Account::serverVersionInt() const
         components.value(2).toInt());
         components.value(2).toInt());
 }
 }
 
 
-int Account::makeServerVersion(int majorVersion, int minorVersion, int patchVersion)
-{
-    return (majorVersion << 16) + (minorVersion << 8) + patchVersion;
-}
-
 bool Account::serverVersionUnsupported() const
 bool Account::serverVersionUnsupported() const
 {
 {
     if (serverVersionInt() == 0) {
     if (serverVersionInt() == 0) {

+ 4 - 1
src/libsync/account.h

@@ -223,7 +223,10 @@ public:
      */
      */
     int serverVersionInt() const;
     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);
     void setServerVersion(const QString &version);
 
 
     /** Whether the server is too old.
     /** Whether the server is too old.