1/*
2 * Copyright (C) by Klaas Freitag <freitag@owncloud.com>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful, but
10 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * for more details.
13 */
14
15#include <QtGui>
16#include <QtWidgets>
17
18#include "activitylistmodel.h"
19#include "activitywidget.h"
20#include "syncresult.h"
21#include "logger.h"
22#include "theme.h"
23#include "folderman.h"
24#include "syncfileitem.h"
25#include "folder.h"
26#include "openfilemanager.h"
27#include "owncloudpropagator.h"
28#include "account.h"
29#include "accountstate.h"
30#include "accountmanager.h"
31#include "activityitemdelegate.h"
32#include "protocolwidget.h"
33#include "issueswidget.h"
34#include "QProgressIndicator.h"
35#include "notificationwidget.h"
36#include "notificationconfirmjob.h"
37#include "servernotificationhandler.h"
38#include "theme.h"
39#include "ocsjob.h"
40
41#include "ui_activitywidget.h"
42
43#include <climits>
44
45// time span in milliseconds which has to be between two
46// refreshes of the notifications
47#define NOTIFICATION_REQUEST_FREE_PERIOD 15000
48
49namespace OCC {
50
51ActivityWidget::ActivityWidget(QWidget *parent)
52 : QWidget(parent)
53 , _ui(new Ui::ActivityWidget)
54 , _notificationRequestsRunning(0)
55{
56 _ui->setupUi(this);
57
58// Adjust copyToClipboard() when making changes here!
59#if defined(Q_OS_MAC)
60 _ui->_activityList->setMinimumWidth(400);
61#endif
62
63 _model = new ActivityListModel(this);
64 ActivityItemDelegate *delegate = new ActivityItemDelegate;
65 delegate->setParent(this);
66 _ui->_activityList->setItemDelegate(delegate);
67 _ui->_activityList->setAlternatingRowColors(true);
68 _ui->_activityList->setModel(_model);
69
70 _ui->_notifyLabel->hide();
71 _ui->_notifyScroll->hide();
72
73 // Create a widget container for the notifications. The ui file defines
74 // a scroll area that get a widget with a layout as children
75 QWidget *w = new QWidget;
76 _notificationsLayout = new QVBoxLayout;
77 w->setLayout(_notificationsLayout);
78 _notificationsLayout->setAlignment(Qt::AlignTop);
79 _ui->_notifyScroll->setAlignment(Qt::AlignTop);
80 _ui->_notifyScroll->setWidget(w);
81
82 showLabels();
83
84 connect(_model, &ActivityListModel::activityJobStatusCode,
85 this, &ActivityWidget::slotAccountActivityStatus);
86
87 connect(AccountManager::instance(), &AccountManager::accountRemoved, this, [this](AccountState *ast) {
88 if (_accountsWithoutActivities.remove(ast->account()->displayName()))
89 showLabels();
90 });
91
92 _copyBtn = _ui->_dialogButtonBox->addButton(tr("Copy"), QDialogButtonBox::ActionRole);
93 _copyBtn->setToolTip(tr("Copy the activity list to the clipboard."));
94 connect(_copyBtn, &QAbstractButton::clicked, this, &ActivityWidget::copyToClipboard);
95
96 connect(_model, &QAbstractItemModel::rowsInserted, this, &ActivityWidget::rowsInserted);
97
98 connect(_ui->_activityList, &QListView::activated, this, &ActivityWidget::slotOpenFile);
99
100 connect(&_removeTimer, &QTimer::timeout, this, &ActivityWidget::slotCheckToCleanWidgets);
101 _removeTimer.setInterval(1000);
102}
103
104ActivityWidget::~ActivityWidget()
105{
106 delete _ui;
107}
108
109void ActivityWidget::slotRefreshActivities(AccountState *ptr)
110{
111 _model->slotRefreshActivity(ptr);
112}
113
114void ActivityWidget::slotRefreshNotifications(AccountState *ptr)
115{
116 // start a server notification handler if no notification requests
117 // are running
118 if (_notificationRequestsRunning == 0) {
119 ServerNotificationHandler *snh = new ServerNotificationHandler;
120 connect(snh, &ServerNotificationHandler::newNotificationList,
121 this, &ActivityWidget::slotBuildNotificationDisplay);
122
123 snh->slotFetchNotifications(ptr);
124 } else {
125 qCWarning(lcActivity) << "Notification request counter not zero.";
126 }
127}
128
129void ActivityWidget::slotRemoveAccount(AccountState *ptr)
130{
131 _model->slotRemoveAccount(ptr);
132}
133
134void ActivityWidget::showLabels()
135{
136 QString t = tr("Server Activities");
137 _ui->_headerLabel->setTextFormat(Qt::RichText);
138 _ui->_headerLabel->setText(t);
139
140 _ui->_notifyLabel->setText(tr("Action Required: Notifications"));
141
142 t.clear();
143 QSetIterator<QString> i(_accountsWithoutActivities);
144 while (i.hasNext()) {
145 t.append(tr("<br/>Account %1 does not have activities enabled.").arg(i.next()));
146 }
147 _ui->_bottomLabel->setTextFormat(Qt::RichText);
148 _ui->_bottomLabel->setText(t);
149}
150
151void ActivityWidget::slotAccountActivityStatus(AccountState *ast, int statusCode)
152{
153 if (!(ast && ast->account())) {
154 return;
155 }
156 if (statusCode == 999) {
157 _accountsWithoutActivities.insert(ast->account()->displayName());
158 } else {
159 _accountsWithoutActivities.remove(ast->account()->displayName());
160 }
161
162 checkActivityTabVisibility();
163 showLabels();
164}
165
166// FIXME: Reused from protocol widget. Move over to utilities.
167QString ActivityWidget::timeString(QDateTime dt, QLocale::FormatType format) const
168{
169 const QLocale loc = QLocale::system();
170 QString dtFormat = loc.dateTimeFormat(format);
171 static const QRegExp re("(HH|H|hh|h):mm(?!:s)");
172 dtFormat.replace(re, "\\1:mm:ss");
173 return loc.toString(dt, dtFormat);
174}
175
176void ActivityWidget::storeActivityList(QTextStream &ts)
177{
178 ActivityList activities = _model->activityList();
179
180 foreach (Activity activity, activities) {
181 ts << right
182 // account name
183 << qSetFieldWidth(30)
184 << activity._accName
185 // separator
186 << qSetFieldWidth(0) << ","
187
188 // date and time
189 << qSetFieldWidth(34)
190 << activity._dateTime.toString()
191 // separator
192 << qSetFieldWidth(0) << ","
193
194 // file
195 << qSetFieldWidth(30)
196 << activity._file
197 // separator
198 << qSetFieldWidth(0) << ","
199
200 // subject
201 << qSetFieldWidth(100)
202 << activity._subject
203 // separator
204 << qSetFieldWidth(0) << ","
205
206 // message (mostly empty)
207 << qSetFieldWidth(55)
208 << activity._message
209 //
210 << qSetFieldWidth(0)
211 << endl;
212 }
213}
214
215void ActivityWidget::checkActivityTabVisibility()
216{
217 int accountCount = AccountManager::instance()->accounts().count();
218 bool hasAccountsWithActivity =
219 _accountsWithoutActivities.count() != accountCount;
220 bool hasNotifications = !_widgetForNotifId.isEmpty();
221
222 _ui->_headerLabel->setVisible(hasAccountsWithActivity);
223 _ui->_activityList->setVisible(hasAccountsWithActivity);
224
225 _ui->_notifyLabel->setVisible(hasNotifications);
226 _ui->_notifyScroll->setVisible(hasNotifications);
227
228 emit hideActivityTab(!hasAccountsWithActivity && !hasNotifications);
229}
230
231void ActivityWidget::slotOpenFile(QModelIndex indx)
232{
233 qCDebug(lcActivity) << indx.isValid() << indx.data(ActivityItemDelegate::PathRole).toString() << QFile::exists(indx.data(ActivityItemDelegate::PathRole).toString());
234 if (indx.isValid()) {
235 QString fullPath = indx.data(ActivityItemDelegate::PathRole).toString();
236
237 if (QFile::exists(fullPath)) {
238 showInFileManager(fullPath);
239 }
240 }
241}
242
243// GUI: Display the notifications.
244// All notifications in list are coming from the same account
245// but in the _widgetForNotifId hash widgets for all accounts are
246// collected.
247void ActivityWidget::slotBuildNotificationDisplay(const ActivityList &list)
248{
249 QHash<QString, int> accNotified;
250 QString listAccountName;
251
252 // Whether a new notification widget was added to the notificationLayout.
253 bool newNotificationShown = false;
254
255 foreach (auto activity, list) {
256 if (_blacklistedNotifications.contains(activity)) {
257 qCInfo(lcActivity) << "Activity in blacklist, skip";
258 continue;
259 }
260
261 NotificationWidget *widget = 0;
262
263 if (_widgetForNotifId.contains(activity.ident())) {
264 widget = _widgetForNotifId[activity.ident()];
265 } else {
266 widget = new NotificationWidget(this);
267 connect(widget, &NotificationWidget::sendNotificationRequest,
268 this, &ActivityWidget::slotSendNotificationRequest);
269 connect(widget, &NotificationWidget::requestCleanupAndBlacklist,
270 this, &ActivityWidget::slotRequestCleanupAndBlacklist);
271
272 _notificationsLayout->addWidget(widget);
273// _ui->_notifyScroll->setMinimumHeight( widget->height());
274 _ui->_notifyScroll->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContentsOnFirstShow);
275 _widgetForNotifId[activity.ident()] = widget;
276 newNotificationShown = true;
277 }
278
279 widget->setActivity(activity);
280
281 // remember the list account name for the strayCat handling below.
282 listAccountName = activity._accName;
283
284 // handle gui logs. In order to NOT annoy the user with every fetching of the
285 // notifications the notification id is stored in a Set. Only if an id
286 // is not in the set, it qualifies for guiLog.
287 // Important: The _guiLoggedNotifications set must be wiped regularly which
288 // will repeat the gui log.
289
290 // after one hour, clear the gui log notification store
291 if (_guiLogTimer.elapsed() > 60 * 60 * 1000) {
292 _guiLoggedNotifications.clear();
293 }
294 if (!_guiLoggedNotifications.contains(activity._id)) {
295 QString host = activity._accName;
296 // store the name of the account that sends the notification to be
297 // able to add it to the tray notification
298 // remove the user name from the account as that is not accurate here.
299 int indx = host.lastIndexOf(QChar('@'));
300 if (indx > -1) {
301 host.remove(0, 1 + indx);
302 }
303 if (!host.isEmpty()) {
304 if (accNotified.contains(host)) {
305 accNotified[host] = accNotified[host] + 1;
306 } else {
307 accNotified[host] = 1;
308 }
309 }
310 _guiLoggedNotifications.insert(activity._id);
311 }
312 }
313
314 // check if there are widgets that have no corresponding activity from
315 // the server any more. Collect them in a list
316 QList<Activity::Identifier> strayCats;
317 foreach (auto id, _widgetForNotifId.keys()) {
318 NotificationWidget *widget = _widgetForNotifId[id];
319
320 bool found = false;
321 // do not mark widgets of other accounts to delete.
322 if (widget->activity()._accName != listAccountName) {
323 continue;
324 }
325
326 foreach (auto activity, list) {
327 if (activity.ident() == id) {
328 // found an activity
329 found = true;
330 break;
331 }
332 }
333 if (!found) {
334 // the activity does not exist any more.
335 strayCats.append(id);
336 }
337 }
338
339 // .. and now delete all these stray cat widgets.
340 foreach (auto strayCatId, strayCats) {
341 NotificationWidget *widgetToGo = _widgetForNotifId[strayCatId];
342 scheduleWidgetToRemove(widgetToGo, 0);
343 }
344
345 checkActivityTabVisibility();
346
347 int newGuiLogCount = accNotified.count();
348
349 if (newGuiLogCount > 0) {
350 // restart the gui log timer now that we show a notification
351 _guiLogTimer.start();
352
353 // Assemble a tray notification
354 QString msg = tr("You received %n new notification(s) from %2.", "", accNotified[accNotified.keys().at(0)]).arg(accNotified.keys().at(0));
355
356 if (newGuiLogCount >= 2) {
357 QString acc1 = accNotified.keys().at(0);
358 QString acc2 = accNotified.keys().at(1);
359 if (newGuiLogCount == 2) {
360 int notiCount = accNotified[acc1] + accNotified[acc2];
361 msg = tr("You received %n new notification(s) from %1 and %2.", "", notiCount).arg(acc1, acc2);
362 } else {
363 msg = tr("You received new notifications from %1, %2 and other accounts.").arg(acc1, acc2);
364 }
365 }
366
367 const QString log = tr("%1 Notifications - Action Required").arg(Theme::instance()->appNameGUI());
368 emit guiLog(log, msg);
369 }
370
371 if (newNotificationShown) {
372 emit newNotification();
373 }
374}
375
376void ActivityWidget::slotSendNotificationRequest(const QString &accountName, const QString &link, const QByteArray &verb)
377{
378 qCInfo(lcActivity) << "Server Notification Request " << verb << link << "on account" << accountName;
379 NotificationWidget *theSender = qobject_cast<NotificationWidget *>(sender());
380
381 const QStringList validVerbs = QStringList() << "GET"
382 << "PUT"
383 << "POST"
384 << "DELETE";
385
386 if (validVerbs.contains(verb)) {
387 AccountStatePtr acc = AccountManager::instance()->account(accountName);
388 if (acc) {
389 NotificationConfirmJob *job = new NotificationConfirmJob(acc->account());
390 QUrl l(link);
391 job->setLinkAndVerb(l, verb);
392 job->setWidget(theSender);
393 connect(job, &AbstractNetworkJob::networkError,
394 this, &ActivityWidget::slotNotifyNetworkError);
395 connect(job, &NotificationConfirmJob::jobFinished,
396 this, &ActivityWidget::slotNotifyServerFinished);
397 job->start();
398
399 // count the number of running notification requests. If this member var
400 // is larger than zero, no new fetching of notifications is started
401 _notificationRequestsRunning++;
402 }
403 } else {
404 qCWarning(lcActivity) << "Notification Links: Invalid verb:" << verb;
405 }
406}
407
408void ActivityWidget::endNotificationRequest(NotificationWidget *widget, int replyCode)
409{
410 _notificationRequestsRunning--;
411 if (widget) {
412 widget->slotNotificationRequestFinished(replyCode);
413 }
414}
415
416void ActivityWidget::slotNotifyNetworkError(QNetworkReply *reply)
417{
418 NotificationConfirmJob *job = qobject_cast<NotificationConfirmJob *>(sender());
419 if (!job) {
420 return;
421 }
422
423 int resultCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
424
425 endNotificationRequest(job->widget(), resultCode);
426 qCWarning(lcActivity) << "Server notify job failed with code " << resultCode;
427}
428
429void ActivityWidget::slotNotifyServerFinished(const QString &reply, int replyCode)
430{
431 NotificationConfirmJob *job = qobject_cast<NotificationConfirmJob *>(sender());
432 if (!job) {
433 return;
434 }
435
436 endNotificationRequest(job->widget(), replyCode);
437 qCInfo(lcActivity) << "Server Notification reply code" << replyCode << reply;
438
439 // if the notification was successful start a timer that triggers
440 // removal of the done widgets in a few seconds
441 // Add 200 millisecs to the predefined value to make sure that the timer in
442 // widget's method readyToClose() has elapsed.
443 if (replyCode == OCS_SUCCESS_STATUS_CODE || replyCode == OCS_SUCCESS_STATUS_CODE_V2) {
444 scheduleWidgetToRemove(job->widget());
445 }
446}
447
448// blacklist the activity coming in here.
449void ActivityWidget::slotRequestCleanupAndBlacklist(const Activity &blacklistActivity)
450{
451 if (!_blacklistedNotifications.contains(blacklistActivity)) {
452 _blacklistedNotifications.append(blacklistActivity);
453 }
454
455 NotificationWidget *widget = _widgetForNotifId[blacklistActivity.ident()];
456 scheduleWidgetToRemove(widget);
457}
458
459void ActivityWidget::scheduleWidgetToRemove(NotificationWidget *widget, int milliseconds)
460{
461 if (!widget) {
462 return;
463 }
464 // in five seconds from now, remove the widget.
465 QDateTime removeTime = QDateTime::currentDateTimeUtc().addMSecs(milliseconds);
466 QDateTime &it = _widgetsToRemove[widget];
467 if (!it.isValid() || it > removeTime) {
468 it = removeTime;
469 }
470 if (!_removeTimer.isActive()) {
471 _removeTimer.start();
472 }
473}
474
475// Called every second to see if widgets need to be removed.
476void ActivityWidget::slotCheckToCleanWidgets()
477{
478 auto currentTime = QDateTime::currentDateTimeUtc();
479 auto it = _widgetsToRemove.begin();
480 while (it != _widgetsToRemove.end()) {
481 // loop over all widgets in the to-remove queue
482 QDateTime t = it.value();
483 NotificationWidget *widget = it.key();
484
485 if (currentTime > t) {
486 // found one to remove!
487 Activity::Identifier id = widget->activity().ident();
488 _widgetForNotifId.remove(id);
489 widget->deleteLater();
490 it = _widgetsToRemove.erase(it);
491 } else {
492 ++it;
493 }
494 }
495
496 if (_widgetsToRemove.isEmpty()) {
497 _removeTimer.stop();
498 }
499
500 // check to see if the whole notification pane should be hidden
501 if (_widgetForNotifId.isEmpty()) {
502 _ui->_notifyLabel->setHidden(true);
503 _ui->_notifyScroll->setHidden(true);
504 }
505}
506
507
508/* ==================================================================== */
509
510ActivitySettings::ActivitySettings(QWidget *parent)
511 : QWidget(parent)
512{
513 QHBoxLayout *hbox = new QHBoxLayout(this);
514 setLayout(hbox);
515
516 // create a tab widget for the three activity views
517 _tab = new QTabWidget(this);
518 hbox->addWidget(_tab);
519 _activityWidget = new ActivityWidget(this);
520 _activityTabId = _tab->addTab(_activityWidget, Theme::instance()->applicationIcon(), tr("Server Activity"));
521 connect(_activityWidget, &ActivityWidget::copyToClipboard, this, &ActivitySettings::slotCopyToClipboard);
522 connect(_activityWidget, &ActivityWidget::hideActivityTab, this, &ActivitySettings::setActivityTabHidden);
523 connect(_activityWidget, &ActivityWidget::guiLog, this, &ActivitySettings::guiLog);
524 connect(_activityWidget, &ActivityWidget::newNotification, this, &ActivitySettings::slotShowActivityTab);
525
526 _protocolWidget = new ProtocolWidget(this);
527 _protocolTabId = _tab->addTab(_protocolWidget, Theme::instance()->syncStateIcon(SyncResult::Success), tr("Sync Protocol"));
528 connect(_protocolWidget, &ProtocolWidget::copyToClipboard, this, &ActivitySettings::slotCopyToClipboard);
529
530 _issuesWidget = new IssuesWidget(this);
531 _syncIssueTabId = _tab->addTab(_issuesWidget, Theme::instance()->syncStateIcon(SyncResult::Problem), QString());
532 slotShowIssueItemCount(0); // to display the label.
533 connect(_issuesWidget, &IssuesWidget::issueCountUpdated,
534 this, &ActivitySettings::slotShowIssueItemCount);
535 connect(_issuesWidget, &IssuesWidget::copyToClipboard,
536 this, &ActivitySettings::slotCopyToClipboard);
537
538 // Add a progress indicator to spin if the acitivity list is updated.
539 _progressIndicator = new QProgressIndicator(this);
540 _tab->setCornerWidget(_progressIndicator);
541
542 connect(&_notificationCheckTimer, &QTimer::timeout,
543 this, &ActivitySettings::slotRegularNotificationCheck);
544
545 // connect a model signal to stop the animation.
546 connect(_activityWidget, &ActivityWidget::rowsInserted, _progressIndicator, &QProgressIndicator::stopAnimation);
547
548 // We want the protocol be the default
549 _tab->setCurrentIndex(1);
550}
551
552void ActivitySettings::setNotificationRefreshInterval(std::chrono::milliseconds interval)
553{
554 qCDebug(lcActivity) << "Starting Notification refresh timer with " << interval.count() / 1000 << " sec interval";
555 _notificationCheckTimer.start(interval.count());
556}
557
558void ActivitySettings::setActivityTabHidden(bool hidden)
559{
560 if (hidden && _activityTabId > -1) {
561 _tab->removeTab(_activityTabId);
562 _activityTabId = -1;
563 _protocolTabId -= 1;
564 _syncIssueTabId -= 1;
565 }
566
567 if (!hidden && _activityTabId == -1) {
568 _activityTabId = _tab->insertTab(0, _activityWidget, Theme::instance()->applicationIcon(), tr("Server Activity"));
569 _protocolTabId += 1;
570 _syncIssueTabId += 1;
571 }
572}
573
574void ActivitySettings::slotShowIssueItemCount(int cnt)
575{
576 QString cntText = tr("Not Synced");
577 if (cnt) {
578 //: %1 is the number of not synced files.
579 cntText = tr("Not Synced (%1)").arg(cnt);
580 }
581 _tab->setTabText(_syncIssueTabId, cntText);
582}
583
584void ActivitySettings::slotShowActivityTab()
585{
586 if (_activityTabId != -1) {
587 _tab->setCurrentIndex(_activityTabId);
588 }
589}
590
591void ActivitySettings::slotShowIssuesTab(const QString &folderAlias)
592{
593 if (_syncIssueTabId == -1)
594 return;
595 _tab->setCurrentIndex(_syncIssueTabId);
596
597 _issuesWidget->showFolderErrors(folderAlias);
598}
599
600void ActivitySettings::slotCopyToClipboard()
601{
602 QString text;
603 QTextStream ts(&text);
604
605 int idx = _tab->currentIndex();
606 QString message;
607
608 if (idx == _activityTabId) {
609 // the activity widget
610 _activityWidget->storeActivityList(ts);
611 message = tr("The server activity list has been copied to the clipboard.");
612 } else if (idx == _protocolTabId) {
613 // the protocol widget
614 _protocolWidget->storeSyncActivity(ts);
615 message = tr("The sync activity list has been copied to the clipboard.");
616 } else if (idx == _syncIssueTabId) {
617 // issues Widget
618 message = tr("The list of unsynced items has been copied to the clipboard.");
619 _issuesWidget->storeSyncIssues(ts);
620 }
621
622 QApplication::clipboard()->setText(text);
623 emit guiLog(tr("Copied to clipboard"), message);
624}
625
626void ActivitySettings::slotRemoveAccount(AccountState *ptr)
627{
628 _activityWidget->slotRemoveAccount(ptr);
629}
630
631void ActivitySettings::slotRefresh(AccountState *ptr)
632{
633 // QElapsedTimer isn't actually constructed as invalid.
634 if (!_timeSinceLastCheck.contains(ptr)) {
635 _timeSinceLastCheck[ptr].invalidate();
636 }
637 QElapsedTimer &timer = _timeSinceLastCheck[ptr];
638
639 // Fetch Activities only if visible and if last check is longer than 15 secs ago
640 if (timer.isValid() && timer.elapsed() < NOTIFICATION_REQUEST_FREE_PERIOD) {
641 qCDebug(lcActivity) << "Do not check as last check is only secs ago: " << timer.elapsed() / 1000;
642 return;
643 }
644 if (ptr && ptr->isConnected()) {
645 if (isVisible() || !timer.isValid()) {
646 _progressIndicator->startAnimation();
647 _activityWidget->slotRefreshActivities(ptr);
648 }
649 _activityWidget->slotRefreshNotifications(ptr);
650 timer.start();
651 }
652}
653
654void ActivitySettings::slotRegularNotificationCheck()
655{
656 AccountManager *am = AccountManager::instance();
657 foreach (AccountStatePtr a, am->accounts()) {
658 slotRefresh(a.data());
659 }
660}
661
662bool ActivitySettings::event(QEvent *e)
663{
664 if (e->type() == QEvent::Show) {
665 AccountManager *am = AccountManager::instance();
666 foreach (AccountStatePtr a, am->accounts()) {
667 slotRefresh(a.data());
668 }
669 }
670 return QWidget::event(e);
671}
672
673ActivitySettings::~ActivitySettings()
674{
675}
676}
677