Commit e999f481 authored by Igor Poboiko's avatar Igor Poboiko
Browse files

[calendar] Implement SyncToken for Calendar service

Summary:
This is an implementation of a `syncToken` Google API feature (see {T8376}, and
https://developers.google.com/calendar/v3/reference/events/list) which is a
native way to perform incremental updates.

When `EventFetchJob` is created, we can provide it with a token by calling
`job->setSyncToken(token)`, which will be used as a parameter for fetching.
Note: some of the parameters are incompatible with `syncToken` (`timeMin`,
`timeMax`, `updatedMin`, see API link above), so if token is provided, those
parameters get ignored.

When the job is finished, the next sync token can be obtained via
`job->syncToken()`. This token then can be used for the next `EventFetchJob`.

Test Plan:
1. Create an `EventFetchJob` with no `syncToken`
2. When job is finished, fetch its `syncToken`
3. Create a new job, using previous `syncToken`
4. Only incremental changes are obtained

Reviewers: dvratil

Subscribers: kde-pim

Tags: #kde_pim

Differential Revision: https://phabricator.kde.org/D28162
parent 7f7d461d
......@@ -207,6 +207,7 @@ static const auto kindParam = QStringLiteral("kind");
static const auto idParam = QStringLiteral("id");
static const auto etagParam = QStringLiteral("etag");
static const auto nextSyncTokenParam = QStringLiteral("nextSyncToken");
static const auto nextPageTokenParam = QStringLiteral("nextPageToken");
static const auto pageTokenParam = QStringLiteral("pageToken");
static const auto itemsParam = QStringLiteral("items");
......@@ -807,6 +808,9 @@ ObjectsList parseEventJSONFeed(const QByteArray& jsonFeed, FeedData& feedData)
// This should always be in Olson format
timezone = data.value(timeZoneParam).toString();
}
if (data.contains(nextSyncTokenParam)) {
feedData.syncToken = data[nextSyncTokenParam].toString();
}
} else {
return {};
}
......
......@@ -39,6 +39,7 @@ class Q_DECL_HIDDEN EventFetchJob::Private
QString calendarId;
QString eventId;
QString filter;
QString syncToken;
bool fetchDeleted = true;
quint64 updatedTimestamp = 0;
quint64 timeMin = 0;
......@@ -107,6 +108,16 @@ quint64 EventFetchJob::timeMax() const
return d->timeMax;
}
void EventFetchJob::setSyncToken(const QString& syncToken)
{
d->syncToken = syncToken;
}
QString EventFetchJob::syncToken()
{
return d->syncToken;
}
void EventFetchJob::setTimeMin(quint64 timestamp)
{
if (isRunning()) {
......@@ -147,14 +158,18 @@ void EventFetchJob::start()
if (!d->filter.isEmpty()) {
query.addQueryItem(QStringLiteral("q"), d->filter);
}
if (d->updatedTimestamp > 0) {
query.addQueryItem(QStringLiteral("updatedMin"), Utils::ts2Str(d->updatedTimestamp));
}
if (d->timeMin > 0) {
query.addQueryItem(QStringLiteral("timeMin"), Utils::ts2Str(d->timeMin));
}
if (d->timeMax > 0) {
query.addQueryItem(QStringLiteral("timeMax"), Utils::ts2Str(d->timeMax));
if (d->syncToken.isEmpty()) {
if (d->updatedTimestamp > 0) {
query.addQueryItem(QStringLiteral("updatedMin"), Utils::ts2Str(d->updatedTimestamp));
}
if (d->timeMin > 0) {
query.addQueryItem(QStringLiteral("timeMin"), Utils::ts2Str(d->timeMin));
}
if (d->timeMax > 0) {
query.addQueryItem(QStringLiteral("timeMax"), Utils::ts2Str(d->timeMax));
}
} else {
query.addQueryItem(QStringLiteral("syncToken"), d->syncToken);
}
url.setQuery(query);
} else {
......@@ -168,8 +183,9 @@ ObjectsList EventFetchJob::handleReplyWithItems(const QNetworkReply *reply, cons
{
if (reply->error() == QNetworkReply::ContentGoneError
|| reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == Gone) {
// Full sync required by server, redo request with no updatedMin
// Full sync required by server, redo request with no updatedMin and no syncToken
d->updatedTimestamp = 0;
d->syncToken.clear();
start();
// Errors are not cleared on success
// Do it here or else the job will fail
......@@ -189,6 +205,7 @@ ObjectsList EventFetchJob::handleReplyWithItems(const QNetworkReply *reply, cons
} else {
items << CalendarService::JSONToEvent(rawData).dynamicCast<Object>();
}
d->syncToken = feedData.syncToken;
} else {
setError(KGAPI2::InvalidResponse);
setErrorString(tr("Invalid response content type"));
......
......@@ -114,6 +114,16 @@ class KGAPICALENDAR_EXPORT EventFetchJob : public KGAPI2::FetchJob
*/
Q_PROPERTY(QString filter READ filter WRITE setFilter)
/**
* @brief A token to fetch updates incrementally
*
* By default the property is empty. Properties timeMin, timeMax,
* updatedMin will be ignored if sync token is specified
*
* @see setSyncToken, syncToken
*/
Q_PROPERTY(QString syncToken READ syncToken WRITE setSyncToken)
public:
/**
......@@ -214,6 +224,18 @@ class KGAPICALENDAR_EXPORT EventFetchJob : public KGAPI2::FetchJob
*/
quint64 timeMin() const;
/**
* @brief Sets token for incremental updates
*
* @param syncToken
*/
void setSyncToken(const QString& syncToken);
/**
* @brief Token for next incremental update
*/
QString syncToken();
protected:
/**
......
......@@ -53,6 +53,8 @@ class KGAPICORE_EXPORT FeedData {
QUrl requestUrl; /**< Original URL of the request. This value is filled
by AccessManager when passing the structure to a
service */
QString syncToken; /**< Sync token that can be used for incremental
updates by some of the services.*/
};
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment