provider.cpp 11.5 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
    Copyright (C) 2016 Volker Krause <vkrause@kde.org>

    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 <http://www.gnu.org/licenses/>.
*/

18
#include <config-userfeedback-version.h>
19

20
#include "provider.h"
21
22
#include "provider_p.h"
#include "abstractdatasource.h"
23
#include "startcountsource.h"
24
#include "surveyinfo.h"
25
#include "usagetimesource.h"
26
27
28

#include <QCoreApplication>
#include <QDebug>
Volker Krause's avatar
Volker Krause committed
29
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
Volker Krause's avatar
Volker Krause committed
30
#include <QJsonArray>
31
32
#include <QJsonDocument>
#include <QJsonObject>
Volker Krause's avatar
Volker Krause committed
33
#endif
34
#include <QMetaEnum>
35
36
37
38
39
40
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSettings>
#include <QUrl>

41
#include <algorithm>
42
43
44
45
46
47
48
#include <numeric>

using namespace UserFeedback;

ProviderPrivate::ProviderPrivate(Provider *qq)
    : q(qq)
    , networkAccessManager(Q_NULLPTR)
Volker Krause's avatar
Volker Krause committed
49
    , submissionInterval(-1)
50
    , statisticsMode(Provider::NoStatistics)
51
    , surveyInterval(-1)
52
53
    , startCount(0)
    , usageTime(0)
54
55
56
57
    , encouragementStarts(-1)
    , encouragementTime(-1)
    , encouragementDelay(300)
    , encouragementDisplayed(false)
58
{
59
60
    auto domain = QCoreApplication::organizationDomain().split(QLatin1Char('.'));
    std::reverse(domain.begin(), domain.end());
Volker Krause's avatar
Volker Krause committed
61
    productId = domain.join(QLatin1String(".")) + QLatin1Char('.') + QCoreApplication::applicationName();
62

63
64
65
    submissionTimer.setSingleShot(true);
    QObject::connect(&submissionTimer, SIGNAL(timeout()), q, SLOT(submit()));

66
    startTime.start();
67
68
69

    encouragementTimer.setSingleShot(true);
    QObject::connect(&encouragementTimer, SIGNAL(timeout()), q, SLOT(emitShowEncouragementMessage()));
70
71
}

Volker Krause's avatar
Volker Krause committed
72
73
74
75
76
ProviderPrivate::~ProviderPrivate()
{
    qDeleteAll(dataSources);
}

77
78
79
80
81
82
83
84
85
86
87
88
void ProviderPrivate::reset()
{
    startCount = 0;
    usageTime = 0;
    startTime.start();
}

int ProviderPrivate::currentApplicationTime() const
{
    return usageTime + (startTime.elapsed() / 1000);
}

89
90
91
92
93
94
95
static QMetaEnum statisticsCollectionModeEnum()
{
    const auto idx = Provider::staticMetaObject.indexOfEnumerator("StatisticsCollectionMode");
    Q_ASSERT(idx >= 0);
    return Provider::staticMetaObject.enumerator(idx);
}

96
97
98
99
100
void ProviderPrivate::load()
{
    QSettings settings;
    settings.beginGroup(QStringLiteral("UserFeedback"));
    lastSubmitTime = settings.value(QStringLiteral("LastSubmission")).toDateTime();
101
102
103

    const auto modeStr = settings.value(QStringLiteral("StatisticsCollectionMode")).toByteArray();
    statisticsMode = static_cast<Provider::StatisticsCollectionMode>(std::max(statisticsCollectionModeEnum().keyToValue(modeStr), 0));
104

105
106
    surveyInterval = settings.value(QStringLiteral("SurveyInterval"), -1).toInt();
    lastSurveyTime = settings.value(QStringLiteral("LastSurvey")).toDateTime();
107
    completedSurveys = settings.value(QStringLiteral("CompletedSurveys"), QStringList()).toStringList();
108
109
110

    startCount = std::max(settings.value(QStringLiteral("ApplicationStartCount"), 0).toInt() + 1, 1);
    usageTime = std::max(settings.value(QStringLiteral("ApplicationTime"), 0).toInt(), 0);
111
112

    encouragementDisplayed = settings.value(QStringLiteral("EncouragementDisplayed"), false).toBool();
113
114
115
116
117
118
119
}

