diff --git a/changelog/unreleased/4808 b/changelog/unreleased/4808 new file mode 100644 index 00000000000..d8351d48d10 --- /dev/null +++ b/changelog/unreleased/4808 @@ -0,0 +1,9 @@ +Enhancement: Add option to pause synchronization on metered connections + +On platforms that support metered-connection detection, an option is now +available to pause folder synchronization when the network connection +switches to metered. When synchronization is paused, force-syncing can +still be done. + +https://github.com/owncloud/client/issues/4808 +https://github.com/owncloud/client/pull/11305 diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 9bba0023550..a9b09499b7b 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -43,6 +43,7 @@ #include #include #include +#include #include #include #include @@ -642,19 +643,42 @@ void AccountSettings::slotScheduleCurrentFolderForceFullDiscovery() void AccountSettings::slotForceSyncCurrentFolder() { if (auto selectedFolder = this->selectedFolder()) { - // Terminate and reschedule any running sync - for (auto *folder : FolderMan::instance()->folders()) { - if (folder->isSyncRunning()) { - folder->slotTerminateSync(tr("User triggered force sync")); - FolderMan::instance()->scheduler()->enqueueFolder(folder); - } + if (Utility::internetConnectionIsMetered() && ConfigFile().pauseSyncWhenMetered()) { + auto messageBox = new QMessageBox(QMessageBox::Question, tr("Internet connection is metered"), + tr("Synchronization is paused because the Internet connection is a metered connection" + "

