1 /*
2  * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.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 "syncstatussummary.h"
16 #include "accountfwd.h"
17 #include "folderman.h"
18 #include "navigationpanehelper.h"
19 #include "networkjobs.h"
20 #include "syncresult.h"
21 #include "tray/usermodel.h"
22 
23 #include <theme.h>
24 
25 namespace {
26 
determineSyncStatus(const OCC::SyncResult & syncResult)27 OCC::SyncResult::Status determineSyncStatus(const OCC::SyncResult &syncResult)
28 {
29     const auto status = syncResult.status();
30 
31     if (status == OCC::SyncResult::Success || status == OCC::SyncResult::Problem) {
32         if (syncResult.hasUnresolvedConflicts()) {
33             return OCC::SyncResult::Problem;
34         }
35         return OCC::SyncResult::Success;
36     } else if (status == OCC::SyncResult::SyncPrepare || status == OCC::SyncResult::Undefined) {
37         return OCC::SyncResult::SyncRunning;
38     }
39     return status;
40 }
41 }
42 
43 namespace OCC {
44 
45 Q_LOGGING_CATEGORY(lcSyncStatusModel, "nextcloud.gui.syncstatusmodel", QtInfoMsg)
46 
SyncStatusSummary(QObject * parent)47 SyncStatusSummary::SyncStatusSummary(QObject *parent)
48     : QObject(parent)
49 {
50     const auto folderMan = FolderMan::instance();
51     connect(folderMan, &FolderMan::folderListChanged, this, &SyncStatusSummary::onFolderListChanged);
52     connect(folderMan, &FolderMan::folderSyncStateChange, this, &SyncStatusSummary::onFolderSyncStateChanged);
53 }
54 
reloadNeeded(AccountState * accountState) const55 bool SyncStatusSummary::reloadNeeded(AccountState *accountState) const
56 {
57     if (_accountState.data() == accountState) {
58         return false;
59     }
60     return true;
61 }
62 
load()63 void SyncStatusSummary::load()
64 {
65     const auto currentUser = UserModel::instance()->currentUser();
66     if (!currentUser) {
67         return;
68     }
69     setAccountState(currentUser->accountState());
70     clearFolderErrors();
71     connectToFoldersProgress(FolderMan::instance()->map());
72     initSyncState();
73 }
74 
syncProgress() const75 double SyncStatusSummary::syncProgress() const
76 {
77     return _progress;
78 }
79 
syncIcon() const80 QUrl SyncStatusSummary::syncIcon() const
81 {
82     return _syncIcon;
83 }
84 
syncing() const85 bool SyncStatusSummary::syncing() const
86 {
87     return _isSyncing;
88 }
89 
onFolderListChanged(const OCC::Folder::Map & folderMap)90 void SyncStatusSummary::onFolderListChanged(const OCC::Folder::Map &folderMap)
91 {
92     connectToFoldersProgress(folderMap);
93 }
94 
markFolderAsError(const Folder * folder)95 void SyncStatusSummary::markFolderAsError(const Folder *folder)
96 {
97     _foldersWithErrors.insert(folder->alias());
98 }
99 
markFolderAsSuccess(const Folder * folder)100 void SyncStatusSummary::markFolderAsSuccess(const Folder *folder)
101 {
102     _foldersWithErrors.erase(folder->alias());
103 }
104 
folderErrors() const105 bool SyncStatusSummary::folderErrors() const
106 {
107     return _foldersWithErrors.size() != 0;
108 }
109 
folderError(const Folder * folder) const110 bool SyncStatusSummary::folderError(const Folder *folder) const
111 {
112     return _foldersWithErrors.find(folder->alias()) != _foldersWithErrors.end();
113 }
114 
clearFolderErrors()115 void SyncStatusSummary::clearFolderErrors()
116 {
117     _foldersWithErrors.clear();
118 }
119 
setSyncStateForFolder(const Folder * folder)120 void SyncStatusSummary::setSyncStateForFolder(const Folder *folder)
121 {
122     if (_accountState && !_accountState->isConnected()) {
123         setSyncing(false);
124         setSyncStatusString(tr("Offline"));
125         setSyncStatusDetailString("");
126         setSyncIcon(Theme::instance()->folderOffline());
127         return;
128     }
129 
130     const auto state = determineSyncStatus(folder->syncResult());
131 
132     switch (state) {
133     case SyncResult::Success:
134     case SyncResult::SyncPrepare:
135         // Success should only be shown if all folders were fine
136         if (!folderErrors() || folderError(folder)) {
137             setSyncing(false);
138             setSyncStatusString(tr("All synced!"));
139             setSyncStatusDetailString("");
140             setSyncIcon(Theme::instance()->syncStatusOk());
141             markFolderAsSuccess(folder);
142         }
143         break;
144     case SyncResult::Error:
145     case SyncResult::SetupError:
146         setSyncing(false);
147         setSyncStatusString(tr("Some files couldn't be synced!"));
148         setSyncStatusDetailString(tr("See below for errors"));
149         setSyncIcon(Theme::instance()->syncStatusError());
150         markFolderAsError(folder);
151         break;
152     case SyncResult::SyncRunning:
153     case SyncResult::NotYetStarted:
154         setSyncing(true);
155         setSyncStatusString(tr("Syncing"));
156         setSyncStatusDetailString("");
157         setSyncIcon(Theme::instance()->syncStatusRunning());
158         break;
159     case SyncResult::Paused:
160     case SyncResult::SyncAbortRequested:
161         setSyncing(false);
162         setSyncStatusString(tr("Sync paused"));
163         setSyncStatusDetailString("");
164         setSyncIcon(Theme::instance()->syncStatusPause());
165         break;
166     case SyncResult::Problem:
167     case SyncResult::Undefined:
168         setSyncing(false);
169         setSyncStatusString(tr("Some files could not be synced!"));
170         setSyncStatusDetailString(tr("See below for warnings"));
171         setSyncIcon(Theme::instance()->syncStatusWarning());
172         markFolderAsError(folder);
173         break;
174     }
175 }
176 
onFolderSyncStateChanged(const Folder * folder)177 void SyncStatusSummary::onFolderSyncStateChanged(const Folder *folder)
178 {
179     if (!folder) {
180         return;
181     }
182 
183     if (!_accountState || folder->accountState() != _accountState.data()) {
184         return;
185     }
186 
187     setSyncStateForFolder(folder);
188 }
189 
calculateOverallPercent(qint64 totalFileCount,qint64 completedFile,qint64 totalSize,qint64 completedSize)190 constexpr double calculateOverallPercent(
191     qint64 totalFileCount, qint64 completedFile, qint64 totalSize, qint64 completedSize)
192 {
193     int overallPercent = 0;
194     if (totalFileCount > 0) {
195         // Add one 'byte' for each file so the percentage is moving when deleting or renaming files
196         overallPercent = qRound(double(completedSize + completedFile) / double(totalSize + totalFileCount) * 100.0);
197     }
198     overallPercent = qBound(0, overallPercent, 100);
199     return overallPercent / 100.0;
200 }
201 
onFolderProgressInfo(const ProgressInfo & progress)202 void SyncStatusSummary::onFolderProgressInfo(const ProgressInfo &progress)
203 {
204     const qint64 completedSize = progress.completedSize();
205     const qint64 currentFile = progress.currentFile();
206     const qint64 completedFile = progress.completedFiles();
207     const qint64 totalSize = qMax(completedSize, progress.totalSize());
208     const qint64 totalFileCount = qMax(currentFile, progress.totalFiles());
209 
210     setSyncProgress(calculateOverallPercent(totalFileCount, completedFile, totalSize, completedSize));
211 
212     if (totalSize > 0) {
213         const auto completedSizeString = Utility::octetsToString(completedSize);
214         const auto totalSizeString = Utility::octetsToString(totalSize);
215 
216         if (progress.trustEta()) {
217             setSyncStatusDetailString(
218                 tr("%1 of %2 · %3 left")
219                     .arg(completedSizeString, totalSizeString)
220                     .arg(Utility::durationToDescriptiveString1(progress.totalProgress().estimatedEta)));
221         } else {
222             setSyncStatusDetailString(tr("%1 of %2").arg(completedSizeString, totalSizeString));
223         }
224     }
225 
226     if (totalFileCount > 0) {
227         setSyncStatusString(tr("Syncing file %1 of %2").arg(currentFile).arg(totalFileCount));
228     }
229 }
230 
setSyncing(bool value)231 void SyncStatusSummary::setSyncing(bool value)
232 {
233     if (value == _isSyncing) {
234         return;
235     }
236 
237     _isSyncing = value;
238     emit syncingChanged();
239 }
240 
setSyncProgress(double value)241 void SyncStatusSummary::setSyncProgress(double value)
242 {
243     if (_progress == value) {
244         return;
245     }
246 
247     _progress = value;
248     emit syncProgressChanged();
249 }
250 
setSyncStatusString(const QString & value)251 void SyncStatusSummary::setSyncStatusString(const QString &value)
252 {
253     if (_syncStatusString == value) {
254         return;
255     }
256 
257     _syncStatusString = value;
258     emit syncStatusStringChanged();
259 }
260 
syncStatusString() const261 QString SyncStatusSummary::syncStatusString() const
262 {
263     return _syncStatusString;
264 }
265 
syncStatusDetailString() const266 QString SyncStatusSummary::syncStatusDetailString() const
267 {
268     return _syncStatusDetailString;
269 }
270 
setSyncIcon(const QUrl & value)271 void SyncStatusSummary::setSyncIcon(const QUrl &value)
272 {
273     if (_syncIcon == value) {
274         return;
275     }
276 
277     _syncIcon = value;
278     emit syncIconChanged();
279 }
280 
setSyncStatusDetailString(const QString & value)281 void SyncStatusSummary::setSyncStatusDetailString(const QString &value)
282 {
283     if (_syncStatusDetailString == value) {
284         return;
285     }
286 
287     _syncStatusDetailString = value;
288     emit syncStatusDetailStringChanged();
289 }
290 
connectToFoldersProgress(const Folder::Map & folderMap)291 void SyncStatusSummary::connectToFoldersProgress(const Folder::Map &folderMap)
292 {
293     for (const auto &folder : folderMap) {
294         if (folder->accountState() == _accountState.data()) {
295             connect(
296                 folder, &Folder::progressInfo, this, &SyncStatusSummary::onFolderProgressInfo, Qt::UniqueConnection);
297         } else {
298             disconnect(folder, &Folder::progressInfo, this, &SyncStatusSummary::onFolderProgressInfo);
299         }
300     }
301 }
302 
onIsConnectedChanged()303 void SyncStatusSummary::onIsConnectedChanged()
304 {
305     setSyncStateToConnectedState();
306 }
307 
setSyncStateToConnectedState()308 void SyncStatusSummary::setSyncStateToConnectedState()
309 {
310     setSyncing(false);
311     setSyncStatusDetailString("");
312     if (_accountState && !_accountState->isConnected()) {
313         setSyncStatusString(tr("Offline"));
314         setSyncIcon(Theme::instance()->folderOffline());
315     } else {
316         setSyncStatusString(tr("All synced!"));
317         setSyncIcon(Theme::instance()->syncStatusOk());
318     }
319 }
320 
setAccountState(AccountStatePtr accountState)321 void SyncStatusSummary::setAccountState(AccountStatePtr accountState)
322 {
323     if (!reloadNeeded(accountState.data())) {
324         return;
325     }
326     if (_accountState) {
327         disconnect(
328             _accountState.data(), &AccountState::isConnectedChanged, this, &SyncStatusSummary::onIsConnectedChanged);
329     }
330     _accountState = accountState;
331     connect(_accountState.data(), &AccountState::isConnectedChanged, this, &SyncStatusSummary::onIsConnectedChanged);
332 }
333 
initSyncState()334 void SyncStatusSummary::initSyncState()
335 {
336     auto syncStateFallbackNeeded = true;
337     for (const auto &folder : FolderMan::instance()->map()) {
338         onFolderSyncStateChanged(folder);
339         syncStateFallbackNeeded = false;
340     }
341 
342     if (syncStateFallbackNeeded) {
343         setSyncStateToConnectedState();
344     }
345 }
346 }
347