void ProviderPrivate::store()
{
    QSettings settings;
    settings.beginGroup(QStringLiteral("UserFeedback"));
    settings.setValue(QStringLiteral("LastSubmission"), lastSubmitTime);
120
    settings.setValue(QStringLiteral("StatisticsCollectionMode"), QString::fromLatin1(statisticsCollectionModeEnum().valueToKey(statisticsMode)));
121

122
123
    settings.setValue(QStringLiteral("SurveyInterval"), surveyInterval);
    settings.setValue(QStringLiteral("LastSurvey"), lastSurveyTime);
124
    settings.setValue(QStringLiteral("CompletedSurveys"), completedSurveys);
125
126
127

    settings.setValue(QStringLiteral("ApplicationStartCount"), startCount);
    settings.setValue(QStringLiteral("ApplicationTime"), currentApplicationTime());
128
129

    settings.setValue(QStringLiteral("EncouragementDisplayed"), encouragementDisplayed);
130
131
132

    foreach (auto source, dataSources)
        source->store(&settings);
133
134
135
136
137
138
139
140
141
142
}

void ProviderPrivate::aboutToQuit()
{
    qDebug() << Q_FUNC_INFO;
    store();
}

QByteArray ProviderPrivate::jsonData() const
{
Volker Krause's avatar
Volker Krause committed
143
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
144
    QJsonObject obj;
145
    if (statisticsMode != Provider::NoStatistics) {
146
        foreach (auto source, dataSources) {
Volker Krause's avatar
Volker Krause committed
147
148
149
150
151
152
153
            if (statisticsMode < source->collectionMode())
                continue;
            const auto data = source->data();
            if (data.canConvert<QVariantMap>())
                obj.insert(source->name(), QJsonObject::fromVariantMap(data.toMap()));
            else if (data.canConvert<QVariantList>())
                obj.insert(source->name(), QJsonArray::fromVariantList(data.value<QVariantList>()));
154
        }
155
    }
156
157
158

    QJsonDocument doc(obj);
    return doc.toJson();
Volker Krause's avatar
Volker Krause committed
159
160
161
162
#else
    qCritical("NOT IMPLEMENTED YET");
    return QByteArray();
#endif
163
164
}

165
166
167
168
169
170
171
172
173
174
175
176
177
void ProviderPrivate::scheduleNextSubmission()
{
    submissionTimer.stop();
    if (submissionInterval <= 0 || (statisticsMode == Provider::NoStatistics && 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)));
}

178
179
180
181
182
void ProviderPrivate::submitFinished()
{
    auto reply = qobject_cast<QNetworkReply*>(q->sender());
    Q_ASSERT(reply);

183
    if (reply->error() != QNetworkReply::NoError) {
Volker Krause's avatar
Volker Krause committed
184
        qWarning() << "failed to submit user feedback:" << reply->errorString() << reply->readAll();
185
        return;
186
    }
187
188

    lastSubmitTime = QDateTime::currentDateTime();
189

Volker Krause's avatar
Volker Krause committed
190
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
191
192
193
    const auto obj = QJsonDocument::fromJson(reply->readAll()).object();
    if (obj.contains(QStringLiteral("survey"))) {
        const auto surveyObj = obj.value(QStringLiteral("survey")).toObject();
194
        const auto survey = SurveyInfo::fromJson(surveyObj);
195
        selectSurvey(survey);
196
    }
Volker Krause's avatar
Volker Krause committed
197
#endif
Volker Krause's avatar
Volker Krause committed
198

199
    scheduleNextSubmission();
200
201
}

202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
void ProviderPrivate::selectSurvey(const SurveyInfo &survey) const
{
    qDebug() << Q_FUNC_INFO << "got survey:" << survey.url();
    if (surveyInterval < 0) // surveys disabled
        return;

    if (!survey.isValid() || completedSurveys.contains(QString::number(survey.id())))
        return;

    if (lastSurveyTime.addDays(surveyInterval) > QDateTime::currentDateTime())
        return;

    emit q->surveyAvailable(survey);
}

217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
void ProviderPrivate::scheduleEncouragement()
{
    encouragementTimer.stop();
    if (encouragementStarts < 0 && encouragementTime < 0) // encouragement disabled
        return;

    if (encouragementStarts > startCount) // we need more starts
        return;

    if (statisticsMode == Provider::AllStatistics && surveyInterval == 0) // already everything enabled
        return;

    Q_ASSERT(encouragementDelay >= 0);
    int timeToEncouragement = encouragementDelay;
    if (encouragementTime > 0)
        timeToEncouragement = std::max(timeToEncouragement, (encouragementTime * 60) - currentApplicationTime());
    encouragementTimer.start(timeToEncouragement);
}

void ProviderPrivate::emitShowEncouragementMessage()
{
    encouragementDisplayed = true; // TODO make this explicit, in case the host application decides to delay?
    emit q->showEncouragementMessage();
}

242
243
244
245
246
247
248
249
250
251
252
253

Provider::Provider(QObject *parent) :
    QObject(parent),
    d(new ProviderPrivate(this))
{
    qDebug() << Q_FUNC_INFO;

    connect(QCoreApplication::instance(), SIGNAL(aboutToQuit()), this, SLOT(aboutToQuit()));

    d->load();
}

