diff --git a/autotests/datasourcetest.cpp b/autotests/datasourcetest.cpp index 0a4064a..16ad7d9 100644 --- a/autotests/datasourcetest.cpp +++ b/autotests/datasourcetest.cpp @@ -1,261 +1,293 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KUserFeedback; class DataSourceTest : public QObject { Q_OBJECT Q_PROPERTY(int prop READ prop WRITE setProp NOTIFY propChanged) public: int prop() const { return m_propValue; } void setProp(int v) { m_propValue = v; emit propChanged(); } signals: void propChanged(); private: int m_propValue = 42; private slots: void initTestCase() { QCoreApplication::setOrganizationDomain(QStringLiteral("kde.org")); QCoreApplication::setOrganizationName(QStringLiteral("KDE")); QStandardPaths::setTestModeEnabled(true); } void testPlatformInfoSource() { PlatformInfoSource src; auto obj = src.data().toMap(); QVERIFY(obj.contains(QLatin1String("os"))); auto v = obj.value(QLatin1String("os")); QCOMPARE(v.type(), QVariant::String); auto s = v.toString(); QVERIFY(!s.isEmpty()); QVERIFY(obj.contains(QLatin1String("version"))); v = obj.value(QLatin1String("version")); QCOMPARE(v.type(), QVariant::String); s = v.toString(); QVERIFY(!s.isEmpty()); } void testScreenInfoSource() { ScreenInfoSource src; auto v = src.data(); QVERIFY(v.canConvert()); auto a = v.value(); QVERIFY(a.size() > 0); for (int i = 0; i < a.size(); ++i) { v = a.at(i); QVERIFY(v.canConvert()); const auto scr = v.toMap(); QVERIFY(scr.contains(QLatin1String("height"))); QVERIFY(scr.contains(QLatin1String("width"))); QVERIFY(scr.contains(QLatin1String("dpi"))); } } void testPropertyRatioSource() { PropertyRatioSource src(this, "prop", QStringLiteral("ratioSample")); src.addValueMapping(42, QStringLiteral("value1")); src.addValueMapping(23, QStringLiteral("value2")); QTest::qWait(1); auto v = src.data(); QVERIFY(v.canConvert()); auto o = v.toMap(); QCOMPARE(o.size(), 0); // nothing recorded QTest::qWait(1200); v = src.data(); o = v.toMap(); QCOMPARE(o.size(), 1); QVERIFY(o.contains(QLatin1String("value1"))); v = o.value(QLatin1String("value1")).toMap().value(QLatin1String("property"));; QCOMPARE(v.type(), QVariant::Double); setProp(23); QTest::qWait(1200); v = src.data(); o = v.toMap(); QCOMPARE(o.size(), 2); QVERIFY(o.contains(QLatin1String("value2"))); v = o.value(QLatin1String("value2")).toMap().value(QLatin1String("property")); QCOMPARE(v.type(), QVariant::Double); } void testMultiPropertyRatioSource() { QSettings s; s.remove(QStringLiteral("MultiPropSource")); s.beginGroup(QStringLiteral("MultiPropSource")); { setProp(5198); PropertyRatioSource src1(this, "prop", QStringLiteral("ratioSample")); src1.addValueMapping(5198, QStringLiteral("value1")); src1.load(&s); PropertyRatioSource src2(this, "prop", QStringLiteral("ratioSample")); src2.addValueMapping(5198, QStringLiteral("value2")); src2.load(&s); QTest::qWait(1200); src1.store(&s); src2.store(&s); } { PropertyRatioSource src3(this, "prop", QStringLiteral("ratioSample")); src3.load(&s); const auto map = src3.data().toMap(); QCOMPARE(map.size(), 2); QVERIFY(map.contains(QStringLiteral("value1"))); QVERIFY(map.contains(QStringLiteral("value2"))); src3.reset(&s); QVERIFY(src3.data().toMap().isEmpty()); } { PropertyRatioSource src4(this, "prop", QStringLiteral("ratioSample")); src4.load(&s); const auto map = src4.data().toMap(); QCOMPARE(map.size(), 0); } } void testApplicationVersionSource() { ApplicationVersionSource src; auto v = src.data(); QVERIFY(v.isNull()); QCoreApplication::setApplicationVersion(QStringLiteral("1.9.84")); auto m = src.data().toMap(); QVERIFY(m.contains(QLatin1String("value"))); QCOMPARE(m.value(QLatin1String("value")).toString(), QLatin1String("1.9.84")); } void testQtVersionSource() { QtVersionSource src; const auto m = src.data().toMap(); QVERIFY(m.contains(QLatin1String("value"))); QCOMPARE(m.value(QLatin1String("value")).toString(), QLatin1String(QT_VERSION_STR)); } void testStartCountSource() { Provider p; auto src = new StartCountSource; QVERIFY(!src->description().isEmpty()); p.addDataSource(src); const auto m = src->data().toMap(); QVERIFY(m.contains(QLatin1String("value"))); QVERIFY(m.value(QLatin1String("value")).toInt() >= 1); } void testUsageTimeSource() { Provider p; auto src = new UsageTimeSource; QVERIFY(!src->description().isEmpty()); src->setTelemetryMode(Provider::DetailedUsageStatistics); p.addDataSource(src); QTest::qWait(1200); const auto m = src->data().toMap(); QVERIFY(m.contains(QLatin1String("value"))); QVERIFY(m.value(QLatin1String("value")).toInt() >= 1); } void testCpuInfoSource() { CpuInfoSource src; const auto m = src.data().toMap(); QVERIFY(m.contains(QLatin1String("architecture"))); QVERIFY(!m.value(QLatin1String("architecture")).toString().isEmpty()); QVERIFY(m.contains(QLatin1String("count"))); QVERIFY(m.value(QLatin1String("count")).toInt() >= 1); } void testLocaleInfoSource() { LocaleInfoSource src; const auto m = src.data().toMap(); QVERIFY(m.contains(QLatin1String("language"))); QVERIFY(!m.value(QLatin1String("language")).toString().isEmpty()); QVERIFY(m.contains(QLatin1String("region"))); QVERIFY(!m.value(QLatin1String("region")).toString().isEmpty()); } void testCompilerInfoSource() { CompilerInfoSource src; const auto m = src.data().toMap(); QVERIFY(m.contains(QLatin1String("type"))); QVERIFY(!m.value(QLatin1String("type")).toString().isEmpty()); QVERIFY(m.contains(QLatin1String("version"))); QVERIFY(!m.value(QLatin1String("version")).toString().isEmpty()); } void testQPAInfoSource() { QPAInfoSource src; QVERIFY(!src.description().isEmpty()); const auto m = src.data().toMap(); QVERIFY(m.contains(QLatin1String("name"))); QVERIFY(!m.value(QLatin1String("name")).toString().isEmpty()); } + + void testActiveState() + { + CpuInfoSource src; + + QVERIFY(src.isActive()); + + src.setActive(false); + + QVERIFY(!src.isActive()); + } + + void testCommonActiveStateSetting() + { + CpuInfoSource src; + src.setActive(false); + + QSettings s; + + src.store(&s); + + src.setActive(true); + + src.load(&s); + + QVERIFY(!src.isActive()); + + // NOTE: reset shouldn't change global settings that might be changed by a user via UI + src.reset(&s); + + QVERIFY(!src.isActive()); + } }; QTEST_MAIN(DataSourceTest) #include "datasourcetest.moc" diff --git a/src/provider/core/abstractdatasource.cpp b/src/provider/core/abstractdatasource.cpp index 70dba3b..a7835db 100644 --- a/src/provider/core/abstractdatasource.cpp +++ b/src/provider/core/abstractdatasource.cpp @@ -1,90 +1,160 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ +#include + #include "abstractdatasource.h" #include "abstractdatasource_p.h" using namespace KUserFeedback; +static bool defaultActiveState() { return true; } +static QString activeStateKey() { return QStringLiteral("activeState"); } + +static QString commonSettingsGroupName() { return QStringLiteral("dataSourceCommonSettings"); } + AbstractDataSourcePrivate::AbstractDataSourcePrivate() : mode(Provider::DetailedUsageStatistics) + , active(defaultActiveState()) { } AbstractDataSourcePrivate::~AbstractDataSourcePrivate() { } +void AbstractDataSourcePrivate::storeCommonSettings(QSettings *settings) +{ + settings->beginGroup(commonSettingsGroupName()); + + settings->setValue(activeStateKey(), active); + + settings->endGroup(); +} + +void AbstractDataSourcePrivate::loadCommonSettings(QSettings *settings) +{ + settings->beginGroup(commonSettingsGroupName()); + + active = settings->value(activeStateKey(), defaultActiveState()).toBool(); + + settings->endGroup(); +} + +// Please note that this function is supposed to be invoked in between +// data submissions, so be careful to don't reset flags that might be +// changed by a user via UI (e.g., active state) +void AbstractDataSourcePrivate::resetCommonSettings(QSettings *settings) +{ + Q_UNUSED(settings); +} + AbstractDataSource::AbstractDataSource(const QString &id, Provider::TelemetryMode mode, AbstractDataSourcePrivate* dd) : d_ptr(dd ? dd : new AbstractDataSourcePrivate) { d_ptr->id = id; d_ptr->mode = mode; } AbstractDataSource::~AbstractDataSource() { delete d_ptr; } QString AbstractDataSource::id() const { return d_ptr->id; } QString AbstractDataSource::name() const { return {}; } void AbstractDataSource::setId(const QString& id) { d_ptr->id = id; } -void AbstractDataSource::load(QSettings *settings) +void AbstractDataSource::loadImpl(QSettings *settings) { Q_UNUSED(settings); } -void AbstractDataSource::store(QSettings *settings) +void AbstractDataSource::storeImpl(QSettings *settings) { Q_UNUSED(settings); } -void AbstractDataSource::reset(QSettings *settings) +void AbstractDataSource::resetImpl(QSettings *settings) { Q_UNUSED(settings); } +void AbstractDataSource::load(QSettings *settings) +{ + Q_D(AbstractDataSource); + d->loadCommonSettings(settings); + + loadImpl(settings); +} + +void AbstractDataSource::store(QSettings *settings) +{ + Q_D(AbstractDataSource); + d->storeCommonSettings(settings); + + storeImpl(settings); +} + +void AbstractDataSource::reset(QSettings *settings) +{ + Q_D(AbstractDataSource); + d->resetCommonSettings(settings); + + resetImpl(settings); +} + Provider::TelemetryMode AbstractDataSource::telemetryMode() const { Q_D(const AbstractDataSource); Q_ASSERT(d->mode != Provider::NoTelemetry); if (d->mode == Provider::NoTelemetry) return Provider::DetailedUsageStatistics; return d->mode; } void AbstractDataSource::setTelemetryMode(Provider::TelemetryMode mode) { Q_D(AbstractDataSource); Q_ASSERT(mode != Provider::NoTelemetry); d->mode = mode; } + +bool AbstractDataSource::isActive() const +{ + Q_D(const AbstractDataSource); + return d->active; +} + +void AbstractDataSource::setActive(bool active) +{ + Q_D(AbstractDataSource); + d->active = active; +} diff --git a/src/provider/core/abstractdatasource.h b/src/provider/core/abstractdatasource.h index cdf80aa..96a73bc 100644 --- a/src/provider/core/abstractdatasource.h +++ b/src/provider/core/abstractdatasource.h @@ -1,142 +1,177 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KUSERFEEDBACK_ABSTRACTDATASOURCE_H #define KUSERFEEDBACK_ABSTRACTDATASOURCE_H #include "kuserfeedbackcore_export.h" #include "provider.h" #include QT_BEGIN_NAMESPACE class QSettings; QT_END_NAMESPACE namespace KUserFeedback { class AbstractDataSourcePrivate; /*! Base class for data sources for telemetry data. */ class KUSERFEEDBACKCORE_EXPORT AbstractDataSource { public: virtual ~AbstractDataSource(); /*! Returns the ID of this data source. * This is used as identifier towards the server and should * not be shown to the user. * @see description() * @returns The data source identifier configured on the feedback server. */ QString id() const; /*! Returns a short name of this data source. * Can be empty if short name is meaningless for this data source. * @returns A translated, human-readable string. */ virtual QString name() const; /*! Returns a human-readable, translated description of what * this source provides. * @see id() * @returns A translated, human-readable string. */ virtual QString description() const = 0; /*! * Returns the data gathered by this source. * * Implement this to return the data provided by this source. * One of the three following formats are expected: * - scalar entries: QAssiciativeIterable * - list entries: QSequentialIterable containing QAssociativeIterable * - map entries: QAssiciativeIterable containing QAssociativeIterable * * The innermost QAssiciativeIterable must only contain one of the following * base types (which has to match the corresponding schema entry element): * - QString * - int * - double * - bool * All keys must be strings. * * @returns A variant complying with the above requirements. */ virtual QVariant data() = 0; /*! Load persistent state for this data source. * @param settings A QSettings object opened in a dedicated group for loading * persistent data. */ - virtual void load(QSettings *settings); + void load(QSettings *settings); /*! Store persistent state for this data source. * @param settings A QSettings object opened in a dedicated group for storing * persistent data. */ - virtual void store(QSettings *settings); + void store(QSettings *settings); /*! Reset the persistent state of this data source. * This is called after a successful submission of data, and can be used * by sources that track differential rather than absolute data to reset * their counters. * @param settings A QSettings object opened in the dedicated group of this * data source. */ - virtual void reset(QSettings *settings); + void reset(QSettings *settings); /*! Returns which telemetry colleciton mode this data source belongs to. * @return The telemetry collection category this source belongs to. */ Provider::TelemetryMode telemetryMode() const; /*! Sets which telemetry colleciton mode this data source belongs to. * @param mode The data collection mode of this source. * Provider::NoTelemetry is not allowed here. */ void setTelemetryMode(Provider::TelemetryMode mode); + /*! Checks whether this data source is active or not + * If the data source is not active, then collected + * data neither will be sent to a server nor appeared + * in the audit log. + * Data source is active by default. + * @return true if active, false otherwise + */ + bool isActive() const; + + /*! Changes active state of the data source + * @param active The new active state for this data source + */ + void setActive(bool active); + protected: /*! Create a new data source named @p name. * The name of the data source must match the corresponding * product schema entry. * @param name Must not be empty. * @param mode The default telemetry mode. */ explicit AbstractDataSource(const QString &id, Provider::TelemetryMode mode = Provider::DetailedUsageStatistics, AbstractDataSourcePrivate *dd = nullptr); /*! Set the ID of this data source. * The ID should not change at runtime, this is only provided * for enabling QML API for generic sources. * @see id() */ void setId(const QString &id); + /*! Invoked by @p load() in order to load individual settings of this data source. + * @see load() description for further details. + * @param settings A QSettings object opened in a dedicated group for loading + * persistent data. + */ + virtual void loadImpl(QSettings *settings); + + /*! Invoked by @p store() in order to store individual settings of this data source. + * @see store() description for further details. + * @param settings A QSettings object opened in a dedicated group for loading + * persistent data. + */ + virtual void storeImpl(QSettings *settings); + + /*! Invoked by @p reset() in order to reset individual settings of this data source. + * @see reset() description for further details. + * @param settings A QSettings object opened in a dedicated group for loading + * persistent data. + */ + virtual void resetImpl(QSettings *settings); + ///@cond internal class AbstractDataSourcePrivate* const d_ptr; ///@endcond private: Q_DECLARE_PRIVATE(AbstractDataSource) Q_DISABLE_COPY(AbstractDataSource) }; } #endif // KUSERFEEDBACK_ABSTRACTDATASOURCE_H diff --git a/src/provider/core/abstractdatasource_p.h b/src/provider/core/abstractdatasource_p.h index b075916..9dec25c 100644 --- a/src/provider/core/abstractdatasource_p.h +++ b/src/provider/core/abstractdatasource_p.h @@ -1,40 +1,49 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef ABSTRACTDATASOURCEPRIVATE_H #define ABSTRACTDATASOURCEPRIVATE_H #include "provider.h" +QT_BEGIN_NAMESPACE +class QSettings; +QT_END_NAMESPACE + namespace KUserFeedback { class AbstractDataSourcePrivate { public: AbstractDataSourcePrivate(); virtual ~AbstractDataSourcePrivate(); QString id; Provider::TelemetryMode mode; + bool active; + + void storeCommonSettings(QSettings *settings); + void loadCommonSettings(QSettings *settings); + void resetCommonSettings(QSettings *settings); private: Q_DISABLE_COPY(AbstractDataSourcePrivate) }; } #endif // ABSTRACTDATASOURCEPRIVATE_H diff --git a/src/provider/core/propertyratiosource.cpp b/src/provider/core/propertyratiosource.cpp index 6514078..46aec50 100644 --- a/src/provider/core/propertyratiosource.cpp +++ b/src/provider/core/propertyratiosource.cpp @@ -1,249 +1,249 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "propertyratiosource.h" #include "abstractdatasource_p.h" #include "logging_p.h" #include #include #include #include #include #include #include #include #include using namespace KUserFeedback; namespace KUserFeedback { class PropertyRatioSourcePrivate : public AbstractDataSourcePrivate { public: PropertyRatioSourcePrivate(); ~PropertyRatioSourcePrivate(); void propertyChanged(); QString valueToString(const QVariant &value) const; void trySetup(); QString description; QPointer obj; QByteArray propertyName; QObject *signalMonitor; QMetaProperty property; QString previousValue; QTime lastChangeTime; QHash ratioSet; // data we are currently tracking QHash baseRatioSet; // data loaded from storage QMap valueMap; }; // inefficient workaround for not being able to connect QMetaMethod to a function directly class SignalMonitor : public QObject { Q_OBJECT public: explicit SignalMonitor(PropertyRatioSourcePrivate *r) : m_receiver(r) {} public Q_SLOTS: void propertyChanged() { m_receiver->propertyChanged(); } private: PropertyRatioSourcePrivate *m_receiver; }; } PropertyRatioSourcePrivate::PropertyRatioSourcePrivate() : obj(nullptr) , signalMonitor(nullptr) { } PropertyRatioSourcePrivate::~PropertyRatioSourcePrivate() { delete signalMonitor; } void PropertyRatioSourcePrivate::propertyChanged() { if (!previousValue.isEmpty() && lastChangeTime.elapsed() > 1000) { ratioSet[previousValue] += lastChangeTime.elapsed() / 1000; } lastChangeTime.start(); previousValue = valueToString(property.read(obj)); } QString PropertyRatioSourcePrivate::valueToString(const QVariant &value) const { const auto it = valueMap.constFind(value); if (it != valueMap.constEnd() && it.key() == value) { return it.value(); } return value.toString(); } void PropertyRatioSourcePrivate::trySetup() { if (!obj || propertyName.isEmpty()) return; auto idx = obj->metaObject()->indexOfProperty(propertyName.constData()); Q_ASSERT(idx >= 0); if (idx < 0) { qCWarning(Log) << "Property" << propertyName << "not found in" << obj << "!"; return; } property = obj->metaObject()->property(idx); if (!property.hasNotifySignal()) { qCWarning(Log) << "Property" << propertyName << "has no notification signal!"; return; } idx = signalMonitor->metaObject()->indexOfMethod("propertyChanged()"); Q_ASSERT(idx >= 0); const auto propertyChangedMethod = signalMonitor->metaObject()->method(idx); QObject::connect(obj, property.notifySignal(), signalMonitor, propertyChangedMethod); lastChangeTime.start(); propertyChangedMethod.invoke(signalMonitor, Qt::QueuedConnection); } PropertyRatioSource::PropertyRatioSource(QObject *obj, const char *propertyName, const QString &sampleName) : AbstractDataSource(sampleName, Provider::DetailedUsageStatistics, new PropertyRatioSourcePrivate) { Q_D(PropertyRatioSource); d->obj = obj; d->propertyName = propertyName; d->signalMonitor = new SignalMonitor(d); d->trySetup(); } QObject* PropertyRatioSource::object() const { Q_D(const PropertyRatioSource); return d->obj; } void PropertyRatioSource::setObject(QObject* object) { Q_D(PropertyRatioSource); if (d->obj == object) return; d->obj = object; d->trySetup(); } QString PropertyRatioSource::propertyName() const { Q_D(const PropertyRatioSource); return QString::fromUtf8(d->propertyName.constData()); } void PropertyRatioSource::setPropertyName(const QString& name) { Q_D(PropertyRatioSource); const auto n = name.toUtf8(); if (d->propertyName == n) return; d->propertyName = n; d->trySetup(); } void PropertyRatioSource::addValueMapping(const QVariant &value, const QString &str) { Q_D(PropertyRatioSource); d->valueMap.insert(value, str); } QString PropertyRatioSource::description() const { Q_D(const PropertyRatioSource); return d->description; } void PropertyRatioSource::setDescription(const QString& desc) { Q_D(PropertyRatioSource); d->description = desc; } QVariant PropertyRatioSource::data() { Q_D(PropertyRatioSource); d->propertyChanged(); QVariantMap m; int total = 0; for (auto it = d->ratioSet.constBegin(); it != d->ratioSet.constEnd(); ++it) total += it.value() + d->baseRatioSet.value(it.key()); if (total <= 0) return m; for (auto it = d->ratioSet.constBegin(); it != d->ratioSet.constEnd(); ++it) { double currentValue = it.value() + d->baseRatioSet.value(it.key()); QVariantMap v; v.insert(QStringLiteral("property"), currentValue / (double)(total)); m.insert(it.key(), v); } return m; } -void PropertyRatioSource::load(QSettings *settings) +void PropertyRatioSource::loadImpl(QSettings *settings) { Q_D(PropertyRatioSource); foreach (const auto &value, settings->childKeys()) { const auto amount = std::max(settings->value(value, 0).toInt(), 0); d->baseRatioSet.insert(value, amount); if (!d->ratioSet.contains(value)) d->ratioSet.insert(value, 0); } } -void PropertyRatioSource::store(QSettings *settings) +void PropertyRatioSource::storeImpl(QSettings *settings) { Q_D(PropertyRatioSource); d->propertyChanged(); // note that a second process can have updated the data meanwhile! for (auto it = d->ratioSet.begin(); it != d->ratioSet.end(); ++it) { if (it.value() == 0) continue; const auto oldValue = std::max(settings->value(it.key(), 0).toInt(), 0); const auto newValue = oldValue + it.value(); settings->setValue(it.key(), newValue); *it = 0; d->baseRatioSet.insert(it.key(), newValue); } } -void PropertyRatioSource::reset(QSettings* settings) +void PropertyRatioSource::resetImpl(QSettings* settings) { Q_D(PropertyRatioSource); d->baseRatioSet.clear(); d->ratioSet.clear(); settings->remove(QString()); } #include "propertyratiosource.moc" diff --git a/src/provider/core/propertyratiosource.h b/src/provider/core/propertyratiosource.h index 1a4142c..4885bd2 100644 --- a/src/provider/core/propertyratiosource.h +++ b/src/provider/core/propertyratiosource.h @@ -1,87 +1,88 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KUSERFEEDBACK_PROPERTYRATIOSOURCE_H #define KUSERFEEDBACK_PROPERTYRATIOSOURCE_H #include "kuserfeedbackcore_export.h" #include "abstractdatasource.h" namespace KUserFeedback { class PropertyRatioSourcePrivate; /*! Records the time ratio a given QObject property has a specific value. * * An example use-case would be the usage ratio of a applications * views/modes selected by a QTabWidget or QRadioButton. * * The default telemetry mode for this source is Provider::DetailedUsageStatistics. */ class KUSERFEEDBACKCORE_EXPORT PropertyRatioSource : public AbstractDataSource { public: /*! Create a new property ratio data source. * @param obj the QObject of which a property should be monitored. * @param propertyName The name of the property to monitor. * This property must have a change notification signal. The value must have * comparison operators registerd. * @param sampleName This is the name of the database field this data source is * associated with. */ explicit PropertyRatioSource(QObject *obj, const char* propertyName, const QString &sampleName); /*! Returns the monitored object. */ QObject* object() const; /*! Sets the monitoried object. */ void setObject(QObject *object); /*! Returns the property name. */ QString propertyName() const; /*! Sets the property name that should be monitored. */ void setPropertyName(const QString &name); /*! Map property value @p value to @p str for sending to the server. * This is useful to map internal identifiers to portable and persistable values, * such as turning pointers or indexes into meaningful descriptions. * @param value The property value to map. * @param str The string the property value @p value should be mapped to. */ void addValueMapping(const QVariant &value, const QString &str); QString description() const override; /*! Set human-readable and translated description of the data provided by this source. * @note This must be set before adding this source, sources without description are * discarded. * @param desc The description. */ void setDescription(const QString &desc); QVariant data() override; - void load(QSettings *settings) override; - void store(QSettings *settings) override; - void reset(QSettings *settings) override; + + void loadImpl(QSettings *settings) override; + void storeImpl(QSettings *settings) override; + void resetImpl(QSettings *settings) override; using AbstractDataSource::setId; private: Q_DECLARE_PRIVATE(PropertyRatioSource) }; } #endif // KUSERFEEDBACK_PROPERTYRATIOSOURCE_H diff --git a/src/provider/core/provider.cpp b/src/provider/core/provider.cpp index bf90d57..fa2b871 100644 --- a/src/provider/core/provider.cpp +++ b/src/provider/core/provider.cpp @@ -1,712 +1,712 @@ /* Copyright (C) 2016 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #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) { 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)) + 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() { submissionTimer.stop(); if (!q->isEnabled()) return; if (submissionInterval <= 0 || (telemetryMode == Provider::NoTelemetry && surveyInterval < 0)) return; Q_ASSERT(submissionInterval > 0); const auto nextSubmission = lastSubmitTime.addDays(submissionInterval); const auto now = QDateTime::currentDateTime(); submissionTimer.start(std::max(0ll, now.msecsTo(nextSubmission))); } void ProviderPrivate::submitFinished(QNetworkReply *reply) { reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { qCWarning(Log) << "failed to submit user feedback:" << 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; } 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()) 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::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; } 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) || telemetryMode < source->telemetryMode()) + 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/selectionratiosource.cpp b/src/provider/core/selectionratiosource.cpp index 01cc45e..bbc1da5 100644 --- a/src/provider/core/selectionratiosource.cpp +++ b/src/provider/core/selectionratiosource.cpp @@ -1,175 +1,175 @@ /* Copyright (C) 2017 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "selectionratiosource.h" #include "abstractdatasource_p.h" #include "logging_p.h" #include #include #include #include #include #include #include using namespace KUserFeedback; namespace KUserFeedback { class SelectionRatioSourcePrivate : public AbstractDataSourcePrivate { public: SelectionRatioSourcePrivate(); ~SelectionRatioSourcePrivate(); void selectionChanged(); QString selectedValue() const; QItemSelectionModel *model; QMetaObject::Connection monitorConnection; QString description; QString previousValue; QTime lastChangeTime; QHash ratioSet; // data we are currently tracking QHash baseRatioSet; // data loaded from storage int role; }; } SelectionRatioSourcePrivate::SelectionRatioSourcePrivate() : model(nullptr) , role(Qt::DisplayRole) { } SelectionRatioSourcePrivate::~SelectionRatioSourcePrivate() { QObject::disconnect(monitorConnection); } void SelectionRatioSourcePrivate::selectionChanged() { if (!previousValue.isEmpty() && lastChangeTime.elapsed() > 1000) { ratioSet[previousValue] += lastChangeTime.elapsed() / 1000; } lastChangeTime.start(); previousValue = selectedValue(); } QString SelectionRatioSourcePrivate::selectedValue() const { const auto idxs = model->selectedIndexes(); if (!model->hasSelection() || idxs.isEmpty()) return QString(); Q_ASSERT(!idxs.isEmpty()); const auto idx = idxs.at(0); return idx.data(role).toString(); } SelectionRatioSource::SelectionRatioSource(QItemSelectionModel* selectionModel, const QString& sampleName) : AbstractDataSource(sampleName, Provider::DetailedUsageStatistics, new SelectionRatioSourcePrivate) { Q_D(SelectionRatioSource); d->model = selectionModel; Q_ASSERT(selectionModel); d->monitorConnection = QObject::connect(selectionModel, &QItemSelectionModel::selectionChanged, [this]() { Q_D(SelectionRatioSource); d->selectionChanged(); }); d->lastChangeTime.start(); d->selectionChanged(); } void SelectionRatioSource::setRole(int role) { Q_D(SelectionRatioSource); d->role = role; } QString SelectionRatioSource::description() const { Q_D(const SelectionRatioSource); return d->description; } void SelectionRatioSource::setDescription(const QString& desc) { Q_D(SelectionRatioSource); d->description = desc; } QVariant SelectionRatioSource::data() { Q_D(SelectionRatioSource); d->selectionChanged(); QVariantMap m; int total = 0; for (auto it = d->ratioSet.constBegin(); it != d->ratioSet.constEnd(); ++it) total += it.value() + d->baseRatioSet.value(it.key()); if (total <= 0) return m; for (auto it = d->ratioSet.constBegin(); it != d->ratioSet.constEnd(); ++it) { double currentValue = it.value() + d->baseRatioSet.value(it.key()); QVariantMap v; v.insert(QStringLiteral("property"), currentValue / (double)(total)); m.insert(it.key(), v); } return m; } -void SelectionRatioSource::load(QSettings *settings) +void SelectionRatioSource::loadImpl(QSettings *settings) { Q_D(SelectionRatioSource); foreach (const auto &value, settings->childKeys()) { const auto amount = std::max(settings->value(value, 0).toInt(), 0); d->baseRatioSet.insert(value, amount); if (!d->ratioSet.contains(value)) d->ratioSet.insert(value, 0); } } -void SelectionRatioSource::store(QSettings *settings) +void SelectionRatioSource::storeImpl(QSettings *settings) { Q_D(SelectionRatioSource); d->selectionChanged(); // note that a second process can have updated the data meanwhile! for (auto it = d->ratioSet.begin(); it != d->ratioSet.end(); ++it) { if (it.value() == 0) continue; const auto oldValue = std::max(settings->value(it.key(), 0).toInt(), 0); const auto newValue = oldValue + it.value(); settings->setValue(it.key(), newValue); *it = 0; d->baseRatioSet.insert(it.key(), newValue); } } -void SelectionRatioSource::reset(QSettings* settings) +void SelectionRatioSource::resetImpl(QSettings* settings) { Q_D(SelectionRatioSource); d->baseRatioSet.clear(); d->ratioSet.clear(); settings->remove(QString()); } diff --git a/src/provider/core/selectionratiosource.h b/src/provider/core/selectionratiosource.h index ff36aa2..de75249 100644 --- a/src/provider/core/selectionratiosource.h +++ b/src/provider/core/selectionratiosource.h @@ -1,74 +1,74 @@ /* Copyright (C) 2017 Volker Krause This program is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef KUSERFEEDBACK_SELECTIONRATIOSOURCE_H #define KUSERFEEDBACK_SELECTIONRATIOSOURCE_H #include "kuserfeedbackcore_export.h" #include "abstractdatasource.h" QT_BEGIN_NAMESPACE class QItemSelectionModel; QT_END_NAMESPACE namespace KUserFeedback { class SelectionRatioSourcePrivate; /*! Records the time ratio a given entry is selected via a QItemSelectionModel. * * An example use-case would be the usage ratio of a applications * views/modes selected using a model-based view sidebar (such as * used in e.g. Kontact). * * The default telemetry mode for this source is Provider::DetailedUsageStatistics. */ class KUSERFEEDBACKCORE_EXPORT SelectionRatioSource : public AbstractDataSource { public: /*! Create a new selection ratio data source. * @param selectionModel The selection to monitor. * @param sampleName This is the name of the database field this data source is * associated with. */ explicit SelectionRatioSource(QItemSelectionModel *selectionModel, const QString &sampleName); /*! Determine which role to consider for the reported value. * By default this is Qt::DisplayRole. */ void setRole(int role); QString description() const override; /*! Set human-readable and translated description of the data provided by this source. * @note This must be set before adding this source, sources without description are * discarded. * @param desc The description. */ void setDescription(const QString &desc); QVariant data() override; - void load(QSettings *settings) override; - void store(QSettings *settings) override; - void reset(QSettings *settings) override; + void loadImpl(QSettings *settings) override; + void storeImpl(QSettings *settings) override; + void resetImpl(QSettings *settings) override; private: Q_DECLARE_PRIVATE(SelectionRatioSource) }; } #endif // KUSERFEEDBACK_SELECTIONRATIOSOURCE_H