#include "accountmanager.h"
#include "owncloudgui.h"
+#include <pushnotifications.h>
#include "syncengine.h"
#include "ocsjob.h"
#include "configfile.h"
this, &User::slotRefresh);
connect(_account.data(), &AccountState::stateChanged,
- [=]() { if (isConnected()) {slotRefresh();} });
+ [=]() { if (isConnected()) {slotRefreshImmediately();} });
connect(_account.data(), &AccountState::stateChanged, this, &User::accountStateChanged);
connect(_account.data(), &AccountState::hasFetchedNavigationApps,
this, &User::slotRebuildNavigationAppList);
void User::setNotificationRefreshInterval(std::chrono::milliseconds interval)
{
- qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval";
- _notificationCheckTimer.start(interval.count());
+ if (!checkPushNotificationsAreReady()) {
+ 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() {
if (_account.data() && _account.data()->isConnected()) {
- this->slotRefreshActivities();
+ slotRefreshActivities();
}
- this->slotRefreshNotifications();
+ slotRefreshNotifications();
}
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.
if (!_timeSinceLastCheck.contains(_account.data())) {
_timeSinceLastCheck[_account.data()].invalidate();
}
if (_account.data() && _account.data()->isConnected()) {
if (!timer.isValid()) {
- this->slotRefreshActivities();
+ slotRefreshActivities();
}
- this->slotRefreshNotifications();
+ slotRefreshNotifications();
timer.start();
}
}
void setNotificationRefreshInterval(std::chrono::milliseconds interval);
void slotRebuildNavigationAppList();
+private:
+ void slotPushNotificationsReady();
+ void slotDisconnectPushNotifications();
+ void slotReceivedPushNotification(Account *account);
+ void slotReceivedPushActivity(Account *account);
+
+ void connectPushNotifications() const;
+ bool checkPushNotificationsAreReady() const;
+
private:
AccountStatePtr _account;
bool _isCurrentUser;
qCInfo(lcAccount) << "Delete push notifications object because authentication failed or connection lost";
_pushNotifications->deleteLater();
_pushNotifications = nullptr;
+ emit pushNotificationsDisabled(this);
};
connect(_pushNotifications, &PushNotifications::connectionLost, this, deletePushNotifications);
void appPasswordRetrieved(QString);
void pushNotificationsReady(Account *account);
+ void pushNotificationsDisabled(Account *account);
protected Q_SLOTS:
void slotCredentialsFetched();
pushNotificationTypes.setFlag(PushNotificationType::Files);
}
+ if (types.contains("activities")) {
+ pushNotificationTypes.setFlag(PushNotificationType::Activities);
+ }
+
+ if (types.contains("notifications")) {
+ pushNotificationTypes.setFlag(PushNotificationType::Notifications);
+ }
+
return pushNotificationTypes;
}
enum PushNotificationType {
None = 0,
- Files = 1
+ Files = 1,
+ Activities = 2,
+ Notifications = 4
};
Q_DECLARE_FLAGS(PushNotificationTypes, PushNotificationType)
Q_DECLARE_OPERATORS_FOR_FLAGS(PushNotificationTypes)
if (message == "notify_file") {
handleNotifyFile();
- } else if (message == "notify_activity" || message == "notify_notification") {
- handleNotification();
+ } else if (message == "notify_activity") {
+ handleNotifyActivity();
+ } else if (message == "notify_notification") {
+ handleNotifyNotification();
} else if (message == "authenticated") {
handleAuthenticated();
} else if (message == "err: Invalid credentials") {
}
}
-void PushNotifications::handleNotification()
+void PushNotifications::handleNotifyNotification()
{
- qCInfo(lcPushNotifications) << "Notification or activity push notification arrived";
- emit notification(_account);
+ qCInfo(lcPushNotifications) << "Push notification arrived";
+ emit notificationsChanged(_account);
+}
+
+void PushNotifications::handleNotifyActivity()
+{
+ qCInfo(lcPushNotifications) << "Push activity arrived";
+ emit activitiesChanged(_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
void handleAuthenticated();
void handleNotifyFile();
void handleInvalidCredentials();
- void handleNotification();
+ void handleNotifyNotification();
+ void handleNotifyActivity();
Account *_account = nullptr;
QWebSocket *_webSocket = nullptr;
QStringList typeList;
typeList.append("files");
+ typeList.append("activities");
+ typeList.append("notifications");
QString websocketUrl("ws://localhost:12345");
Q_OBJECT
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()
{
QStringList typeList;
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()
{
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 notificationsPushNotificationsAvailable = capabilities.availablePushNotifications().testFlag(OCC::PushNotificationType::Notifications);
+ QCOMPARE(activitiesPushNotificationsAvailable, false);
QCOMPARE(filesPushNotificationsAvailable, false);
+ QCOMPARE(notificationsPushNotificationsAvailable, false);
}
void testPushNotificationsWebSocketUrl_urlAvailable_returnUrl()
auto account = FakeWebSocketServer::createAccount();
auto credentials = new CredentialsStub(user, password);
account->setCredentials(credentials);
- QSignalSpy notificationSpy(account->pushNotifications(), &OCC::PushNotifications::notification);
- QVERIFY(notificationSpy.isValid());
+ QSignalSpy activitySpy(account->pushNotifications(), &OCC::PushNotifications::activitiesChanged);
+ QVERIFY(activitySpy.isValid());
// Wait for authentication and then send notify_file push notification
QVERIFY(processTextMessageSpy.wait());
socket->sendTextMessage("notify_activity");
// notification signal should be emitted
- QVERIFY(notificationSpy.wait());
- QCOMPARE(notificationSpy.count(), 1);
- auto accountFilesChanged = notificationSpy.at(0).at(0).value<OCC::Account *>();
+ QVERIFY(activitySpy.wait());
+ QCOMPARE(activitySpy.count(), 1);
+ auto accountFilesChanged = activitySpy.at(0).at(0).value<OCC::Account *>();
QCOMPARE(accountFilesChanged, account.data());
}
auto account = FakeWebSocketServer::createAccount();
auto credentials = new CredentialsStub(user, password);
account->setCredentials(credentials);
- QSignalSpy notificationSpy(account->pushNotifications(), &OCC::PushNotifications::notification);
+ QSignalSpy notificationSpy(account->pushNotifications(), &OCC::PushNotifications::notificationsChanged);
QVERIFY(notificationSpy.isValid());
// Wait for authentication and then send notify_file push notification
auto accountSent = pushNotificationsReady.at(0).at(0).value<OCC::Account *>();
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)