254
255
256
257
258
Provider::~Provider()
{
    delete d;
}

259
260
261
262
263
264
265
266
267
268
void Provider::setProductIdentifier(const QString &productId)
{
    d->productId = productId;
}

void Provider::setFeedbackServer(const QUrl &url)
{
    d->serverUrl = url;
}

Volker Krause's avatar
Volker Krause committed
269
270
271
void Provider::setSubmissionInterval(int days)
{
    d->submissionInterval = days;
272
    d->scheduleNextSubmission();
Volker Krause's avatar
Volker Krause committed
273
274
}

275
276
277
278
279
280
281
Provider::StatisticsCollectionMode Provider::statisticsCollectionMode() const
{
    return d->statisticsMode;
}

void Provider::setStatisticsCollectionMode(StatisticsCollectionMode mode)
{
282
283
284
    if (d->statisticsMode == mode)
        return;

285
    d->statisticsMode = mode;
286
    d->scheduleNextSubmission();
287
    d->scheduleEncouragement();
Volker Krause's avatar
Volker Krause committed
288
    d->store();
289
    emit statisticsCollectionModeChanged();
290
291
}

292
293
void Provider::addDataSource(AbstractDataSource *source, StatisticsCollectionMode mode)
{
294
295
296
297
298
299
300
301
302
303
304
305
    // sanity-check sources
    if (mode == NoStatistics) {
        qCritical() << "Source" << source->name() << "attempts to report data unconditionally, ignoring!";
        delete source;
        return;
    }
    if (source->description().isEmpty()) {
        qCritical() << "Source" << source->name() << "has no description, ignoring!";
        delete source;
        return;
    }

306
    Q_ASSERT(mode != NoStatistics);
307
    Q_ASSERT(!source->description().isEmpty());
308
    source->setCollectionMode(mode);
309
310
311
312

    // 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<StartCountSource*>(source))
        countSrc->setProvider(d);
313
314
    if (auto timeSrc = dynamic_cast<UsageTimeSource*>(source))
        timeSrc->setProvider(d);
315

316
    d->dataSources.push_back(source);
317
318
319
320

    QSettings settings;
    settings.beginGroup(QStringLiteral("UserFeedback"));
    source->load(&settings);
321
322
}

Volker Krause's avatar
Volker Krause committed
323
324
325
326
327
QVector<AbstractDataSource*> Provider::dataSources() const
{
    return d->dataSources;
}

328
329
330
331
332
333
334
int Provider::surveyInterval() const
{
    return d->surveyInterval;
}

void Provider::setSurveyInterval(int days)
{
335
336
337
    if (d->surveyInterval == days)
        return;

338
    d->surveyInterval = days;
339
    d->scheduleNextSubmission();
340
    d->scheduleEncouragement();
Volker Krause's avatar
Volker Krause committed
341
    d->store();
342
    emit surveyIntervalChanged();
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
}

void Provider::setApplicationStartsUntilEncouragement(int starts)
{
    d->encouragementStarts = starts;
    d->scheduleEncouragement();
}

void Provider::setApplicationUsageTimeUntilEncouragement(int minutes)
{
    d->encouragementTime = minutes;
    d->scheduleEncouragement();
}

void Provider::setEncouragementDelay(int secs)
{
    d->encouragementDelay = std::max(0, secs);
    d->scheduleEncouragement();
361
362
}

363
364
365
void Provider::setSurveyCompleted(const SurveyInfo &info)
{
    d->completedSurveys.push_back(QString::number(info.id()));
366
    d->lastSurveyTime = QDateTime::currentDateTime();
367
368
369
    d->store();
}

370
371
void Provider::submit()
{
372
373
374
375
376
    if (!d->serverUrl.isValid()) {
        qWarning() << "No feedback server URL specified!";
        return;
    }

377
378
379
380
    if (!d->networkAccessManager)
        d->networkAccessManager = new QNetworkAccessManager(this);

    auto url = d->serverUrl;
Volker Krause's avatar
Volker Krause committed
381
    url.setPath(url.path() + QStringLiteral("/receiver/submit/") + d->productId);
382
383
    QNetworkRequest request(url);
    request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
Volker Krause's avatar
Volker Krause committed
384
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
385
    request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("UserFeedback/") + QStringLiteral(USERFEEDBACK_VERSION));
Volker Krause's avatar
Volker Krause committed
386
#endif
387
388
389
390
391
    auto reply = d->networkAccessManager->post(request, d->jsonData());
    connect(reply, SIGNAL(finished()), this, SLOT(submitFinished()));
}

#include "moc_provider.cpp"