Do you really want to force a Synchronization now?"), + QMessageBox::Yes | QMessageBox::No, ocApp()->gui()->settingsDialog()); + messageBox->setAttribute(Qt::WA_DeleteOnClose); + connect(messageBox, &QMessageBox::accepted, this, [this, selectedFolder] { doForceSyncCurrentFolder(selectedFolder); }); + messageBox->open(); + ownCloudGui::raiseDialog(messageBox); + } else { + doForceSyncCurrentFolder(selectedFolder); } + } +} - selectedFolder->slotWipeErrorBlacklist(); // issue #6757 - selectedFolder->slotNextSyncFullLocalDiscovery(); // ensure we don't forget about local errors - // Insert the selected folder at the front of the queue - FolderMan::instance()->scheduler()->enqueueFolder(selectedFolder, SyncScheduler::Priority::High); +void AccountSettings::doForceSyncCurrentFolder(Folder *selectedFolder) +{ + // Prevent new sync starts + FolderMan::instance()->scheduler()->stop(); + + // Terminate and reschedule any running sync + for (auto *folder : FolderMan::instance()->folders()) { + if (folder->isSyncRunning()) { + folder->slotTerminateSync(tr("User triggered force sync")); + FolderMan::instance()->scheduler()->enqueueFolder(folder); + } } + + selectedFolder->slotWipeErrorBlacklist(); // issue #6757 + selectedFolder->slotNextSyncFullLocalDiscovery(); // ensure we don't forget about local errors + + // Insert the selected folder at the front of the queue + FolderMan::instance()->scheduler()->enqueueFolder(selectedFolder, SyncScheduler::Priority::High); + + // Restart scheduler + FolderMan::instance()->scheduler()->start(); } void AccountSettings::slotAccountStateChanged() @@ -670,19 +694,27 @@ void AccountSettings::slotAccountStateChanged() _model->slotUpdateFolderState(folder); } + auto acceptOAuthLogin = [this]() { + if (_askForOAuthLoginDialog != nullptr) { + _askForOAuthLoginDialog->accept(); + } + }; + const QString server = QStringLiteral("%1") .arg(Utility::escape(safeUrl.toString())); switch (state) { + case AccountState::PausedDueToMetered: + showConnectionLabel(tr("Sync to %1 is paused due to metered internet connection.").arg(server)); + acceptOAuthLogin(); + break; case AccountState::Connected: { QStringList errors; if (account->serverSupportLevel() != Account::ServerSupportLevel::Supported) { errors << tr("The server version %1 is unsupported! Proceed at your own risk.").arg(account->capabilities().status().versionString()); } showConnectionLabel(tr("Connected to %1.").arg(server), errors); - if (_askForOAuthLoginDialog != nullptr) { - _askForOAuthLoginDialog->accept(); - } + acceptOAuthLogin(); break; } case AccountState::ServiceUnavailable: diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index a58aeb3b39c..2cd39c0354b 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -89,6 +89,7 @@ protected slots: QStringList errors = QStringList()); bool event(QEvent *) override; void createAccountToolbox(); + void doForceSyncCurrentFolder(Folder *selectedFolder); /// Returns the alias of the selected folder, empty string if none Folder *selectedFolder() const; diff --git a/src/gui/accountstate.cpp b/src/gui/accountstate.cpp index 598cc895e95..b9d3f72e816 100644 --- a/src/gui/accountstate.cpp +++ b/src/gui/accountstate.cpp @@ -18,6 +18,7 @@ #include "application.h" #include "configfile.h" #include "fetchserversettings.h" +#include "guiutility.h" #include "libsync/creds/abstractcredentials.h" #include "libsync/creds/httpcredentials.h" @@ -136,6 +137,18 @@ AccountState::AccountState(AccountPtr account) break; } }); + + connect(qNetInfo, &QNetworkInformation::isMeteredChanged, this, [this](bool isMetered) { + if (ConfigFile().pauseSyncWhenMetered()) { + if (state() == State::Connected && isMetered) { + qCInfo(lcAccountState) << "Network switched to a metered connection, setting account state to PausedDueToMetered"; + setState(State::PausedDueToMetered); + } else if (state() == State::PausedDueToMetered && !isMetered) { + qCInfo(lcAccountState) << "Network switched to a NON-metered connection, setting account state to Connected"; + setState(State::Connected); + } + } + }); } #endif // as a fallback and to recover after server issues we also poll @@ -231,6 +244,8 @@ void AccountState::setState(State state) _connectionValidator->deleteLater(); _connectionValidator.clear(); checkConnectivity(); + } else if (_state == Connected && Utility::internetConnectionIsMetered() && ConfigFile().pauseSyncWhenMetered()) { + _state = PausedDueToMetered; } } @@ -290,7 +305,7 @@ void AccountState::signIn() bool AccountState::isConnected() const { - return _state == Connected; + return _state == Connected || _state == PausedDueToMetered; } void AccountState::tagLastSuccessfullETagRequest(const QDateTime &tp) diff --git a/src/gui/accountstate.h b/src/gui/accountstate.h index 19d5d019c4c..ecf01d41363 100644 --- a/src/gui/accountstate.h +++ b/src/gui/accountstate.h @@ -78,6 +78,11 @@ class AccountState : public QObject /// We are currently asking the user for credentials AskingCredentials, + /// We are on a metered internet connection, and the user preference + /// is to pause syncing in this case. This state is entered from and + /// left to a `Connected` state. + PausedDueToMetered, + Connecting }; Q_ENUM(State) diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index c619c92a8e5..f35f611b212 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -806,7 +806,6 @@ void Folder::setVirtualFilesEnabled(bool enabled) }; if (isSyncRunning()) { connect(this, &Folder::syncFinished, this, finalizeVfsSwitch, Qt::SingleShotConnection); - QString reason; slotTerminateSync(tr("Switching VFS mode on folder '%1'").arg(displayName())); } else { finalizeVfsSwitch(); diff --git a/src/gui/folder.h b/src/gui/folder.h index 1585fb79fd9..c5e597f7aa6 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -277,6 +277,7 @@ class Folder : public QObject { return *_engine; } + Vfs &vfs() { OC_ENFORCE(_vfs); diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index fcc1217be4e..f10ad5c4de0 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -73,7 +73,7 @@ void TrayOverallStatusResult::addResult(Folder *f) lastSyncDone = time; } - auto status = f->syncPaused() ? SyncResult::Paused : f->syncResult().status(); + auto status = f->syncPaused() || f->accountState()->state() == AccountState::PausedDueToMetered ? SyncResult::Paused : f->syncResult().status(); if (status == SyncResult::Undefined) { status = SyncResult::Problem; } diff --git a/src/gui/folderstatusmodel.cpp b/src/gui/folderstatusmodel.cpp index d598c167d2c..8425af9a1c3 100644 --- a/src/gui/folderstatusmodel.cpp +++ b/src/gui/folderstatusmodel.cpp @@ -303,7 +303,7 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const auto status = f->syncResult(); if (!accountConnected) { status.setStatus(SyncResult::Status::Offline); - } else if (f->syncPaused()) { + } else if (f->syncPaused() || f->accountState()->state() == AccountState::PausedDueToMetered) { status.setStatus(SyncResult::Status::Paused); } return Theme::instance()->syncStateIconName(status); diff --git a/src/gui/guiutility.cpp b/src/gui/guiutility.cpp index 5301503c888..67adbe7346b 100644 --- a/src/gui/guiutility.cpp +++ b/src/gui/guiutility.cpp @@ -16,13 +16,14 @@ #include "application.h" #include "settingsdialog.h" -#include #include +#include #include +#include #include #include +#include #include -#include #include "theme.h" @@ -94,6 +95,15 @@ QString Utility::vfsFreeSpaceActionText() return QCoreApplication::translate("utility", "Free up local space"); } +bool Utility::internetConnectionIsMetered() +{ + if (auto *qNetInfo = QNetworkInformation::instance()) { + return qNetInfo->isMetered(); + } + + return false; +} + void Utility::markDirectoryAsSyncRoot(const QString &path) { Q_ASSERT(getDirectorySyncRootMarking(path).isEmpty()); diff --git a/src/gui/guiutility.h b/src/gui/guiutility.h index a259573b5a4..92931016867 100644 --- a/src/gui/guiutility.h +++ b/src/gui/guiutility.h @@ -51,10 +51,11 @@ namespace Utility { QString socketApiSocketPath(); + bool internetConnectionIsMetered(); + void markDirectoryAsSyncRoot(const QString &path); QString getDirectorySyncRootMarking(const QString &path); void unmarkDirectoryAsSyncRoot(const QString &path); - } // namespace Utility } // namespace OCC diff --git a/src/gui/main.cpp b/src/gui/main.cpp index 83d16529711..5d882ff79e6 100644 --- a/src/gui/main.cpp +++ b/src/gui/main.cpp @@ -449,16 +449,20 @@ int main(int argc, char **argv) return -1; } + setupLogging(options); + #if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0) + qCDebug(lcMain) << QNetworkInformation::availableBackends().join(QStringLiteral(", ")); if (!QNetworkInformation::loadDefaultBackend()) { qCWarning(lcMain) << "Failed to load QNetworkInformation"; + } else { + qCDebug(lcMain) << "Loaded network information backend:" << QNetworkInformation::instance()->backendName() + << "supported features:" << QNetworkInformation::instance()->supportedFeatures(); } #else qCWarning(lcMain) << "QNetworkInformation is not available"; #endif - setupLogging(options); - platform->setApplication(&app); auto folderManager = FolderMan::createInstance(); diff --git a/src/gui/networksettings.cpp b/src/gui/networksettings.cpp index c327d08d9b3..e31224b8db7 100644 --- a/src/gui/networksettings.cpp +++ b/src/gui/networksettings.cpp @@ -22,6 +22,7 @@ #include "theme.h" #include +#include #include #include #include @@ -69,6 +70,7 @@ NetworkSettings::NetworkSettings(QWidget *parent) loadProxySettings(); loadBWLimitSettings(); + loadMeteredSettings(); // proxy connect(_ui->typeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &NetworkSettings::saveProxySettings); @@ -92,6 +94,8 @@ NetworkSettings::NetworkSettings(QWidget *parent) connect(_ui->hostLineEdit, &QLineEdit::textChanged, this, &NetworkSettings::checkEmptyProxyHost); checkEmptyProxyHost(); checkAccountLocalhost(); + + connect(_ui->pauseSyncWhenMeteredCheckbox, &QAbstractButton::clicked, this, &NetworkSettings::saveMeteredSettings); } NetworkSettings::~NetworkSettings() @@ -177,6 +181,21 @@ void NetworkSettings::loadBWLimitSettings() _ui->uploadSpinBox->setValue(cfgFile.uploadLimit()); } +void NetworkSettings::loadMeteredSettings() +{ + if (QNetworkInformation *qNetInfo = QNetworkInformation::instance()) { + if (Utility::isWindows() // The backend implements the metered feature, but does not report it as supported. + // See https://bugreports.qt.io/browse/QTBUG-118741 + || qNetInfo->supports(QNetworkInformation::Feature::Metered)) { + _ui->pauseSyncWhenMeteredCheckbox->setChecked(ConfigFile().pauseSyncWhenMetered()); + return; + } + } + + _ui->pauseSyncWhenMeteredCheckbox->setEnabled(false); + _ui->pauseSyncWhenMeteredCheckbox->setToolTip(tr("Querying metered connection status is not supported on this platform")); +} + void NetworkSettings::saveProxySettings() { ConfigFile cfgFile; @@ -231,6 +250,13 @@ void NetworkSettings::saveBWLimitSettings() FolderMan::instance()->setDirtyNetworkLimits(); } +void NetworkSettings::saveMeteredSettings() +{ + bool pauseSyncWhenMetered = _ui->pauseSyncWhenMeteredCheckbox->isChecked(); + ConfigFile().setPauseSyncWhenMetered(pauseSyncWhenMetered); + FolderMan::instance()->scheduler()->setPauseSyncWhenMetered(pauseSyncWhenMetered); +} + void NetworkSettings::checkEmptyProxyHost() { if (_ui->hostLineEdit->isEnabled() && _ui->hostLineEdit->text().isEmpty()) { diff --git a/src/gui/networksettings.h b/src/gui/networksettings.h index 6c6ef5f9e4f..d1694c6606d 100644 --- a/src/gui/networksettings.h +++ b/src/gui/networksettings.h @@ -41,6 +41,7 @@ class NetworkSettings : public QWidget private slots: void saveProxySettings(); void saveBWLimitSettings(); + void saveMeteredSettings(); /// Red marking of host field if empty and enabled void checkEmptyProxyHost(); @@ -53,6 +54,7 @@ private slots: private: void loadProxySettings(); void loadBWLimitSettings(); + void loadMeteredSettings(); CredentialManager *_credentialManager; diff --git a/src/gui/networksettings.ui b/src/gui/networksettings.ui index 7e2476126b4..c2b4fec3919 100644 --- a/src/gui/networksettings.ui +++ b/src/gui/networksettings.ui @@ -14,6 +14,13 @@ Form + + + + Pause synchronization when the Internet connection is metered + + + diff --git a/src/gui/scheduling/syncscheduler.cpp b/src/gui/scheduling/syncscheduler.cpp index 1c1652f6fcd..a487efa3768 100644 --- a/src/gui/scheduling/syncscheduler.cpp +++ b/src/gui/scheduling/syncscheduler.cpp @@ -19,6 +19,10 @@ #include "libsync/configfile.h" #include "libsync/syncengine.h" +#include "guiutility.h" + +#include + using namespace std::chrono_literals; using namespace OCC; @@ -27,6 +31,28 @@ Q_LOGGING_CATEGORY(lcSyncScheduler, "gui.scheduler.syncscheduler", QtInfoMsg) class FolderPriorityQueue { +private: + struct Element + { + Element() { } + + Element(Folder *f, SyncScheduler::Priority p) + : folder(f) + , rawFolder(f) + , priority(p) + { + } + + // We don't own the folder, so it might get deleted + QPointer folder = nullptr; + // raw pointer for lookup in _scheduledFolders + Folder *rawFolder = nullptr; + SyncScheduler::Priority priority = SyncScheduler::Priority::Low; + + friend bool operator<(const Element &lhs, const Element &rhs) { return lhs.priority < rhs.priority; } + }; + + public: FolderPriorityQueue() = default; @@ -59,37 +85,19 @@ class FolderPriorityQueue auto empty() { return _queue.empty(); } auto size() { return _queue.size(); } - QPointer pop() + std::pair pop() { - QPointer out; - while (!_queue.empty() && !out) { + Element out; + while (!_queue.empty() && !out.folder) { // could be a nullptr by now - out = _queue.top().folder; + out = _queue.top(); _scheduledFolders.erase(_queue.top().rawFolder); _queue.pop(); } - return out; + return std::make_pair(out.folder, out.priority); } private: - struct Element - { - Element(Folder *f, SyncScheduler::Priority p) - : folder(f) - , rawFolder(f) - , priority(p) - { - } - - // We don't own the folder, so it might get deleted - QPointer folder; - // raw pointer for lookup in _scheduledFolders - Folder *rawFolder; - SyncScheduler::Priority priority; - - friend bool operator<(const Element &lhs, const Element &rhs) { return lhs.priority < rhs.priority; } - }; - // the actual queue std::priority_queue _queue; // helper container to ensure we don't enqueue a Folder multiple times @@ -98,6 +106,7 @@ class FolderPriorityQueue SyncScheduler::SyncScheduler(FolderMan *parent) : QObject(parent) + , _pauseSyncWhenMetered(ConfigFile().pauseSyncWhenMetered()) , _queue(new FolderPriorityQueue) { new ETagWatcher(parent, this); @@ -147,11 +156,23 @@ void SyncScheduler::startNext() } auto nextSync = _queue->pop(); - while (nextSync && !nextSync->canSync()) { + while (nextSync.first && !nextSync.first->canSync()) { nextSync = _queue->pop(); } - _currentSync = nextSync; + _currentSync = nextSync.first; + auto syncPriority = nextSync.second; + if (!_currentSync.isNull()) { + if (_pauseSyncWhenMetered && Utility::internetConnectionIsMetered()) { + if (syncPriority == Priority::High) { + qCInfo(lcSyncScheduler) << "Scheduler is paused due to metered internet connection, BUT next sync is HIGH priority, so allow sync to start"; + } else { + enqueueFolder(_currentSync, syncPriority); + qCInfo(lcSyncScheduler) << "Scheduler is paused due to metered internet connection, next sync is not started"; + return; + } + } + connect( _currentSync, &Folder::syncFinished, this, [this](const SyncResult &result) { @@ -205,3 +226,11 @@ bool SyncScheduler::hasCurrentRunningSyncRunning() const { return _currentSync; } + +void SyncScheduler::setPauseSyncWhenMetered(bool pauseSyncWhenMetered) +{ + _pauseSyncWhenMetered = pauseSyncWhenMetered; + if (!pauseSyncWhenMetered) { + startNext(); + } +} diff --git a/src/gui/scheduling/syncscheduler.h b/src/gui/scheduling/syncscheduler.h index 5e2ad54234e..779f5d89201 100644 --- a/src/gui/scheduling/syncscheduler.h +++ b/src/gui/scheduling/syncscheduler.h @@ -56,11 +56,14 @@ class SyncScheduler : public QObject bool hasCurrentRunningSyncRunning() const; + void setPauseSyncWhenMetered(bool pauseSyncWhenMetered); + private: void startNext(); bool _running = false; + bool _pauseSyncWhenMetered; QPointer _currentSync; FolderPriorityQueue *_queue; }; diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp index 92cc339c3d2..a9ae3db1a2f 100644 --- a/src/libsync/configfile.cpp +++ b/src/libsync/configfile.cpp @@ -97,6 +97,10 @@ const QString downloadLimitC() { return QStringLiteral("BWLimit/downloadLimit"); const QString newBigFolderSizeLimitC() { return QStringLiteral("newBigFolderSizeLimit"); } const QString useNewBigFolderSizeLimitC() { return QStringLiteral("useNewBigFolderSizeLimit"); } const QString confirmExternalStorageC() { return QStringLiteral("confirmExternalStorage"); } +const QString pauseSyncWhenMeteredC() +{ + return QStringLiteral("pauseWhenMetered"); +} const QString moveToTrashC() { return QStringLiteral("moveToTrash"); } const QString issuesWidgetFilterC() @@ -721,6 +725,16 @@ void ConfigFile::setConfirmExternalStorage(bool isChecked) setValue(confirmExternalStorageC(), isChecked); } +bool ConfigFile::pauseSyncWhenMetered() const +{ + return getValue(pauseSyncWhenMeteredC(), {}, false).toBool(); +} + +void ConfigFile::setPauseSyncWhenMetered(bool isChecked) +{ + setValue(pauseSyncWhenMeteredC(), isChecked); +} + bool ConfigFile::moveToTrash() const { if (Theme::instance()->enableMoveToTrash()) { diff --git a/src/libsync/configfile.h b/src/libsync/configfile.h index da86e9c1847..4eb3c76a706 100644 --- a/src/libsync/configfile.h +++ b/src/libsync/configfile.h @@ -143,6 +143,9 @@ class OWNCLOUDSYNC_EXPORT ConfigFile bool confirmExternalStorage() const; void setConfirmExternalStorage(bool); + bool pauseSyncWhenMetered() const; + void setPauseSyncWhenMetered(bool isChecked); + /** If we should move the files deleted on the server in the trash */ bool moveToTrash() const; void setMoveToTrash(bool);