Use push notifications for Tray activities/notifications fetch trigger.

Signed-off-by: allexzander <blackslayer4@gmail.com>
This commit is contained in:
allexzander 2021-01-26 16:57:46 +02:00
parent f2ffa74a7b
commit 8c4d5333c1
11 changed files with 282 additions and 22 deletions

View file

@ -3,6 +3,7 @@
#include "accountmanager.h" #include "accountmanager.h"
#include "owncloudgui.h" #include "owncloudgui.h"
#include <pushnotifications.h>
#include "syncengine.h" #include "syncengine.h"
#include "ocsjob.h" #include "ocsjob.h"
#include "configfile.h" #include "configfile.h"
@ -40,7 +41,7 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent)
this, &User::slotRefresh); this, &User::slotRefresh);
connect(_account.data(), &AccountState::stateChanged, connect(_account.data(), &AccountState::stateChanged,
[=]() { if (isConnected()) {slotRefresh();} }); [=]() { if (isConnected()) {slotRefreshImmediately();} });
connect(_account.data(), &AccountState::stateChanged, this, &User::accountStateChanged); connect(_account.data(), &AccountState::stateChanged, this, &User::accountStateChanged);
connect(_account.data(), &AccountState::hasFetchedNavigationApps, connect(_account.data(), &AccountState::hasFetchedNavigationApps,
this, &User::slotRebuildNavigationAppList); this, &User::slotRebuildNavigationAppList);
@ -105,19 +106,90 @@ void User::slotBuildNotificationDisplay(const ActivityList &list)
void User::setNotificationRefreshInterval(std::chrono::milliseconds interval) void User::setNotificationRefreshInterval(std::chrono::milliseconds interval)
{ {
qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval"; if (!checkPushNotificationsAreReady()) {
_notificationCheckTimer.start(interval.count()); qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval";
_notificationCheckTimer.start(interval.count());
}
}
void User::slotPushNotificationsReady()
{
qCInfo(lcActivity) << "Push notifications are ready";
if (_notificationCheckTimer.isActive()) {
// as we are now able to use push notifications - let's stop the polling timer
_notificationCheckTimer.stop();
}
connectPushNotifications();
}
void User::slotDisconnectPushNotifications()
{
disconnect(_account->account()->pushNotifications(), &PushNotifications::notificationsChanged, this, &User::slotReceivedPushNotification);
disconnect(_account->account()->pushNotifications(), &PushNotifications::activitiesChanged, this, &User::slotReceivedPushActivity);
disconnect(_account->account().data(), &Account::pushNotificationsDisabled, this, &User::slotDisconnectPushNotifications);
// connection to WebSocket may have dropped or an error occured, so we need to bring back the polling until we have re-established the connection
setNotificationRefreshInterval(ConfigFile().notificationRefreshInterval());
}
void User::slotReceivedPushNotification(Account *account)
{
if (account->id() == _account->account()->id()) {
slotRefreshNotifications();
}
}
void User::slotReceivedPushActivity(Account *account)
{
if (account->id() == _account->account()->id()) {
slotRefreshActivities();
}
}
void User::connectPushNotifications() const
{
connect(_account->account().data(), &Account::pushNotificationsDisabled, this, &User::slotDisconnectPushNotifications, Qt::UniqueConnection);
connect(_account->account()->pushNotifications(), &PushNotifications::notificationsChanged, this, &User::slotReceivedPushNotification, Qt::UniqueConnection);
connect(_account->account()->pushNotifications(), &PushNotifications::activitiesChanged, this, &User::slotReceivedPushActivity, Qt::UniqueConnection);
}
bool User::checkPushNotificationsAreReady() const
{
const auto pushNotifications = _account->account()->pushNotifications();
const auto pushActivitiesAvailable = _account->account()->capabilities().availablePushNotifications() & PushNotificationType::Activities;
const auto pushNotificationsAvailable = _account->account()->capabilities().availablePushNotifications() & PushNotificationType::Notifications;
const auto pushActivitiesAndNotificationsAvailable = pushActivitiesAvailable && pushNotificationsAvailable;
if (pushActivitiesAndNotificationsAvailable && pushNotifications && pushNotifications->isReady()) {
connectPushNotifications();
return true;
} else {
connect(_account->account().data(), &Account::pushNotificationsReady, this, &User::slotPushNotificationsReady, Qt::UniqueConnection);
return false;
}
} }
void User::slotRefreshImmediately() { void User::slotRefreshImmediately() {
if (_account.data() && _account.data()->isConnected()) { if (_account.data() && _account.data()->isConnected()) {
this->slotRefreshActivities(); slotRefreshActivities();
} }
this->slotRefreshNotifications(); slotRefreshNotifications();
} }
void User::slotRefresh() void User::slotRefresh()
{ {
if (checkPushNotificationsAreReady()) {
// we are relying on WebSocket push notifications - ignore refresh attempts from UI
_timeSinceLastCheck[_account.data()].invalidate();
return;
}
// QElapsedTimer isn't actually constructed as invalid. // QElapsedTimer isn't actually constructed as invalid.
if (!_timeSinceLastCheck.contains(_account.data())) { if (!_timeSinceLastCheck.contains(_account.data())) {
_timeSinceLastCheck[_account.data()].invalidate(); _timeSinceLastCheck[_account.data()].invalidate();
@ -131,9 +203,9 @@ void User::slotRefresh()
} }
if (_account.data() && _account.data()->isConnected()) { if (_account.data() && _account.data()->isConnected()) {
if (!timer.isValid()) { if (!timer.isValid()) {
this->slotRefreshActivities(); slotRefreshActivities();
} }
this->slotRefreshNotifications(); slotRefreshNotifications();
timer.start(); timer.start();
} }
} }

