diff --git a/src/provider/core/provider.cpp b/src/provider/core/provider.cpp index 43fbea4..4a67096 100644 --- a/src/provider/core/provider.cpp +++ b/src/provider/core/provider.cpp @@ -1,740 +1,740 @@ /* Copyright (C) 2016 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include #include "logging_p.h" #include "provider.h" #include "provider_p.h" #include "abstractdatasource.h" #include "startcountsource.h" #include "surveyinfo.h" #include "usagetimesource.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KUserFeedback { #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) Q_LOGGING_CATEGORY(Log, "org.kde.UserFeedback", QtInfoMsg) #else Q_LOGGING_CATEGORY(Log, "org.kde.UserFeedback") #endif } using namespace KUserFeedback; ProviderPrivate::ProviderPrivate(Provider *qq) : q(qq) , networkAccessManager(nullptr) , redirectCount(0) , submissionInterval(-1) , telemetryMode(Provider::NoTelemetry) , surveyInterval(-1) , startCount(0) , usageTime(0) , encouragementStarts(-1) , encouragementTime(-1) , encouragementDelay(300) , encouragementInterval(-1) , backoffIntervalMinutes(-1) { submissionTimer.setSingleShot(true); QObject::connect(&submissionTimer, &QTimer::timeout, q, &Provider::submit); startTime.start(); encouragementTimer.setSingleShot(true); QObject::connect(&encouragementTimer, &QTimer::timeout, q, [this]() { emitShowEncouragementMessage(); }); } ProviderPrivate::~ProviderPrivate() { qDeleteAll(dataSources); } int ProviderPrivate::currentApplicationTime() const { return usageTime + (startTime.elapsed() / 1000); } static QMetaEnum telemetryModeEnum() { const auto idx = Provider::staticMetaObject.indexOfEnumerator("TelemetryMode"); Q_ASSERT(idx >= 0); return Provider::staticMetaObject.enumerator(idx); } std::unique_ptr ProviderPrivate::makeSettings() const { // attempt to put our settings next to the application ones, // so replicate how QSettings handles this auto org = #ifdef Q_OS_MAC QCoreApplication::organizationDomain().isEmpty() ? QCoreApplication::organizationName() : QCoreApplication::organizationDomain(); #else QCoreApplication::organizationName().isEmpty() ? QCoreApplication::organizationDomain() : QCoreApplication::organizationName(); #endif if (org.isEmpty()) org = QLatin1String("Unknown Organization"); std::unique_ptr s(new QSettings(org, QStringLiteral("UserFeedback.") + productId)); return s; } std::unique_ptr ProviderPrivate::makeGlobalSettings() const { const auto org = #ifdef Q_OS_MAC QStringLiteral("kde.org"); #else QStringLiteral("KDE"); #endif std::unique_ptr s(new QSettings(org, QStringLiteral("UserFeedback"))); return s; } void ProviderPrivate::load() { auto s = makeSettings(); s->beginGroup(QStringLiteral("UserFeedback")); lastSubmitTime = s->value(QStringLiteral("LastSubmission")).toDateTime(); const auto modeStr = s->value(QStringLiteral("StatisticsCollectionMode")).toByteArray(); telemetryMode = static_cast(std::max(telemetryModeEnum().keyToValue(modeStr.constData()), 0)); surveyInterval = s->value(QStringLiteral("SurveyInterval"), -1).toInt(); lastSurveyTime = s->value(QStringLiteral("LastSurvey")).toDateTime(); completedSurveys = s->value(QStringLiteral("CompletedSurveys"), QStringList()).toStringList(); startCount = std::max(s->value(QStringLiteral("ApplicationStartCount"), 0).toInt(), 0); usageTime = std::max(s->value(QStringLiteral("ApplicationTime"), 0).toInt(), 0); lastEncouragementTime = s->value(QStringLiteral("LastEncouragement")).toDateTime(); s->endGroup(); foreach (auto source, dataSources) { s->beginGroup(QStringLiteral("Source-") + source->id()); source->load(s.get()); s->endGroup(); } auto g = makeGlobalSettings(); g->beginGroup(QStringLiteral("UserFeedback")); lastSurveyTime = std::max(g->value(QStringLiteral("LastSurvey")).toDateTime(), lastSurveyTime); lastEncouragementTime = std::max(g->value(QStringLiteral("LastEncouragement")).toDateTime(), lastEncouragementTime); } void ProviderPrivate::store() { auto s = makeSettings(); s->beginGroup(QStringLiteral("UserFeedback")); // another process might have changed this, so read the base value first before writing usageTime = std::max(s->value(QStringLiteral("ApplicationTime"), 0).toInt(), usageTime); s->setValue(QStringLiteral("ApplicationTime"), currentApplicationTime()); usageTime = currentApplicationTime(); startTime.restart(); s->endGroup(); foreach (auto source, dataSources) { s->beginGroup(QStringLiteral("Source-") + source->id()); source->store(s.get()); s->endGroup(); } } void ProviderPrivate::storeOne(const QString &key, const QVariant &value) { auto s = makeSettings(); s->beginGroup(QStringLiteral("UserFeedback")); s->setValue(key, value); } void ProviderPrivate::storeOneGlobal(const QString &key, const QVariant &value) { auto s = makeGlobalSettings(); s->beginGroup(QStringLiteral("UserFeedback")); s->setValue(key, value); } void ProviderPrivate::aboutToQuit() { store(); } bool ProviderPrivate::isValidSource(AbstractDataSource *source) const { if (source->id().isEmpty()) { qCWarning(Log) << "Skipping data source with empty name!"; return false; } if (source->telemetryMode() == Provider::NoTelemetry) { qCWarning(Log) << "Source" << source->id() << "attempts to report data unconditionally, ignoring!"; return false; } if (source->description().isEmpty()) { qCWarning(Log) << "Source" << source->id() << "has no description, ignoring!"; return false; } Q_ASSERT(!source->id().isEmpty()); Q_ASSERT(source->telemetryMode() != Provider::NoTelemetry); Q_ASSERT(!source->description().isEmpty()); return true; } QByteArray ProviderPrivate::jsonData(Provider::TelemetryMode mode) const { QJsonObject obj; if (mode != Provider::NoTelemetry) { foreach (auto source, dataSources) { if (!isValidSource(source) || !source->isActive()) continue; if (mode < source->telemetryMode()) continue; const auto data = source->data(); if (data.canConvert()) obj.insert(source->id(), QJsonObject::fromVariantMap(data.toMap())); else if (data.canConvert()) obj.insert(source->id(), QJsonArray::fromVariantList(data.value())); else qCWarning(Log) << "wrong type for" << source->id() << data; } } QJsonDocument doc(obj); return doc.toJson(); } void ProviderPrivate::scheduleNextSubmission(qint64 minTime) { submissionTimer.stop(); if (!q->isEnabled()) return; if (submissionInterval <= 0 || (telemetryMode == Provider::NoTelemetry && surveyInterval < 0)) return; if (minTime == 0) { // If this is a regularly scheduled submission reset the backoff backoffIntervalMinutes = -1; } Q_ASSERT(submissionInterval > 0); const auto nextSubmission = lastSubmitTime.addDays(submissionInterval); const auto now = QDateTime::currentDateTime(); submissionTimer.start(std::max(minTime, now.msecsTo(nextSubmission))); } void ProviderPrivate::submitFinished(QNetworkReply *reply) { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { if (backoffIntervalMinutes == -1) { backoffIntervalMinutes = 2; } else { backoffIntervalMinutes = backoffIntervalMinutes * 2; } qCWarning(Log) << "failed to submit user feedback:" << reply->errorString() << reply->readAll() << ". Calling scheduleNextSubmission with minTime" << backoffIntervalMinutes << "minutes"; scheduleNextSubmission(backoffIntervalMinutes * 60000ll); return; } const auto redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); if (redirectTarget.isValid()) { if (++redirectCount >= 20) { qCWarning(Log) << "Redirect loop on" << reply->url().resolved(redirectTarget).toString(); return; } submit(reply->url().resolved(redirectTarget)); return; } lastSubmitTime = QDateTime::currentDateTime(); auto s = makeSettings(); s->beginGroup(QStringLiteral("UserFeedback")); s->setValue(QStringLiteral("LastSubmission"), lastSubmitTime); s->endGroup(); writeAuditLog(lastSubmitTime); // reset source counters foreach (auto source, dataSources) { s->beginGroup(QStringLiteral("Source-") + source->id()); source->reset(s.get()); s->endGroup(); } const auto obj = QJsonDocument::fromJson(reply->readAll()).object(); const auto it = obj.find(QLatin1String("surveys")); if (it != obj.end() && surveyInterval >= 0) { const auto a = it.value().toArray(); qCDebug(Log) << "received" << a.size() << "surveys"; foreach(const auto &s, a) { const auto survey = SurveyInfo::fromJson(s.toObject()); if (selectSurvey(survey)) break; } } scheduleNextSubmission(); } QVariant ProviderPrivate::sourceData(const QString& sourceId) const { foreach (auto src, dataSources) { if (src->id() == sourceId) return src->data(); } return QVariant(); } bool ProviderPrivate::selectSurvey(const SurveyInfo &survey) const { qCDebug(Log) << "got survey:" << survey.url() << survey.target(); if (!q->isEnabled() || !survey.isValid() || completedSurveys.contains(survey.uuid().toString())) return false; - if (lastSurveyTime.addDays(surveyInterval) > QDateTime::currentDateTime()) + if (surveyInterval != 0 && lastSurveyTime.addDays(surveyInterval) > QDateTime::currentDateTime()) return false; if (!survey.target().isEmpty()) { SurveyTargetExpressionParser parser; if (!parser.parse(survey.target())) { qCDebug(Log) << "failed to parse target expression"; return false; } SurveyTargetExpressionEvaluator eval; eval.setDataProvider(this); if (!eval.evaluate(parser.expression())) return false; } qCDebug(Log) << "picked survey:" << survey.url(); emit q->surveyAvailable(survey); return true; } Provider::TelemetryMode ProviderPrivate::highestTelemetryMode() const { auto mode = Provider::NoTelemetry; foreach (auto src, dataSources) mode = std::max(mode, src->telemetryMode()); return mode; } void ProviderPrivate::scheduleEncouragement() { encouragementTimer.stop(); if (!q->isEnabled()) return; // already done, not repetition if (lastEncouragementTime.isValid() && encouragementInterval <= 0) return; if (encouragementStarts < 0 && encouragementTime < 0) // encouragement disabled return; if (encouragementStarts > startCount) // we need more starts return; if (telemetryMode >= highestTelemetryMode() && surveyInterval == 0) // already everything enabled return; // no repetition if some feedback is enabled if (lastEncouragementTime.isValid() && (telemetryMode > Provider::NoTelemetry || surveyInterval >= 0)) return; Q_ASSERT(encouragementDelay >= 0); int timeToEncouragement = encouragementDelay; if (encouragementTime > 0) timeToEncouragement = std::max(timeToEncouragement, encouragementTime - currentApplicationTime()); if (lastEncouragementTime.isValid()) { Q_ASSERT(encouragementInterval > 0); const auto targetTime = lastEncouragementTime.addDays(encouragementInterval); timeToEncouragement = std::max(timeToEncouragement, (int)QDateTime::currentDateTime().secsTo(targetTime)); } encouragementTimer.start(timeToEncouragement * 1000); } void ProviderPrivate::emitShowEncouragementMessage() { lastEncouragementTime = QDateTime::currentDateTime(); // TODO make this explicit, in case the host application decides to delay? storeOne(QStringLiteral("LastEncouragement"), lastEncouragementTime); storeOneGlobal(QStringLiteral("LastEncouragement"), lastEncouragementTime); emit q->showEncouragementMessage(); } Provider::Provider(QObject *parent) : QObject(parent), d(new ProviderPrivate(this)) { qCDebug(Log); connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, this, [this]() { d->aboutToQuit(); }); auto domain = QCoreApplication::organizationDomain().split(QLatin1Char('.')); std::reverse(domain.begin(), domain.end()); auto id = domain.join(QLatin1String(".")); if (!id.isEmpty()) id += QLatin1Char('.'); id += QCoreApplication::applicationName(); setProductIdentifier(id); } Provider::~Provider() { delete d; } bool Provider::isEnabled() const { auto s = d->makeGlobalSettings(); s->beginGroup(QStringLiteral("UserFeedback")); return s->value(QStringLiteral("Enabled"), true).toBool(); } void Provider::setEnabled(bool enabled) { if (enabled == isEnabled()) return; d->storeOneGlobal(QStringLiteral("Enabled"), enabled); emit enabledChanged(); } QString Provider::productIdentifier() const { return d->productId; } void Provider::setProductIdentifier(const QString &productId) { Q_ASSERT(!productId.isEmpty()); if (productId == d->productId) return; d->productId = productId; d->load(); d->startCount++; d->storeOne(QStringLiteral("ApplicationStartCount"), d->startCount); emit providerSettingsChanged(); d->scheduleEncouragement(); d->scheduleNextSubmission(); } QUrl Provider::feedbackServer() const { return d->serverUrl; } void Provider::setFeedbackServer(const QUrl &url) { if (d->serverUrl == url) return; d->serverUrl = url; emit providerSettingsChanged(); } int Provider::submissionInterval() const { return d->submissionInterval; } void Provider::setSubmissionInterval(int days) { if (d->submissionInterval == days) return; d->submissionInterval = days; emit providerSettingsChanged(); d->scheduleNextSubmission(); } Provider::TelemetryMode Provider::telemetryMode() const { return d->telemetryMode; } void Provider::setTelemetryMode(TelemetryMode mode) { if (d->telemetryMode == mode) return; d->telemetryMode = mode; d->storeOne(QStringLiteral("StatisticsCollectionMode"), QString::fromLatin1(telemetryModeEnum().valueToKey(d->telemetryMode))); d->scheduleNextSubmission(); d->scheduleEncouragement(); emit telemetryModeChanged(); } void Provider::addDataSource(AbstractDataSource *source) { // special cases for sources where we track the data here, as it's needed even if we don't report it if (auto countSrc = dynamic_cast(source)) countSrc->setProvider(d); if (auto timeSrc = dynamic_cast(source)) timeSrc->setProvider(d); d->dataSources.push_back(source); d->dataSourcesById[source->id()] = source; auto s = d->makeSettings(); s->beginGroup(QStringLiteral("Source-") + source->id()); source->load(s.get()); } QVector Provider::dataSources() const { return d->dataSources; } AbstractDataSource *Provider::dataSource(const QString &id) const { auto it = d->dataSourcesById.find(id); return it != std::end(d->dataSourcesById) ? *it : nullptr; } int Provider::surveyInterval() const { return d->surveyInterval; } void Provider::setSurveyInterval(int days) { if (d->surveyInterval == days) return; d->surveyInterval = days; d->storeOne(QStringLiteral("SurveyInterval"), d->surveyInterval); d->scheduleNextSubmission(); d->scheduleEncouragement(); emit surveyIntervalChanged(); } int Provider::applicationStartsUntilEncouragement() const { return d->encouragementStarts; } void Provider::setApplicationStartsUntilEncouragement(int starts) { if (d->encouragementStarts == starts) return; d->encouragementStarts = starts; emit providerSettingsChanged(); d->scheduleEncouragement(); } int Provider::applicationUsageTimeUntilEncouragement() const { return d->encouragementTime; } void Provider::setApplicationUsageTimeUntilEncouragement(int secs) { if (d->encouragementTime == secs) return; d->encouragementTime = secs; emit providerSettingsChanged(); d->scheduleEncouragement(); } int Provider::encouragementDelay() const { return d->encouragementDelay; } void Provider::setEncouragementDelay(int secs) { if (d->encouragementDelay == secs) return; d->encouragementDelay = std::max(0, secs); emit providerSettingsChanged(); d->scheduleEncouragement(); } int Provider::encouragementInterval() const { return d->encouragementInterval; } void Provider::setEncouragementInterval(int days) { if (d->encouragementInterval == days) return; d->encouragementInterval = days; emit providerSettingsChanged(); d->scheduleEncouragement(); } void Provider::surveyCompleted(const SurveyInfo &info) { d->completedSurveys.push_back(info.uuid().toString()); d->lastSurveyTime = QDateTime::currentDateTime(); auto s = d->makeSettings(); s->beginGroup(QStringLiteral("UserFeedback")); s->setValue(QStringLiteral("LastSurvey"), d->lastSurveyTime); s->setValue(QStringLiteral("CompletedSurveys"), d->completedSurveys); d->storeOneGlobal(QStringLiteral("LastSurvey"), d->lastSurveyTime); } void Provider::load() { d->load(); } void Provider::store() { d->store(); } void Provider::submit() { if (!isEnabled()) { qCWarning(Log) << "Global kill switch is enabled"; return; } if (d->productId.isEmpty()) { qCWarning(Log) << "No productId specified!"; return; } if (!d->serverUrl.isValid()) { qCWarning(Log) << "No feedback server URL specified!"; return; } if (!d->networkAccessManager) d->networkAccessManager = new QNetworkAccessManager(this); auto url = d->serverUrl; auto path = d->serverUrl.path(); if (!path.endsWith(QLatin1Char('/'))) path += QLatin1Char('/'); path += QStringLiteral("receiver/submit/") + d->productId; url.setPath(path); d->submitProbe(url); } void ProviderPrivate::submit(const QUrl &url) { QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); request.setHeader(QNetworkRequest::UserAgentHeader, QString(QStringLiteral("KUserFeedback/") + QStringLiteral(KUSERFEEDBACK_VERSION_STRING))); auto reply = networkAccessManager->post(request, jsonData(telemetryMode)); QObject::connect(reply, &QNetworkReply::finished, q, [this, reply]() { submitFinished(reply); }); } void ProviderPrivate::submitProbe(const QUrl &url) { QNetworkRequest request(url); request.setHeader(QNetworkRequest::UserAgentHeader, QString(QStringLiteral("KUserFeedback/") + QStringLiteral(KUSERFEEDBACK_VERSION_STRING))); auto reply = networkAccessManager->get(request); QObject::connect(reply, &QNetworkReply::finished, q, [this, reply]() { submitProbeFinished(reply); }); } void ProviderPrivate::submitProbeFinished(QNetworkReply *reply) { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { qCWarning(Log) << "failed to probe user feedback submission interface:" << reply->errorString() << reply->readAll(); return; } const auto redirectTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); if (redirectTarget.isValid()) { if (++redirectCount >= 20) { qCWarning(Log) << "Redirect loop on" << reply->url().resolved(redirectTarget).toString(); return; } submitProbe(reply->url().resolved(redirectTarget)); return; } submit(reply->url()); } void ProviderPrivate::writeAuditLog(const QDateTime &dt) { const QString path = QStandardPaths::writableLocation(QStandardPaths::DataLocation) + QStringLiteral("/kuserfeedback/audit"); QDir().mkpath(path); QJsonObject docObj; foreach (auto source, dataSources) { if (!isValidSource(source) || !source->isActive() || telemetryMode < source->telemetryMode()) continue; QJsonObject obj; const auto data = source->data(); if (data.canConvert()) obj.insert(QLatin1String("data"), QJsonObject::fromVariantMap(data.toMap())); else if (data.canConvert()) obj.insert(QLatin1String("data"), QJsonArray::fromVariantList(data.value())); if (obj.isEmpty()) continue; obj.insert(QLatin1String("telemetryMode"), QString::fromLatin1(telemetryModeEnum().valueToKey(source->telemetryMode()))); obj.insert(QLatin1String("description"), source->description()); docObj.insert(source->id(), obj); } QFile file(path + QLatin1Char('/') + dt.toString(QStringLiteral("yyyyMMdd-hhmmss")) + QStringLiteral(".log")); if (!file.open(QFile::WriteOnly)) { qCWarning(Log) << "Unable to open audit log file:" << file.fileName() << file.errorString(); return; } QJsonDocument doc(docObj); file.write(doc.toJson()); qCDebug(Log) << "Audit log written:" << file.fileName(); } #include "moc_provider.cpp" diff --git a/src/provider/core/provider.h b/src/provider/core/provider.h index b9ba778..0a55ba2 100644 --- a/src/provider/core/provider.h +++ b/src/provider/core/provider.h @@ -1,302 +1,303 @@ /* Copyright (C) 2016 Volker Krause Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef KUSERFEEDBACK_PROVIDER_H #define KUSERFEEDBACK_PROVIDER_H #include "kuserfeedbackcore_export.h" #include #include #include namespace KUserFeedback { class AbstractDataSource; class ProviderPrivate; class SurveyInfo; /*! The central object managing data sources and transmitting feedback to the server. * * The defaults for this class are very defensive, so in order to make it actually * operational and submit data, there is a number of settings you need to set in * code, namely submission intervals, encouragement settings and adding data sources. * The settings about what data to submit (telemetryMode) and how often * to bother the user with surveys (surveyInterval) should not be set to hardcoded * values in code, but left as choices to the user. */ class KUSERFEEDBACKCORE_EXPORT Provider : public QObject { Q_OBJECT /*! The global enabled state of the feedback functionality. * If this is @c false, all feedback functionality has to be disabled completely. */ Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled NOTIFY enabledChanged) /*! The interval in which the user accepts surveys. * This should be configurable for the user. * @c -1 indicates surveys are disabled. * @see surveyInterval(), setSurveyInterval() */ Q_PROPERTY(int surveyInterval READ surveyInterval WRITE setSurveyInterval NOTIFY surveyIntervalChanged) /*! The telemetry mode the user has configured. * This should be configurable for the user. * @see telemetryMode(), setTelemetryMode() */ Q_PROPERTY(TelemetryMode telemetryMode READ telemetryMode WRITE setTelemetryMode NOTIFY telemetryModeChanged) /*! Unique product id as set on the feedback server. * @see setProductIdentifier */ Q_PROPERTY(QString productIdentifier READ productIdentifier WRITE setProductIdentifier NOTIFY providerSettingsChanged) /*! URL of the feedback server. * @see setFeedbackServer */ Q_PROPERTY(QUrl feedbackServer READ feedbackServer WRITE setFeedbackServer NOTIFY providerSettingsChanged) /*! Submission interval in days. * @see setSubmissionInterval */ Q_PROPERTY(int submissionInterval READ submissionInterval WRITE setSubmissionInterval NOTIFY providerSettingsChanged) /*! Times the application has to be started before an encouragement message is shown. * @see setApplicationStartsUntilEncouragement */ Q_PROPERTY(int applicationStartsUntilEncouragement READ applicationStartsUntilEncouragement WRITE setApplicationStartsUntilEncouragement NOTIFY providerSettingsChanged) /*! Application usage time in seconds before an encouragement message is shown. * @see setApplicationUsageTimeUntilEncouragement */ Q_PROPERTY(int applicationUsageTimeUntilEncouragement READ applicationUsageTimeUntilEncouragement WRITE setApplicationUsageTimeUntilEncouragement NOTIFY providerSettingsChanged) /*! Encouragement delay after application start in seconds. * @see setEncouragementDelay */ Q_PROPERTY(int encouragementDelay READ encouragementDelay WRITE setEncouragementDelay NOTIFY providerSettingsChanged) /*! Encouragement interval. * @see setEncouragementInterval */ Q_PROPERTY(int encouragementInterval READ encouragementInterval WRITE setEncouragementInterval NOTIFY providerSettingsChanged) public: /*! Telemetry collection modes. * Colleciton modes are inclusive, ie. higher modes always imply data from * lower modes too. */ enum TelemetryMode { NoTelemetry, ///< Transmit no data at all. BasicSystemInformation = 0x10, ///< Transmit basic information about the system. BasicUsageStatistics = 0x20, ///< Transmit basic usage statistics. DetailedSystemInformation = 0x30, ///< Transmit detailed system information. DetailedUsageStatistics = 0x40, ///< Transmit detailed usage statistics. }; #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) Q_ENUM(TelemetryMode) #else Q_ENUMS(TelemetryMode) #endif /*! Create a new feedback provider. * @param parent The parent object. */ explicit Provider(QObject *parent = nullptr); ~Provider(); /*! Returns whether feedback functionality is enabled on this system. * This should be checked everywhere showing feedback UI to the user * to respect the global "kill switch" for this. Provider does check * this internally for encouragements, surveys and telemetry submission. */ bool isEnabled() const; /*! Set the global (user-wide) activation state for feedback functionality. * @see isEnabled */ void setEnabled(bool enabled); /*! Returns the current product identifier. */ QString productIdentifier() const; /*! Set the product identifier. * This is used to distinguish independent products on the same server. * If this is not specified, the product identifier is dervied from the application name * organisation domain specified in QCoreApplication. * @param productId Unique product identifier, as configured on the feedback server. */ void setProductIdentifier(const QString &productId); /*! Returns the current feedback server URL. */ QUrl feedbackServer() const; /*! Set the feedback server URL. * This must be called with an appropriate URL for this class to be operational. * @param url The URL of the feedback server. */ void setFeedbackServer(const QUrl &url); /*! Returns the current submission interval. * @return Days between telemetry submissions, or -1 if submission is off. */ int submissionInterval() const; /*! Set the automatic submission interval in days. * This must be called with a positive number for this class to be operational, * as the default is -1 (no submission ever). */ void setSubmissionInterval(int days); /*! Returns the current telemetry collection mode. * The default is NoTelemetry. */ TelemetryMode telemetryMode() const; /*! Set which telemetry data should be submitted. */ void setTelemetryMode(TelemetryMode mode); /*! Adds a data source for telemetry data collection. * @param source The data source to add. The Provider takes ownership of @p source. */ void addDataSource(AbstractDataSource *source); /*! Returns all data sources that have been added to this provider. * @see addDataSource */ QVector dataSources() const; /*! Returns a data source with matched @p id * @param id data source unique identifier * @return pointer to found data source or nullptr if data source is not found */ AbstractDataSource *dataSource(const QString &id) const; /*! Returns the minimum time between two surveys in days. * The default is -1 (no surveys enabled). */ int surveyInterval() const; /*! Sets the minimum time in days between two surveys. * @c -1 indicates no surveys should be requested. + * @c 0 indicates no minimum time between surveys at all (i.e. bother the user as often as you want). */ void setSurveyInterval(int days); /*! Returns the amount of application starts before an encouragement message is shown. */ int applicationStartsUntilEncouragement() const; /*! Set the amount of application starts until the encouragement message should be shown. * The default is -1, ie. no encouragement based on application starts. * @param starts The amount of application starts after which an encouragement * message should be displayed. */ void setApplicationStartsUntilEncouragement(int starts); /*! Returns the amount of application usage time before an encouragement message is shown. */ int applicationUsageTimeUntilEncouragement() const; /*! Set the amount of usage time until the encouragement message should be shown. * The default is -1, ie. no encouragement based on application usage time. * @param secs Amount of seconds until the encouragement should be shown. */ void setApplicationUsageTimeUntilEncouragement(int secs); /*! Returns the current encouragement delay in seconds. */ int encouragementDelay() const; /*! Set the delay after application start for the earliest display of the encouragement message. * The default is 300, ie. 5 minutes after the application start. * @note This only adds an additional contraint on usage time and startup count based * encouragement messages, it does not actually trigger encouragement messages itself. * * @param secs Amount of seconds after the application start for the earliest display * of an encouragement message. * * @see setApplicationStartsUntilEncouragement, setApplicationUsageTimeUntilEncouragement */ void setEncouragementDelay(int secs); /*! Returns the current encouragement interval. */ int encouragementInterval() const; /*! Sets the interval after the encouragement should be repeated. * Encouragement messages are only repeated if no feedback options have been enabled. * The default is -1, that is no repeated encouragement at all. * @param days Days between encouragement messages, 0 disables repeated encouragements. */ void setEncouragementInterval(int days); public Q_SLOTS: /*! Manually submit currently recorded data. */ void submit(); /*! Marks the given survey as completed. This avoids getting further notification * about the same survey. */ void surveyCompleted(const KUserFeedback::SurveyInfo &info); /*! Manually load settings of the provider and all added data sources. * Automatically invoked after object construction and changing product ID. * @note Potentially long operation. */ void load(); /*! Manually store settings of the provider and all added data sources. * Will be autromatically invoked upon @p QCoreApplication::aboutToQuit signal. * @note Potentially long operation. */ void store(); Q_SIGNALS: /*! Emitted whenever there is a new survey available that can be presented * to the user. */ void surveyAvailable(const KUserFeedback::SurveyInfo &survey); /*! Indicate that the encouragement notice should be shown. */ void showEncouragementMessage(); /*! Emitted when the survey interval changed. */ void surveyIntervalChanged(); /*! Emitted when the telemetry collection mode has changed. */ void telemetryModeChanged(); /*! Emitted when any provider setting changed. */ void providerSettingsChanged(); /*! Emitted when the global enabled state changed. */ void enabledChanged(); private: friend class ProviderPrivate; ProviderPrivate * const d; // for UI Q_PRIVATE_SLOT(d, QByteArray jsonData(KUserFeedback::Provider::TelemetryMode)) // for testing Q_PRIVATE_SLOT(d, bool selectSurvey(const KUserFeedback::SurveyInfo&)) }; } Q_DECLARE_METATYPE(KUserFeedback::Provider::TelemetryMode) #endif // KUSERFEEDBACK_PROVIDER_H