View file

@ -71,6 +71,15 @@ public slots:
void setNotificationRefreshInterval(std::chrono::milliseconds interval); void setNotificationRefreshInterval(std::chrono::milliseconds interval);
void slotRebuildNavigationAppList(); void slotRebuildNavigationAppList();
private:
void slotPushNotificationsReady();
void slotDisconnectPushNotifications();
void slotReceivedPushNotification(Account *account);
void slotReceivedPushActivity(Account *account);
void connectPushNotifications() const;
bool checkPushNotificationsAreReady() const;
private: private:
AccountStatePtr _account; AccountStatePtr _account;
bool _isCurrentUser; bool _isCurrentUser;

View file

@ -216,6 +216,7 @@ void Account::trySetupPushNotifications()
qCInfo(lcAccount) << "Delete push notifications object because authentication failed or connection lost"; qCInfo(lcAccount) << "Delete push notifications object because authentication failed or connection lost";
_pushNotifications->deleteLater(); _pushNotifications->deleteLater();
_pushNotifications = nullptr; _pushNotifications = nullptr;
emit pushNotificationsDisabled(this);
}; };
connect(_pushNotifications, &PushNotifications::connectionLost, this, deletePushNotifications); connect(_pushNotifications, &PushNotifications::connectionLost, this, deletePushNotifications);

View file

@ -283,6 +283,7 @@ signals:
void appPasswordRetrieved(QString); void appPasswordRetrieved(QString);
void pushNotificationsReady(Account *account); void pushNotificationsReady(Account *account);
void pushNotificationsDisabled(Account *account);
protected Q_SLOTS: protected Q_SLOTS:
void slotCredentialsFetched(); void slotCredentialsFetched();

View file

@ -190,6 +190,14 @@ PushNotificationTypes Capabilities::availablePushNotifications() const
pushNotificationTypes.setFlag(PushNotificationType::Files); pushNotificationTypes.setFlag(PushNotificationType::Files);
} }
if (types.contains("activities")) {
pushNotificationTypes.setFlag(PushNotificationType::Activities);
}
if (types.contains("notifications")) {
pushNotificationTypes.setFlag(PushNotificationType::Notifications);
}
return pushNotificationTypes; return pushNotificationTypes;
} }

View file

@ -28,7 +28,9 @@ class DirectEditor;
enum PushNotificationType { enum PushNotificationType {
None = 0, None = 0,
Files = 1 Files = 1,
Activities = 2,
Notifications = 4
}; };
Q_DECLARE_FLAGS(PushNotificationTypes, PushNotificationType) Q_DECLARE_FLAGS(PushNotificationTypes, PushNotificationType)
Q_DECLARE_OPERATORS_FOR_FLAGS(PushNotificationTypes) Q_DECLARE_OPERATORS_FOR_FLAGS(PushNotificationTypes)

View file

@ -73,8 +73,10 @@ void PushNotifications::onWebSocketTextMessageReceived(const QString &message)
if (message == "notify_file") { if (message == "notify_file") {
handleNotifyFile(); handleNotifyFile();
} else if (message == "notify_activity" || message == "notify_notification") { } else if (message == "notify_activity") {
handleNotification(); handleNotifyActivity();
} else if (message == "notify_notification") {
handleNotifyNotification();
} else if (message == "authenticated") { } else if (message == "authenticated") {
handleAuthenticated(); handleAuthenticated();
} else if (message == "err: Invalid credentials") { } else if (message == "err: Invalid credentials") {
@ -181,9 +183,15 @@ void PushNotifications::handleInvalidCredentials()
} }
} }
void PushNotifications::handleNotification() void PushNotifications::handleNotifyNotification()
{ {
qCInfo(lcPushNotifications) << "Notification or activity push notification arrived"; qCInfo(lcPushNotifications) << "Push notification arrived";
emit notification(_account); emit notificationsChanged(_account);
}
void PushNotifications::handleNotifyActivity()
{
qCInfo(lcPushNotifications) << "Push activity arrived";
emit activitiesChanged(_account);
} }
} }

View file

@ -64,9 +64,14 @@ signals:
void filesChanged(Account *account); void filesChanged(Account *account);
/** /**
* Will be emitted if there is a new notification or activity on the server * Will be emitted if activities have been changed on the server
*/ */
void notification(Account *account); void activitiesChanged(Account *account);
/**
* Will be emitted if notifications have been changed on the server
*/
void notificationsChanged(Account *account);
/** /**
* Will be emitted if push notifications are unable to authenticate * Will be emitted if push notifications are unable to authenticate
@ -100,7 +105,8 @@ private:
void handleAuthenticated(); void handleAuthenticated();
void handleNotifyFile(); void handleNotifyFile();
void handleInvalidCredentials(); void handleInvalidCredentials();
void handleNotification(); void handleNotifyNotification();
void handleNotifyActivity();
Account *_account = nullptr; Account *_account = nullptr;
QWebSocket *_webSocket = nullptr; QWebSocket *_webSocket = nullptr;

View file

@ -70,6 +70,8 @@ OCC::AccountPtr FakeWebSocketServer::createAccount()
QStringList typeList; QStringList typeList;
typeList.append("files"); typeList.append("files");
typeList.append("activities");
typeList.append("notifications");
QString websocketUrl("ws://localhost:12345"); QString websocketUrl("ws://localhost:12345");

View file

@ -7,6 +7,40 @@ class TestCapabilities : public QObject
Q_OBJECT Q_OBJECT
private slots: private slots:
void testPushNotificationsAvailable_pushNotificationsForActivitiesAvailable_returnTrue()
{
QStringList typeList;
typeList.append("activities");
QVariantMap notifyPushMap;
notifyPushMap["type"] = typeList;
QVariantMap capabilitiesMap;
capabilitiesMap["notify_push"] = notifyPushMap;
const auto &capabilities = OCC::Capabilities(capabilitiesMap);
const auto activitiesPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Activities);
QCOMPARE(activitiesPushNotificationsAvailable, true);
}
void testPushNotificationsAvailable_pushNotificationsForActivitiesNotAvailable_returnFalse()
{
QStringList typeList;
typeList.append("noactivities");
QVariantMap notifyPushMap;
notifyPushMap["type"] = typeList;
QVariantMap capabilitiesMap;
capabilitiesMap["notify_push"] = notifyPushMap;
const auto &capabilities = OCC::Capabilities(capabilitiesMap);
const auto activitiesPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Activities);
QCOMPARE(activitiesPushNotificationsAvailable, false);
}
void testPushNotificationsAvailable_pushNotificationsForFilesAvailable_returnTrue() void testPushNotificationsAvailable_pushNotificationsForFilesAvailable_returnTrue()
{ {
QStringList typeList; QStringList typeList;
@ -41,12 +75,50 @@ private slots:
QCOMPARE(filesPushNotificationsAvailable, false); QCOMPARE(filesPushNotificationsAvailable, false);
} }
void testPushNotificationsAvailable_pushNotificationsForNotificationsAvailable_returnTrue()
{
QStringList typeList;
typeList.append("notifications");
QVariantMap notifyPushMap;
notifyPushMap["type"] = typeList;
QVariantMap capabilitiesMap;
capabilitiesMap["notify_push"] = notifyPushMap;
const auto &capabilities = OCC::Capabilities(capabilitiesMap);
const auto notificationsPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Notifications);
QCOMPARE(notificationsPushNotificationsAvailable, true);
}
void testPushNotificationsAvailable_pushNotificationsForNotificationsNotAvailable_returnFalse()
{
QStringList typeList;
typeList.append("nonotifications");
QVariantMap notifyPushMap;
notifyPushMap["type"] = typeList;
QVariantMap capabilitiesMap;
capabilitiesMap["notify_push"] = notifyPushMap;
const auto &capabilities = OCC::Capabilities(capabilitiesMap);
const auto notificationsPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Notifications);
QCOMPARE(notificationsPushNotificationsAvailable, false);
}
void testPushNotificationsAvailable_pushNotificationsNotAvailable_returnFalse() void testPushNotificationsAvailable_pushNotificationsNotAvailable_returnFalse()
{ {
const auto &capabilities = OCC::Capabilities(QVariantMap()); const auto &capabilities = OCC::Capabilities(QVariantMap());
const auto activitiesPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Activities);
const auto filesPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Files); const auto filesPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Files);
const auto notificationsPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Notifications);
QCOMPARE(activitiesPushNotificationsAvailable, false);
QCOMPARE(filesPushNotificationsAvailable, false); QCOMPARE(filesPushNotificationsAvailable, false);
QCOMPARE(notificationsPushNotificationsAvailable, false);
} }
void testPushNotificationsWebSocketUrl_urlAvailable_returnUrl() void testPushNotificationsWebSocketUrl_urlAvailable_returnUrl()

View file

@ -84,8 +84,8 @@ private slots:
auto account = FakeWebSocketServer::createAccount(); auto account = FakeWebSocketServer::createAccount();
auto credentials = new CredentialsStub(user, password); auto credentials = new CredentialsStub(user, password);
account->setCredentials(credentials); account->setCredentials(credentials);
QSignalSpy notificationSpy(account->pushNotifications(), &OCC::PushNotifications::notification); QSignalSpy activitySpy(account->pushNotifications(), &OCC::PushNotifications::activitiesChanged);
QVERIFY(notificationSpy.isValid()); QVERIFY(activitySpy.isValid());
// Wait for authentication and then send notify_file push notification // Wait for authentication and then send notify_file push notification
QVERIFY(processTextMessageSpy.wait()); QVERIFY(processTextMessageSpy.wait());
@ -94,9 +94,9 @@ private slots:
socket->sendTextMessage("notify_activity"); socket->sendTextMessage("notify_activity");
// notification signal should be emitted // notification signal should be emitted
QVERIFY(notificationSpy.wait()); QVERIFY(activitySpy.wait());
QCOMPARE(notificationSpy.count(), 1); QCOMPARE(activitySpy.count(), 1);
auto accountFilesChanged = notificationSpy.at(0).at(0).value<OCC::Account *>(); auto accountFilesChanged = activitySpy.at(0).at(0).value<OCC::Account *>();
QCOMPARE(accountFilesChanged, account.data()); QCOMPARE(accountFilesChanged, account.data());
} }
@ -111,7 +111,7 @@ private slots:
auto account = FakeWebSocketServer::createAccount(); auto account = FakeWebSocketServer::createAccount();
auto credentials = new CredentialsStub(user, password); auto credentials = new CredentialsStub(user, password);
account->setCredentials(credentials); account->setCredentials(credentials);
QSignalSpy notificationSpy(account->pushNotifications(), &OCC::PushNotifications::notification); QSignalSpy notificationSpy(account->pushNotifications(), &OCC::PushNotifications::notificationsChanged);
QVERIFY(notificationSpy.isValid()); QVERIFY(notificationSpy.isValid());
// Wait for authentication and then send notify_file push notification // Wait for authentication and then send notify_file push notification
@ -273,6 +273,85 @@ private slots:
auto accountSent = pushNotificationsReady.at(0).at(0).value<OCC::Account *>(); auto accountSent = pushNotificationsReady.at(0).at(0).value<OCC::Account *>();
QCOMPARE(accountSent, account.data()); QCOMPARE(accountSent, account.data());
} }
void testAccount_web_socket_connectionLost_emitNotificationsDisabled()
{
FakeWebSocketServer fakeServer;
auto account = FakeWebSocketServer::createAccount();
QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
QVERIFY(processTextMessageSpy.isValid());
const QString user = "user";
const QString password = "password";
auto credentials = new CredentialsStub(user, password);
account->setCredentials(credentials);
// Need to set reconnect timer interval to zero for tests
account->pushNotifications()->setReconnectTimerInterval(0);
QSignalSpy connectionLostSpy(account->pushNotifications(), &OCC::PushNotifications::connectionLost);
QVERIFY(connectionLostSpy.isValid());
QSignalSpy pushNotificationsDisabledSpy(account.data(), &OCC::Account::pushNotificationsDisabled);
QVERIFY(pushNotificationsDisabledSpy.isValid());
// Wait for authentication and then sent a network error
processTextMessageSpy.wait();
QCOMPARE(processTextMessageSpy.count(), 2);
auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
socket->abort();
QVERIFY(pushNotificationsDisabledSpy.wait());
QCOMPARE(pushNotificationsDisabledSpy.count(), 1);
QCOMPARE(connectionLostSpy.count(), 1);
auto accountSent = pushNotificationsDisabledSpy.at(0).at(0).value<OCC::Account *>();
QCOMPARE(accountSent, account.data());
}
void testAccount_web_socket_authenticationFailed_emitNotificationsDisabled()
{
FakeWebSocketServer fakeServer;
auto account = FakeWebSocketServer::createAccount();
QSignalSpy processTextMessageSpy(&fakeServer, &FakeWebSocketServer::processTextMessage);
QVERIFY(processTextMessageSpy.isValid());
const QString user = "user";
const QString password = "password";
auto credentials = new CredentialsStub(user, password);
account->setCredentials(credentials);
account->pushNotifications()->setReconnectTimerInterval(0);
QSignalSpy authenticationFailedSpy(account->pushNotifications(), &OCC::PushNotifications::authenticationFailed);
QVERIFY(authenticationFailedSpy.isValid());
QSignalSpy pushNotificationsDisabledSpy(account.data(), &OCC::Account::pushNotificationsDisabled);
QVERIFY(pushNotificationsDisabledSpy.isValid());
// Let three authentication attempts fail
QVERIFY(processTextMessageSpy.wait());
QCOMPARE(processTextMessageSpy.count(), 2);
auto socket = processTextMessageSpy.at(0).at(0).value<QWebSocket *>();
socket->sendTextMessage("err: Invalid credentials");
QVERIFY(processTextMessageSpy.wait());
QCOMPARE(processTextMessageSpy.count(), 4);
socket = processTextMessageSpy.at(2).at(0).value<QWebSocket *>();
socket->sendTextMessage("err: Invalid credentials");
QVERIFY(processTextMessageSpy.wait());
QCOMPARE(processTextMessageSpy.count(), 6);
socket = processTextMessageSpy.at(4).at(0).value<QWebSocket *>();
socket->sendTextMessage("err: Invalid credentials");
// Now the authenticationFailed and pushNotificationsDisabled Signals should be emitted
QVERIFY(pushNotificationsDisabledSpy.wait());
QCOMPARE(pushNotificationsDisabledSpy.count(), 1);
QCOMPARE(authenticationFailedSpy.count(), 1);
auto accountSent = pushNotificationsDisabledSpy.at(0).at(0).value<OCC::Account *>();
QCOMPARE(accountSent, account.data());
}
}; };
QTEST_GUILESS_MAIN(TestPushNotifications) QTEST_GUILESS_MAIN(TestPushNotifications)