googlecalendarsyncadaptor.cpp 124 KB
Newer Older
1 2
/****************************************************************************
 **
3
 ** Copyright (C) 2013-2014 Jolla Ltd.
4 5
 ** Contact: Chris Adams <chris.adams@jollamobile.com>
 **
6 7 8 9 10 11 12 13 14 15 16 17 18 19
 ** This program/library is free software; you can redistribute it and/or
 ** modify it under the terms of the GNU Lesser General Public License
 ** version 2.1 as published by the Free Software Foundation.
 **
 ** This program/library 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
 ** Lesser General Public License for more details.
 **
 ** You should have received a copy of the GNU Lesser General Public
 ** License along with this program/library; if not, write to the Free
 ** Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 ** 02110-1301 USA
 **
20 21 22
 ****************************************************************************/

#include "googlecalendarsyncadaptor.h"
23
#include "googlecalendarincidencecomparator.h"
24 25 26 27 28
#include "trace.h"

#include <QtCore/QUrlQuery>
#include <QtCore/QFile>
#include <QtCore/QDir>
29
#include <QtCore/QByteArray>
30 31
#include <QtCore/QJsonArray>
#include <QtCore/QJsonObject>
32 33
#include <QtCore/QJsonDocument>
#include <QtCore/QSettings>
34 35 36 37

#include <QtSql/QSqlQuery>
#include <QtSql/QSqlError>

38
#include <ksystemtimezone.h>
39
#include <kdatetime.h>
40

41
//----------------------------------------------
42

43 44
#define RFC3339_FORMAT      "%Y-%m-%dT%H:%M:%S%:z"
#define RFC3339_FORMAT_NTZC "%Y-%m-%dT%H:%M:%S%z"
45 46
#define RFC3339_QDATE_FORMAT_MS "yyyy-MM-ddThh:mm:ss.zzzZ"
#define RFC3339_QDATE_FORMAT_MS_NTZC "yyyy-MM-ddThh:mm:ss.zzz"
47 48 49
#define KDATEONLY_FORMAT    "%Y-%m-%d"
#define QDATEONLY_FORMAT    "yyyy-MM-dd"
#define KLONGTZ_FORMAT      "%:Z"
50 51 52
#define RFC5545_KDATETIME_FORMAT "%Y%m%dT%H%M%SZ"
#define RFC5545_KDATETIME_FORMAT_NTZC "%Y%m%dT%H%M%S"
#define RFC5545_QDATE_FORMAT "yyyyMMdd"
53 54 55

namespace {

56 57
static int GOOGLE_CAL_SYNC_PLUGIN_VERSION = 2;

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
void errorDumpStr(const QString &str)
{
    // Dump the entire string to the log.
    // Note that the log cannot handle newlines,
    // so we separate the string into chunks.
    Q_FOREACH (const QString &chunk, str.split('\n', QString::SkipEmptyParts)) {
        SOCIALD_LOG_ERROR(chunk);
    }
}

void traceDumpStr(const QString &str)
{
    // 8 is the minimum log level for TRACE logs
    // as defined in Buteo's LogMacros.h
    if (Buteo::Logger::instance()->getLogLevel() < 8) {
        return;
    }

    // Dump the entire string to the log.
    // Note that the log cannot handle newlines,
    // so we separate the string into chunks.
    Q_FOREACH (const QString &chunk, str.split('\n', QString::SkipEmptyParts)) {
        SOCIALD_LOG_TRACE(chunk);
    }
}

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102
// returns true if the ghost-event cleanup sync has been performed.
bool ghostEventCleanupPerformed()
{
    QString settingsFileName = QString::fromLatin1("%1/%2/gcal.ini")
            .arg(QString::fromLatin1(PRIVILEGED_DATA_DIR))
            .arg(QString::fromLatin1(SYNC_DATABASE_DIR));
    QSettings settingsFile(settingsFileName, QSettings::IniFormat);
    return settingsFile.value(QString::fromLatin1("cleaned"), QVariant::fromValue<bool>(false)).toBool();
}
void setGhostEventCleanupPerformed()
{
    QString settingsFileName = QString::fromLatin1("%1/%2/gcal.ini")
            .arg(QString::fromLatin1(PRIVILEGED_DATA_DIR))
            .arg(QString::fromLatin1(SYNC_DATABASE_DIR));
    QSettings settingsFile(settingsFileName, QSettings::IniFormat);
    settingsFile.setValue(QString::fromLatin1("cleaned"), QVariant::fromValue<bool>(true));
    settingsFile.sync();
}

103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
void uniteIncidenceLists(const KCalCore::Incidence::List &first, KCalCore::Incidence::List *second)
{
    int originalSecondSize = second->size();
    bool foundMatch = false;
    Q_FOREACH (KCalCore::Incidence::Ptr inc, first) {
        foundMatch = false;
        for (int i = 0; i < originalSecondSize; ++i) {
            if (inc->uid() == second->at(i)->uid() && inc->recurrenceId() == second->at(i)->recurrenceId()) {
                // found a match
                foundMatch = true;
                break;
            }
        }
        if (!foundMatch) {
            second->append(inc);
        }
    }
}

122 123
QString gCalEventId(KCalCore::Incidence::Ptr event)
{
124 125 126 127 128 129 130 131 132
    // we abuse the comments field to store our gcal-id.
    // we should use a custom property, but those are deleted on incidence deletion.
    const QStringList &comments(event->comments());
    Q_FOREACH (const QString &comment, comments) {
        if (comment.startsWith("jolla-sociald:gcal-id:")) {
            return comment.mid(22);
        }
    }
    return QString();
133
}
134

135 136
void setGCalEventId(KCalCore::Incidence::Ptr event, const QString &id)
{
137 138 139 140 141 142 143 144 145 146 147 148 149
    // we abuse the comments field to store our gcal-id.
    // we should use a custom property, but those are deleted on incidence deletion.
    const QStringList &comments(event->comments());
    Q_FOREACH (const QString &comment, comments) {
        if (comment.startsWith("jolla-sociald:gcal-id:")) {
            // remove any existing gcal-id comment.
            event->removeComment(comment);
            break;
        }
    }
    event->addComment(QStringLiteral("jolla-sociald:gcal-id:%1").arg(id));
}

150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
void setRemoteUidCustomField(KCalCore::Incidence::Ptr event, const QString &uid, const QString &id)
{
    // store it also in a custom property purely for invitation lookup purposes.
    if (!uid.isEmpty()) {
        event->setNonKDECustomProperty("X-SAILFISHOS-REMOTE-UID", uid.toUtf8());
    } else {
        // Google Calendar invites are sent as invitations with uid suffixed with @google.com.
        if (id.endsWith(QLatin1String("@google.com"), Qt::CaseInsensitive)) {
            event->setNonKDECustomProperty("X-SAILFISHOS-REMOTE-UID", id.toUtf8());
        } else {
            QString suffixedId = id;
            suffixedId.append(QLatin1String("@google.com"));
            event->setNonKDECustomProperty("X-SAILFISHOS-REMOTE-UID", suffixedId.toUtf8());
        }
    }
}

167 168 169 170
QString gCalETag(KCalCore::Incidence::Ptr event)
{
    return event->customProperty("jolla-sociald", "gcal-etag");
}
171

172 173 174 175 176 177
void setGCalETag(KCalCore::Incidence::Ptr event, const QString &etag)
{
    // note: custom properties are purged on incidence deletion.
    event->setCustomProperty("jolla-sociald", "gcal-etag", etag);
}

178 179 180 181 182 183 184 185
KDateTime datetimeFromUpdateStr(const QString &update)
{
    // generally, this is an RFC3339 date with timezone information, like:
    // 2015-04-25T12:02:40.988Z
    // However, our version of KDateTime is old enough that we don't support this
    // date format directly.
    bool tzIncluded = update.endsWith('Z');
    QDateTime qdt = tzIncluded
186 187
                  ? QLocale::c().toDateTime(update, RFC3339_QDATE_FORMAT_MS)
                  : QLocale::c().toDateTime(update, RFC3339_QDATE_FORMAT_MS_NTZC);
188 189 190 191
    if (tzIncluded) {
        qdt.setTimeSpec(Qt::UTC);
    }
    return KDateTime(qdt, tzIncluded ? KDateTime::UTC : KDateTime::ClockTime);
192 193
}

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238
QList<KDateTime> datetimesFromExRDateStr(const QString &exrdatestr, bool *isDateOnly)
{
    // possible forms:
    // RDATE:19970714T123000Z
    // RDATE;VALUE=DATE-TIME:19970714T123000Z
    // RDATE;VALUE=DATE-TIME:19970714T123000Z,19970715T123000Z
    // RDATE;TZID=America/New_York:19970714T083000
    // RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H
    // RDATE;VALUE=DATE:19970101,19970120

    QList<KDateTime> retn;
    QString str = exrdatestr;
    *isDateOnly = false; // by default.

    if (str.startsWith(QStringLiteral("exdate"), Qt::CaseInsensitive)) {
        str.remove(0, 6);
    } else if (str.startsWith(QStringLiteral("rdate"), Qt::CaseInsensitive)) {
        str.remove(0, 5);
    } else {
        SOCIALD_LOG_ERROR("not an ex/rdate string:" << exrdatestr);
        return retn;
    }

    if (str.startsWith(';')) {
        str.remove(0,1);
        if (str.startsWith("DATE-TIME:", Qt::CaseInsensitive)) {
            str.remove(0, 10);
            QStringList dts = str.split(',');
            Q_FOREACH (const QString &dtstr, dts) {
                if (dtstr.endsWith('Z')) {
                    // UTC
                    KDateTime kdt = KDateTime::fromString(dtstr, RFC5545_KDATETIME_FORMAT);
                    kdt.setTimeSpec(KDateTime::Spec::UTC());
                    retn.append(kdt);
                } else {
                    // Floating time
                    KDateTime kdt = KDateTime::fromString(dtstr, RFC5545_KDATETIME_FORMAT_NTZC);
                    kdt.setTimeSpec(KDateTime::Spec::ClockTime());
                    retn.append(kdt);
                }
            }
        } else if (str.startsWith("DATE:", Qt::CaseInsensitive)) {
            str.remove(0, 5);
            QStringList dts = str.split(',');
            Q_FOREACH(const QString &dstr, dts) {
239
                QDate date = QLocale::c().toDate(dstr, RFC5545_QDATE_FORMAT);
240 241 242 243 244 245
                KDateTime kdt(date, KDateTime::Spec::ClockTime());
                retn.append(kdt);
            }
        } else if (str.startsWith("PERIOD:", Qt::CaseInsensitive)) {
            SOCIALD_LOG_ERROR("unsupported parameter in ex/rdate string:" << exrdatestr);
            // TODO: support PERIOD formats, or just switch to CalDAV for Google sync...
246
        } else if (str.startsWith("TZID=") && str.contains(':')) {
247 248
            str.remove(0, 5);
            QString tzidstr = str.mid(0, str.indexOf(':')); // something like: "Australia/Brisbane"
249 250 251 252 253
            KTimeZone tz = KSystemTimeZones::zone(tzidstr);
            str.remove(0, tzidstr.size()+1);
            QStringList dts = str.split(',');
            Q_FOREACH (const QString &dtstr, dts) {
                KDateTime kdt = KDateTime::fromString(dtstr, RFC5545_KDATETIME_FORMAT_NTZC);
254 255 256 257 258 259
                if (!kdt.isValid()) {
                    // try parsing from alternate formats
                    kdt = KDateTime::fromString(dtstr, RFC3339_FORMAT_NTZC);
                }
                if (!kdt.isValid()) {
                    SOCIALD_LOG_ERROR("unable to parse datetime from ex/rdate string:" << exrdatestr);
260
                } else {
261 262 263 264 265 266 267
                    if (tz.isValid()) {
                        kdt.setTimeSpec(tz);
                    } else {
                        kdt.setTimeSpec(KDateTime::Spec::ClockTime());
                        SOCIALD_LOG_INFO("WARNING: unknown tzid:" << tzidstr << "; assuming clock-time instead!");
                    }
                    retn.append(kdt);
268 269 270 271 272 273 274 275 276 277 278 279
                }
            }
        } else {
            SOCIALD_LOG_ERROR("invalid parameter in ex/rdate string:" << exrdatestr);
        }
    } else if (str.startsWith(':')) {
        str.remove(0,1);
        QStringList dts = str.split(',');
        Q_FOREACH (const QString &dtstr, dts) {
            if (dtstr.endsWith('Z')) {
                // UTC
                KDateTime kdt = KDateTime::fromString(dtstr, RFC5545_KDATETIME_FORMAT);
280 281 282 283 284 285 286 287 288 289 290
                if (!kdt.isValid()) {
                    // try parsing from alternate formats
                    kdt = KDateTime::fromString(dtstr, RFC3339_FORMAT);
                }
                if (!kdt.isValid()) {
                    SOCIALD_LOG_ERROR("unable to parse datetime from ex/rdate string:" << exrdatestr);
                } else {
                    // parsed successfully
                    kdt.setTimeSpec(KDateTime::Spec::UTC());
                    retn.append(kdt);
                }
291 292 293
            } else {
                // Floating time
                KDateTime kdt = KDateTime::fromString(dtstr, RFC5545_KDATETIME_FORMAT_NTZC);
294 295 296 297 298 299 300 301 302 303 304
                if (!kdt.isValid()) {
                    // try parsing from alternate formats
                    kdt = KDateTime::fromString(dtstr, RFC3339_FORMAT_NTZC);
                }
                if (!kdt.isValid()) {
                    SOCIALD_LOG_ERROR("unable to parse datetime from ex/rdate string:" << exrdatestr);
                } else {
                    // parsed successfully
                    kdt.setTimeSpec(KDateTime::Spec::ClockTime());
                    retn.append(kdt);
                }
305 306 307 308 309 310 311 312 313
            }
        }
    } else {
        SOCIALD_LOG_ERROR("not a valid ex/rdate string:" << exrdatestr);
    }

    return retn;
}

314 315 316
QJsonArray recurrenceArray(KCalCore::Event::Ptr event, KCalCore::ICalFormat &icalFormat)
{
    QJsonArray retn;
317 318

    // RRULE
319 320 321 322 323 324
    KCalCore::Recurrence *kcalRecurrence = event->recurrence();
    Q_FOREACH (KCalCore::RecurrenceRule *rrule, kcalRecurrence->rRules()) {
        QString rruleStr = icalFormat.toString(rrule);
        rruleStr.replace("\r\n", "");
        retn.append(QJsonValue(rruleStr));
    }
325 326

    // EXRULE
327 328 329 330 331 332
    Q_FOREACH (KCalCore::RecurrenceRule *exrule, kcalRecurrence->exRules()) {
        QString exruleStr = icalFormat.toString(exrule);
        exruleStr.replace("RRULE", "EXRULE");
        exruleStr.replace("\r\n", "");
        retn.append(QJsonValue(exruleStr));
    }
333 334 335

    // RDATE (date)
    QString rdates;
336
    Q_FOREACH (const QDate &rdate, kcalRecurrence->rDates()) {
337
        rdates.append(QLocale::c().toString(rdate, RFC5545_QDATE_FORMAT));
338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
        rdates.append(',');
    }
    if (rdates.size()) {
        rdates.chop(1); // trailing comma
        retn.append(QJsonValue(QString::fromLatin1("RDATE;VALUE=DATE:%1").arg(rdates)));
    }

    // RDATE (date-time)
    QString rdatetimes;
    Q_FOREACH (const KDateTime &rdatetime, kcalRecurrence->rDateTimes()) {
        if (rdatetime.timeSpec() == KDateTime::Spec::ClockTime()) {
            rdatetimes.append(rdatetime.toString(RFC5545_KDATETIME_FORMAT_NTZC));
        } else {
            rdatetimes.append(rdatetime.toUtc().toString(RFC5545_KDATETIME_FORMAT));
        }
        rdatetimes.append(',');
    }
    if (rdatetimes.size()) {
        rdatetimes.chop(1); // trailing comma
        retn.append(QJsonValue(QString::fromLatin1("RDATE;VALUE=DATE-TIME:%1").arg(rdatetimes)));
358
    }
359 360 361

    // EXDATE (date)
    QString exdates;
362
    Q_FOREACH (const QDate &exdate, kcalRecurrence->exDates()) {
363
        exdates.append(QLocale::c().toString(exdate, RFC5545_QDATE_FORMAT));
364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383
        exdates.append(',');
    }
    if (exdates.size()) {
        exdates.chop(1); // trailing comma
        retn.append(QJsonValue(QString::fromLatin1("EXDATE;VALUE=DATE:%1").arg(exdates)));
    }

    // EXDATE (date-time)
    QString exdatetimes;
    Q_FOREACH (const KDateTime &exdatetime, kcalRecurrence->exDateTimes()) {
        if (exdatetime.timeSpec() == KDateTime::Spec::ClockTime()) {
            exdatetimes.append(exdatetime.toString(RFC5545_KDATETIME_FORMAT_NTZC));
        } else {
            exdatetimes.append(exdatetime.toUtc().toString(RFC5545_KDATETIME_FORMAT));
        }
        exdatetimes.append(',');
    }
    if (exdatetimes.size()) {
        exdatetimes.chop(1); // trailing comma
        retn.append(QJsonValue(QString::fromLatin1("EXDATE;VALUE=DATE-TIME:%1").arg(exdatetimes)));
384
    }
385

386 387 388
    return retn;
}

389 390 391 392 393 394 395 396 397 398 399
KDateTime parseRecurrenceId(const QJsonObject &originalStartTime)
{
    QString recurrenceIdStr = originalStartTime.value(QLatin1String("dateTime")).toVariant().toString();
    QString recurrenceIdTzStr = originalStartTime.value(QLatin1String("timeZone")).toVariant().toString();
    KDateTime recurrenceId = KDateTime::fromString(recurrenceIdStr, RFC3339_FORMAT);
    if (!recurrenceIdTzStr.isEmpty()) {
        recurrenceId = recurrenceId.toTimeSpec(KTimeZone(recurrenceIdTzStr));
    }
    return recurrenceId;
}

400
QJsonObject kCalToJson(KCalCore::Event::Ptr event, KCalCore::ICalFormat &icalFormat, bool setUidProperty = false)
401 402
{
    QString eventId = gCalEventId(event);
403
    QJsonObject start, end, reminders;
404

405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
    QJsonArray attendees;
    const KCalCore::Attendee::List attendeesList = event->attendees();
    if (!attendeesList.isEmpty()) {
        Q_FOREACH (auto att, attendeesList) {
            if (att->email().isEmpty()) {
                continue;
            }
            QJsonObject attendee;
            attendee.insert("email", att->email());
            if (att->role() == KCalCore::Attendee::OptParticipant) {
                attendee.insert("optional", true);
            }
            const QString &name = att->name();
            if (!name.isEmpty()) {
                attendee.insert("displayName", name);
            }
            attendees.append(attendee);
        }
    }
424 425 426
    // insert the date/time and timeZone information into the Json object.
    // note that timeZone is required for recurring events, for some reason.
    if (event->dtStart().isDateOnly() || (event->allDay() && event->dtStart().time() == QTime(0,0,0))) {
427
        start.insert(QLatin1String("date"), QLocale::c().toString(event->dtStart().date(), QDATEONLY_FORMAT));
428 429 430 431 432
    } else {
        start.insert(QLatin1String("dateTime"), event->dtStart().toString(RFC3339_FORMAT));
        start.insert(QLatin1String("timeZone"), QJsonValue(event->dtStart().toString(KLONGTZ_FORMAT)));
    }
    if (event->dtEnd().isDateOnly() || (event->allDay() && event->dtEnd().time() == QTime(0,0,0))) {
433
        // note: for iCal spec, allDay events need to have an end date of real-end-date+1 as end date is exclusive.
434
        end.insert(QLatin1String("date"), QLocale::c().toString(event->dateEnd().addDays(1), QDATEONLY_FORMAT));
435 436 437 438 439 440 441
    } else {
        end.insert(QLatin1String("dateTime"), event->dtEnd().toString(RFC3339_FORMAT));
        end.insert(QLatin1String("timeZone"), QJsonValue(event->dtEnd().toString(KLONGTZ_FORMAT)));
    }

    QJsonObject retn;
    if (!eventId.isEmpty()) retn.insert(QLatin1String("id"), eventId);
442 443 444 445 446 447
    if (event->recurrence()) {
        QJsonArray recArray = recurrenceArray(event, icalFormat);
        if (recArray.size()) {
            retn.insert(QLatin1String("recurrence"), recArray);
        }
    }
448 449 450 451 452 453
    retn.insert(QLatin1String("summary"), event->summary());
    retn.insert(QLatin1String("description"), event->description());
    retn.insert(QLatin1String("location"), event->location());
    retn.insert(QLatin1String("start"), start);
    retn.insert(QLatin1String("end"), end);
    retn.insert(QLatin1String("sequence"), QString::number(event->revision()+1));
454 455 456
    if (!attendees.isEmpty()) {
        retn.insert(QLatin1String("attendees"), attendees);
    }
457 458 459
    //retn.insert(QLatin1String("locked"), event->readOnly()); // only allow locking server-side.
    // we may wish to support locking/readonly from local side also, in the future.

460 461 462 463 464 465
    // if the event has no alarms associated with it, don't let Google add one automatically.
    if (event->alarms().count() == 0) {
        reminders.insert(QLatin1String("useDefault"), false);
        retn.insert(QLatin1String("reminders"), reminders);
    }

466 467 468 469 470 471 472 473 474 475 476 477
    if (setUidProperty) {
        // now we store private extended properties: local uid.
        // this allows us to detect partially-upsynced artifacts during subsequent syncs.
        // usually this codepath will be hit for localAdditions being upsynced,
        // but sometimes also if we need to update the mapping due to clean-sync.
        QJsonObject privateExtendedProperties;
        privateExtendedProperties.insert(QLatin1String("x-jolla-sociald-mkcal-uid"), event->uid());
        QJsonObject extendedProperties;
        extendedProperties.insert(QLatin1String("private"), privateExtendedProperties);
        retn.insert(QLatin1String("extendedProperties"), extendedProperties);
    }

478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
    return retn;
}

void extractStartAndEnd(const QJsonObject &eventData,
                        bool *startExists,
                        bool *endExists,
                        bool *startIsDateOnly,
                        bool *endIsDateOnly,
                        bool *isAllDay,
                        KDateTime *start,
                        KDateTime *end)
{
    *startIsDateOnly = false, *endIsDateOnly = false;
    QString startTimeString, endTimeString;
    QJsonObject startTimeData = eventData.value(QLatin1String("start")).toObject();
    QJsonObject endTimeData = eventData.value(QLatin1String("end")).toObject();
    if (!startTimeData.value(QLatin1String("date")).toVariant().toString().isEmpty()) {
        *startExists = true;
        *startIsDateOnly = true; // all-day event.
        startTimeString = startTimeData.value(QLatin1String("date")).toVariant().toString();
    } else if (!startTimeData.value(QLatin1String("dateTime")).toVariant().toString().isEmpty()) {
        *startExists = true;
        startTimeString = startTimeData.value(QLatin1String("dateTime")).toVariant().toString();
    } else {
        *startExists = false;
    }
    if (!endTimeData.value(QLatin1String("date")).toVariant().toString().isEmpty()) {
        *endExists = true;
        *endIsDateOnly = true; // all-day event.
        endTimeString = endTimeData.value(QLatin1String("date")).toVariant().toString();
    } else if (!endTimeData.value(QLatin1String("dateTime")).toVariant().toString().isEmpty()) {
        *endExists = true;
        endTimeString = endTimeData.value(QLatin1String("dateTime")).toVariant().toString();
    } else {
        *endExists = false;
    }

    if (*startExists) {
        if (!*startIsDateOnly) {
            KDateTime parsedStartTime = KDateTime::fromString(startTimeString, RFC3339_FORMAT);
            KDateTime ntzcStartTime = KDateTime::fromString(startTimeString, RFC3339_FORMAT_NTZC);
            if (ntzcStartTime.time() > parsedStartTime.time()) parsedStartTime = ntzcStartTime;

            // different format?  let KDateTime detect the format automatically.
            if (parsedStartTime.isNull()) {
                parsedStartTime = KDateTime::fromString(startTimeString);
            }

            *start = parsedStartTime.toLocalZone();
        } else {
528
            *start = KDateTime(QLocale::c().toDate(startTimeString, QDATEONLY_FORMAT), QTime(), KDateTime::ClockTime);
529
            start->setDateOnly(true);
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545
        }
    }

    if (*endExists) {
        if (!*endIsDateOnly) {
            KDateTime parsedEndTime = KDateTime::fromString(endTimeString, RFC3339_FORMAT);
            KDateTime ntzcEndTime = KDateTime::fromString(endTimeString, RFC3339_FORMAT_NTZC);
            if (ntzcEndTime.time() > parsedEndTime.time()) parsedEndTime = ntzcEndTime;

            // different format?  let KDateTime detect the format automatically.
            if (parsedEndTime.isNull()) {
                parsedEndTime = KDateTime::fromString(endTimeString);
            }

            *end = parsedEndTime.toLocalZone();
        } else {
546
            // Special handling for all-day events is required.
547
            if (*startExists && *startIsDateOnly) {
548 549
                if (QLocale::c().toDate(startTimeString, QDATEONLY_FORMAT)
                        == QLocale::c().toDate(endTimeString, QDATEONLY_FORMAT)) {
550 551 552
                    // single-day all-day event
                    *endExists = false;
                    *isAllDay = true;
553 554
                } else if (QLocale::c().toDate(startTimeString, QDATEONLY_FORMAT)
                        == QLocale::c().toDate(endTimeString, QDATEONLY_FORMAT).addDays(-1)) {
555 556 557 558 559
                    // Google will send a single-day all-day event has having an end-date
                    // of startDate+1 to conform to iCal spec.  Hence, this is actually
                    // a single-day all-day event, despite the difference in end-date.
                    *endExists = false;
                    *isAllDay = true;
560
                } else {
561 562 563
                    // multi-day all-day event.
                    // as noted above, Google will send all-day events as having an end-date
                    // of real-end-date+1 in order to conform to iCal spec (exclusive end dt).
564
                    *end = KDateTime(QLocale::c().toDate(endTimeString, QDATEONLY_FORMAT), QTime(), KDateTime::ClockTime);
565
                    end->setDateOnly(true);
566 567 568
                    *isAllDay = true;
                }
            } else {
569
                *end = KDateTime(QLocale::c().toDate(endTimeString, QDATEONLY_FORMAT), QTime(), KDateTime::ClockTime);
570
                end->setDateOnly(true);
571 572 573 574 575 576 577 578 579
                *isAllDay = false;
            }
        }
    }
}

void extractRecurrence(const QJsonArray &recurrence, KCalCore::Event::Ptr event, KCalCore::ICalFormat &icalFormat)
{
    KCalCore::Recurrence *kcalRecurrence = event->recurrence();
580
    kcalRecurrence->clear(); // avoid adding duplicate recurrence information
581 582
    for (int i = 0; i < recurrence.size(); ++i) {
        QString ruleStr = recurrence.at(i).toString();
583
        if (ruleStr.startsWith(QString::fromLatin1("rrule"), Qt::CaseInsensitive)) {
584 585
            KCalCore::RecurrenceRule *rrule = new KCalCore::RecurrenceRule;
            if (!icalFormat.fromString(rrule, ruleStr.mid(6))) {
586 587
                SOCIALD_LOG_DEBUG("unable to parse RRULE information:" << ruleStr);
                traceDumpStr(QString::fromUtf8(QJsonDocument(recurrence).toJson()));
588 589 590
            } else {
                kcalRecurrence->addRRule(rrule);
            }
591
        } else if (ruleStr.startsWith(QString::fromLatin1("exrule"), Qt::CaseInsensitive)) {
592 593
            KCalCore::RecurrenceRule *exrule = new KCalCore::RecurrenceRule;
            if (!icalFormat.fromString(exrule, ruleStr.mid(7))) {
594 595
                SOCIALD_LOG_DEBUG("unable to parse EXRULE information:" << ruleStr);
                traceDumpStr(QString::fromUtf8(QJsonDocument(recurrence).toJson()));
596 597 598
            } else {
                kcalRecurrence->addExRule(exrule);
            }
599 600 601 602 603 604
        } else if (ruleStr.startsWith(QString::fromLatin1("rdate"), Qt::CaseInsensitive)) {
            bool isDateOnly = false;
            QList<KDateTime> rdatetimes = datetimesFromExRDateStr(ruleStr, &isDateOnly);
            if (!rdatetimes.size()) {
                SOCIALD_LOG_DEBUG("unable to parse RDATE information:" << ruleStr);
                traceDumpStr(QString::fromUtf8(QJsonDocument(recurrence).toJson()));
605
            } else {
606 607 608 609 610 611 612
                Q_FOREACH (const KDateTime &kdt, rdatetimes) {
                    if (isDateOnly) {
                        kcalRecurrence->addRDate(kdt.date());
                    } else {
                        kcalRecurrence->addRDateTime(kdt);
                    }
                }
613
            }
614 615 616 617 618 619
        } else if (ruleStr.startsWith(QString::fromLatin1("exdate"), Qt::CaseInsensitive)) {
            bool isDateOnly = false;
            QList<KDateTime> exdatetimes = datetimesFromExRDateStr(ruleStr, &isDateOnly);
            if (!exdatetimes.size()) {
                SOCIALD_LOG_DEBUG("unable to parse EXDATE information:" << ruleStr);
                traceDumpStr(QString::fromUtf8(QJsonDocument(recurrence).toJson()));
620
            } else {
621 622 623 624 625 626 627
                Q_FOREACH (const KDateTime &kdt, exdatetimes) {
                    if (isDateOnly) {
                        kcalRecurrence->addExDate(kdt.date());
                    } else {
                        kcalRecurrence->addExDateTime(kdt);
                    }
                }
628 629
            }
        } else {
630 631
          SOCIALD_LOG_DEBUG("unknown recurrence information:" << ruleStr);
          traceDumpStr(QString::fromUtf8(QJsonDocument(recurrence).toJson()));
632 633 634 635
        }
    }
}

636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676
void extractOrganizer(const QJsonObject &creatorObj, const QJsonObject &organizerObj, KCalCore::Event::Ptr event)
{
    if (!creatorObj.value(QLatin1String("displayName")).toVariant().toString().isEmpty()
                || !creatorObj.value(QLatin1String("email")).toVariant().toString().isEmpty()) {
        KCalCore::Person::Ptr organizer(new KCalCore::Person(
                creatorObj.value(QLatin1String("displayName")).toVariant().toString(),
                creatorObj.value(QLatin1String("email")).toVariant().toString()));
        event->setOrganizer(organizer);
    } else if (!organizerObj.value(QLatin1String("displayName")).toVariant().toString().isEmpty()
                || !organizerObj.value(QLatin1String("email")).toVariant().toString().isEmpty()) {
        KCalCore::Person::Ptr organizer(new KCalCore::Person(
                organizerObj.value(QLatin1String("displayName")).toVariant().toString(),
                organizerObj.value(QLatin1String("email")).toVariant().toString()));
        event->setOrganizer(organizer);
    } else {
        KCalCore::Person::Ptr organizer(new KCalCore::Person);
        event->setOrganizer(organizer);
    }
}

void extractAttendees(const QJsonArray &attendees, KCalCore::Event::Ptr event)
{
    event->clearAttendees();
    for (int i = 0; i < attendees.size(); ++i) {
        QJsonObject attendeeObj = attendees.at(i).toObject();
        if (!attendeeObj.value(QLatin1String("organizer")).toVariant().toBool()) {
            KCalCore::Attendee::Ptr attendee(new KCalCore::Attendee(
                    attendeeObj.value(QLatin1String("displayName")).toVariant().toString(),
                    attendeeObj.value(QLatin1String("email")).toVariant().toString()));
            if (attendeeObj.find(QLatin1String("optional")) != attendeeObj.end()) {
                if (attendeeObj.value(QLatin1String("optional")).toVariant().toBool()) {
                    attendee->setRole(KCalCore::Attendee::OptParticipant);
                } else {
                    attendee->setRole(KCalCore::Attendee::ReqParticipant);
                }
            }
            event->addAttendee(attendee);
        }
    }
}

677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704
int nearestNemoReminderStartOffset(int googleStartOffset)
{
    // Google supports arbitrary start offsets, whereas in Nemo's UI
    // we only allow specific reminder offsets.
    // See nemo-qml-plugin-calendar::NemoCalendarEvent::Reminder for
    // those offset definitions.
    // Also, Nemo reminder offsets are negative and in seconds,
    // whereas Google start offsets are positive and in minutes.
    if (googleStartOffset >= 0 && googleStartOffset <= 5) {
        return -5 * 60;     // 5 minutes before event start
    } else if (googleStartOffset > 5 && googleStartOffset <= 15) {
        return -15 * 60;    // 15 minutes before event start
    } else if (googleStartOffset > 15 && googleStartOffset <= 30) {
        return -30 * 60;    // 30 minutes before event start
    } else if (googleStartOffset > 30 && googleStartOffset <= 60) {
        return -60 * 60;    // 1 hour before event start
    } else if (googleStartOffset > 60 && googleStartOffset <= 120) {
        return -120 * 60;   // 2 hours before event start
    } else if (googleStartOffset > 120 && googleStartOffset <= 1440) {
        return -1440 * 60;  // 1 day before event start (24 hours)
    } else if (googleStartOffset > 1440) {
        return -2880 * 60;  // 2 days before event start (48 hours)
    }

    // default reminder: 15 minutes before event start.
    return -15 * 60;
}

705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725
#define START_EVENT_UPDATES_IF_REQUIRED(event, changed) \
    if (*changed == false) {                            \
        event->startUpdates();                          \
    }                                                   \
    *changed = true;

#define UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, getter, setter, newValue, changed) \
    if (event->getter() != newValue) {                                              \
        START_EVENT_UPDATES_IF_REQUIRED(event, changed)                             \
        event->setter(newValue);                                                    \
    }

#define END_EVENT_UPDATES_IF_REQUIRED(event, changed, startedUpdates)               \
    if (*changed == false) {                                                        \
        SOCIALD_LOG_DEBUG("Ignoring spurious change reported for:" <<               \
                          event->uid() << event->revision() << event->summary());   \
    } else if (startedUpdates) {                                                    \
        event->endUpdates();                                                        \
    }

void extractAlarms(const QJsonObject &json, KCalCore::Event::Ptr event, int defaultReminderStartOffset, bool *changed)
726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767
{
    int startOffset = -1;
    if (json.contains(QStringLiteral("reminders"))) {
        QJsonObject reminders = json.value(QStringLiteral("reminders")).toObject();
        if (reminders.value(QStringLiteral("useDefault")).toBool()) {
            if (defaultReminderStartOffset > 0) {
                startOffset = defaultReminderStartOffset;
            } else {
                SOCIALD_LOG_DEBUG("not adding default reminder even though requested: not popup or invalid start offset.");
            }
        } else {
            QJsonArray overrides = reminders.value(QStringLiteral("overrides")).toArray();
            for (int i = 0; i < overrides.size(); ++i) {
                QJsonObject override = overrides.at(i).toObject();
                if (override.value(QStringLiteral("method")).toString() == QStringLiteral("popup")) {
                    startOffset = override.value(QStringLiteral("minutes")).toInt();
                }
            }
        }
        if (startOffset > -1) {
            startOffset = nearestNemoReminderStartOffset(startOffset);
            SOCIALD_LOG_DEBUG("event needs reminder with start offset (seconds):" << startOffset);
            KCalCore::Alarm::List alarms = event->alarms();
            int alarmCount = 0;
            // check that we have only one non-procedure alarm,
            // and then check to see if its start offset is correct.
            for (int i = 0; i < alarms.count(); ++i) {
                // we don't count Procedure type alarms.
                if (alarms.at(i)->type() != KCalCore::Alarm::Procedure) {
                    alarmCount += 1;
                    if (alarms.at(i)->startOffset().asSeconds() == startOffset) {
                        // no change required to this alarm.
                    } else {
                        alarmCount += 1; // this will cause alarm modification.
                    }
                }
            }
            if (alarmCount == 1) {
                // no need to modify alarms for this event
                SOCIALD_LOG_DEBUG("event already has reminder with start offset (seconds):" << startOffset);
            } else {
                SOCIALD_LOG_DEBUG("setting event reminder with start offset (seconds):" << startOffset);
768
                START_EVENT_UPDATES_IF_REQUIRED(event, changed);
769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786
                for (int i = 0; i < alarms.count(); ++i) {
                    if (alarms.at(i)->type() != KCalCore::Alarm::Procedure) {
                        event->removeAlarm(alarms.at(i));
                    }
                }
                KCalCore::Alarm::Ptr alarm = event->newAlarm();
                alarm->setEnabled(true);
                alarm->setStartOffset(KCalCore::Duration(startOffset));
            }
        }
    }
    if (startOffset == -1) {
        // no reminders were defined in the json received from Google.
        // remove any alarms as required from the local event.
        KCalCore::Alarm::List alarms = event->alarms();
        for (int i = 0; i < alarms.count(); ++i) {
            if (alarms.at(i)->type() != KCalCore::Alarm::Procedure) {
                SOCIALD_LOG_DEBUG("removing event reminder with start offset (seconds):" << alarms.at(i)->startOffset().asSeconds());
787
                START_EVENT_UPDATES_IF_REQUIRED(event, changed);
788 789 790 791 792 793
                event->removeAlarm(alarms.at(i));
            }
        }
    }
}

794
void jsonToKCal(const QJsonObject &json, KCalCore::Event::Ptr event, int defaultReminderStartOffset, KCalCore::ICalFormat &icalFormat, bool *changed)
795
{
796 797 798 799 800 801 802
    bool alreadyStarted = *changed; // if this is true, we don't need to call startUpdates/endUpdates() in this function.
    if (!alreadyStarted && gCalETag(event) == json.value(QLatin1String("etag")).toVariant().toString()) {
        SOCIALD_LOG_DEBUG("Ignoring non-remote-changed:" << event->uid() << ","
                          << gCalETag(event) << "==" << json.value(QLatin1String("etag")).toVariant().toString());
        return; // this event has not changed server-side since we last saw it.
    }

803 804 805 806 807
    KDateTime start, end;
    bool startExists = false, endExists = false;
    bool startIsDateOnly = false, endIsDateOnly = false;
    bool isAllDay = false;
    extractStartAndEnd(json, &startExists, &endExists, &startIsDateOnly, &endIsDateOnly, &isAllDay, &start, &end);
808 809 810 811 812 813 814 815
    if (gCalEventId(event) != json.value(QLatin1String("id")).toVariant().toString()) {
        START_EVENT_UPDATES_IF_REQUIRED(event, changed);
        setGCalEventId(event, json.value(QLatin1String("id")).toVariant().toString());
    }
    if (gCalETag(event) != json.value(QLatin1String("etag")).toVariant().toString()) {
        START_EVENT_UPDATES_IF_REQUIRED(event, changed);
        setGCalETag(event, json.value(QLatin1String("etag")).toVariant().toString());
    }
816
    setRemoteUidCustomField(event, json.value(QLatin1String("iCalUID")).toVariant().toString(), json.value(QLatin1String("id")).toVariant().toString());
817
    extractRecurrence(json.value(QLatin1String("recurrence")).toArray(), event, icalFormat);
818 819
    extractOrganizer(json.value(QLatin1String("creator")).toObject(), json.value(QLatin1String("organizer")).toObject(), event);
    extractAttendees(json.value(QLatin1String("attendees")).toArray(), event);
820 821 822 823
    UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, isReadOnly, setReadOnly, json.value(QLatin1String("locked")).toVariant().toBool(), changed)
    UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, summary, setSummary, json.value(QLatin1String("summary")).toVariant().toString(), changed)
    UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, description, setDescription, json.value(QLatin1String("description")).toVariant().toString(), changed)
    UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, location, setLocation, json.value(QLatin1String("location")).toVariant().toString(), changed)
824
    UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, revision, setRevision, json.value(QLatin1String("sequence")).toVariant().toInt(), changed)
825
    if (startExists) {
826
        UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, dtStart, setDtStart, start, changed)
827 828
    }
    if (endExists) {
829 830 831 832 833
        if (!event->hasEndDate() || event->dtEnd() != end) {
            START_EVENT_UPDATES_IF_REQUIRED(event, changed);
            event->setHasEndDate(true);
            event->setDtEnd(end);
        }
834
    } else {
835
        UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, hasEndDate, setHasEndDate, false, changed)
836 837
    }
    if (isAllDay) {
838
        UPDATE_EVENT_PROPERTY_IF_REQUIRED(event, allDay, setAllDay, false, changed)
839
    }
840 841
    extractAlarms(json, event, defaultReminderStartOffset, changed);
    END_EVENT_UPDATES_IF_REQUIRED(event, changed, !alreadyStarted);
842 843
}

844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861
bool remoteModificationIsReal(const QJsonObject &json, KCalCore::Event::Ptr event)
{
    if (gCalEventId(event) != json.value(QLatin1String("id")).toVariant().toString()) {
        return true; // this event is either a partial-upsync-artifact or a new remote addition.
    }
    if (gCalETag(event) != json.value(QLatin1String("etag")).toVariant().toString()) {
        return true; // this event has changed server-side since we last saw it.
    }
    return false; // this event has not changed server-side since we last saw it.
}

bool localModificationIsReal(const QJsonObject &local, const QJsonObject &remote, int defaultReminderStartOffset, KCalCore::ICalFormat &icalFormat)
{
    bool changed = true;
    KCalCore::Event::Ptr localEvent = KCalCore::Event::Ptr(new KCalCore::Event);
    KCalCore::Event::Ptr remoteEvent = KCalCore::Event::Ptr(new KCalCore::Event);
    jsonToKCal(local, localEvent, defaultReminderStartOffset, icalFormat, &changed);
    jsonToKCal(remote, remoteEvent, defaultReminderStartOffset, icalFormat, &changed);
862
    if (GoogleCalendarIncidenceComparator::incidencesEqual(localEvent, remoteEvent, true)) {
863 864 865 866 867
        return false; // they're equal, so the local modification is not real.
    }
    return true;
}

868 869 870
// returns true if the last sync was marked as successful, and then marks the current
// sync as being unsuccessful.  The sync adapter should set it to true manually
// once sync succeeds.
871
bool wasLastSyncSuccessful(int accountId, bool *needCleanSync)
872 873 874 875
{
    QString settingsFileName = QString::fromLatin1("%1/%2/gcal.ini")
            .arg(QString::fromLatin1(PRIVILEGED_DATA_DIR))
            .arg(QString::fromLatin1(SYNC_DATABASE_DIR));
876 877 878 879 880 881
    if (!QFile::exists(settingsFileName)) {
        SOCIALD_LOG_DEBUG("gcal.ini settings file does not exist, triggering clean sync");
        *needCleanSync = true;
        return false;
    }

882
    QSettings settingsFile(settingsFileName, QSettings::IniFormat);
883 884
    // needCleanSync will be true if and only if an unrecoverable error occurred during the previous sync.
    *needCleanSync = settingsFile.value(QString::fromLatin1("%1-needCleanSync").arg(accountId), QVariant::fromValue<bool>(false)).toBool();
885 886
    bool retn = settingsFile.value(QString::fromLatin1("%1-success").arg(accountId), QVariant::fromValue<bool>(false)).toBool();
    settingsFile.setValue(QString::fromLatin1("%1-success").arg(accountId), QVariant::fromValue<bool>(false));
887 888 889 890 891 892
    int pluginVersion = settingsFile.value(QString::fromLatin1("%1-pluginVersion").arg(accountId), QVariant::fromValue<int>(1)).toInt();
    if (pluginVersion != GOOGLE_CAL_SYNC_PLUGIN_VERSION) {
        settingsFile.setValue(QString::fromLatin1("%1-pluginVersion").arg(accountId), GOOGLE_CAL_SYNC_PLUGIN_VERSION);
        SOCIALD_LOG_DEBUG("Google cal sync plugin version mismatch, force clean sync");
        retn = false;
    }
893 894 895 896 897 898 899 900 901 902 903
    settingsFile.sync();
    return retn;
}

void setLastSyncSuccessful(QList<int> accountIds)
{
    QString settingsFileName = QString::fromLatin1("%1/%2/gcal.ini")
            .arg(QString::fromLatin1(PRIVILEGED_DATA_DIR))
            .arg(QString::fromLatin1(SYNC_DATABASE_DIR));
    QSettings settingsFile(settingsFileName, QSettings::IniFormat);
    Q_FOREACH(int accountId, accountIds) {
904
        settingsFile.setValue(QString::fromLatin1("%1-needCleanSync").arg(accountId), QVariant::fromValue<bool>(false));
905 906 907 908 909
        settingsFile.setValue(QString::fromLatin1("%1-success").arg(accountId), QVariant::fromValue<bool>(true));
    }
    settingsFile.sync();
}

910 911 912 913 914 915 916 917 918 919 920 921 922
void setLastSyncRequiresCleanSync(QList<int> accountIds)
{
    QString settingsFileName = QString::fromLatin1("%1/%2/gcal.ini")
            .arg(QString::fromLatin1(PRIVILEGED_DATA_DIR))
            .arg(QString::fromLatin1(SYNC_DATABASE_DIR));
    QSettings settingsFile(settingsFileName, QSettings::IniFormat);
    Q_FOREACH(int accountId, accountIds) {
        settingsFile.setValue(QString::fromLatin1("%1-needCleanSync").arg(accountId), QVariant::fromValue<bool>(true));
        settingsFile.setValue(QString::fromLatin1("%1-success").arg(accountId), QVariant::fromValue<bool>(false));
    }
    settingsFile.sync();
}

923 924
}

925 926
GoogleCalendarSyncAdaptor::GoogleCalendarSyncAdaptor(QObject *parent)
    : GoogleDataTypeSyncAdaptor(SocialNetworkSyncAdaptor::Calendars, parent)
927 928
    , m_calendar(mKCal::ExtendedCalendar::Ptr(new mKCal::ExtendedCalendar(QLatin1String("UTC"))))
    , m_storage(mKCal::ExtendedCalendar::defaultStorage(m_calendar))
929
    , m_storageNeedsSave(false)
930
{
931
    setInitialActive(true);
932 933 934 935 936 937
}

GoogleCalendarSyncAdaptor::~GoogleCalendarSyncAdaptor()
{
}

938 939 940 941 942 943
QString GoogleCalendarSyncAdaptor::syncServiceName() const
{
    return QStringLiteral("google-calendars");
}

void GoogleCalendarSyncAdaptor::sync(const QString &dataTypeString, int accountId)
944 945
{
    m_storage->open(); // we close it in finalCleanup()
946 947 948
    m_prevSinceTimestamp[accountId] = lastSyncTimestamp(QLatin1String("google"),
                                                        SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Calendars),
                                                        accountId);
949
    GoogleDataTypeSyncAdaptor::sync(dataTypeString, accountId);
950 951
}

952 953
void GoogleCalendarSyncAdaptor::finalCleanup()
{
954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973
    if (m_syncSucceeded.size()) {
        // there is only one account per sync run, even though we haven't fully
        // cleaned up the multi-account-isms from the member variables / API.
        int accountId = m_syncSucceeded.keys().first();
        if (m_syncSucceeded[accountId]) {
            applyRemoteChangesLocally(accountId);
            // only update the local last sync timestamp if sync succeeded
            // otherwise, reset it back to the previous last sync timestamp.
            QDateTime newSyncTimestamp = m_syncSucceeded[accountId]
                                       ? m_newSinceTimestamp[accountId]
                                       : m_prevSinceTimestamp[accountId];
            updateLastSyncTimestamp(QLatin1String("google"),
                                    SocialNetworkSyncAdaptor::dataTypeName(SocialNetworkSyncAdaptor::Calendars),
                                    accountId,
                                    newSyncTimestamp);
            if (!m_syncSucceeded[accountId]) {
                SOCIALD_LOG_INFO("Error occurred while applying remote changes locally; reset last sync timestamp to:" << newSyncTimestamp);
            } else {
                // also update the remote sync timestamp in each notebook.
                Q_FOREACH (const QString &updatedCalendarId, m_calendarsFinishedRequested.keys()) {
974 975 976 977 978
                    // Update the sync date for the notebook, to the timestamp reported by Google
                    // in the calendar request for the remote calendar associated with the notebook,
                    // if that timestamp is recent (within the last week).  If it is older than that,
                    // update it to the current date minus one day, otherwise Google will return
                    // 410 GONE "UpdatedMin too old" error on subsequent requests.
979 980 981 982 983 984 985
                    QString updateTimestamp = m_calendarsFinishedRequested.value(updatedCalendarId);
                    mKCal::Notebook::Ptr notebook = notebookForCalendarId(accountId, updatedCalendarId);
                    if (!notebook) {
                        // may have been deleted due to a purge operation.
                        continue;
                    }
                    KDateTime oldSyncDate = notebook->syncDate();
986 987 988 989 990
                    KDateTime syncDate = datetimeFromUpdateStr(updateTimestamp);
                    KDateTime yesterdayDate = KDateTime::currentDateTime(KDateTime::Spec::UTC()).addDays(-1);
                    if (qAbs(syncDate.daysTo(yesterdayDate)) >= 7) {
                        syncDate = yesterdayDate;
                    }
991 992 993 994 995 996 997 998 999
                    if (oldSyncDate < syncDate) {
                        notebook->setSyncDate(syncDate);
                    }
                    m_storage->updateNotebook(notebook);
                    m_storageNeedsSave = true;
                }
            }
        }
    }
1000

1001 1002 1003
    if (m_storageNeedsSave) {
        m_storage->save();
    }
1004
    m_storageNeedsSave = false;
1005 1006 1007 1008 1009 1010 1011 1012

    // set the success status for each of our account settings.
    QList<int> succeededAccounts;
    Q_FOREACH (int accountId, m_syncSucceeded.keys()) {
        if (m_syncSucceeded.value(accountId)) {
            succeededAccounts.append(accountId);
        }
    }
1013 1014 1015
    if (succeededAccounts.size()) {
        setLastSyncSuccessful(succeededAccounts);
    }
1016 1017 1018 1019 1020 1021 1022 1023 1024
    QList<int> cleanSyncAccounts;
    Q_FOREACH (int accountId, m_cleanSyncRequired.keys()) {
        if (m_cleanSyncRequired.value(accountId)) {
            cleanSyncAccounts.append(accountId);
        }
    }
    if (cleanSyncAccounts.size()) {
        setLastSyncRequiresCleanSync(cleanSyncAccounts);
    }
1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047

    if (!ghostEventCleanupPerformed()) {
        // Delete any events which are not associated with a notebook.
        // These events are ghost events, caused by a bug which previously
        // existed in the sync adapter code due to mkcal deleteNotebook semantics.
        // The mkcal API doesn't allow us to determine which notebook a
        // given incidence belongs to, so we have to instead load
        // everything and then find the ones which are ophaned.
        // Note: we do this separately / after the commit above, because
        // loading all events from the database is expensive.
        SOCIALD_LOG_INFO("performing ghost event cleanup");
        m_storage->load();
        KCalCore::Incidence::List allIncidences;
        m_storage->allIncidences(&allIncidences);
        mKCal::Notebook::List allNotebooks = m_storage->notebooks();
        QSet<QString> notebookIncidenceUids;
        foreach (mKCal::Notebook::Ptr notebook, allNotebooks) {
            KCalCore::Incidence::List currNbIncidences;
            m_storage->allIncidences(&currNbIncidences, notebook->uid());
            foreach (KCalCore::Incidence::Ptr incidence, currNbIncidences) {
                notebookIncidenceUids.insert(incidence->uid());
            }
        }
1048
        int foundOrphans = 0;
1049 1050 1051 1052 1053
        foreach (const KCalCore::Incidence::Ptr incidence, allIncidences) {
            if (!notebookIncidenceUids.contains(incidence->uid())) {
                // orphan/ghost incidence.  must be deleted.
                SOCIALD_LOG_DEBUG("deleting orphan event with uid:" << incidence->uid());
                m_calendar->deleteIncidence(m_calendar->incidence(incidence->uid(), incidence->recurrenceId()));
1054
                foundOrphans++;
1055 1056
            }
        }
1057
        if (foundOrphans == 0) {
1058 1059 1060 1061
            setGhostEventCleanupPerformed();
            SOCIALD_LOG_INFO("orphan cleanup completed without finding orphans!");
        } else if (m_storage->save()) {
            setGhostEventCleanupPerformed();
1062
            SOCIALD_LOG_INFO("orphan cleanup deleted" << foundOrphans << "; storage save completed!");
1063
        } else {
1064
            SOCIALD_LOG_ERROR("orphan cleanup found" << foundOrphans << "; but storage save failed!");
1065 1066 1067 1068
        }
    }

    m_storage->close();
1069 1070
}

1071
void GoogleCalendarSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode)
1072
{
1073 1074 1075 1076 1077
    if (mode == SocialNetworkSyncAdaptor::CleanUpPurge) {
        // need to initialise the database
        m_storage->open(); // we close it in finalCleanup()
    }

1078
    // We clean all the entries in the calendar
1079 1080 1081 1082 1083 1084 1085 1086
    // Delete the notebooks from the storage
    foreach (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) {
        if (notebook->pluginName().startsWith(QString(QLatin1String("google-")))
                && notebook->account() == QString::number(oldId)) {
            // remove the incidences and delete the notebook
            notebook->setIsReadOnly(false);
            m_storage->deleteNotebook(notebook);
            m_storageNeedsSave = true;
1087 1088
        }
    }
1089 1090 1091 1092 1093

    if (mode == SocialNetworkSyncAdaptor::CleanUpPurge) {
        // and commit any changes made.
        finalCleanup();
    }
1094 1095 1096 1097
}

void GoogleCalendarSyncAdaptor::beginSync(int accountId, const QString &accessToken)
{
1098
    SOCIALD_LOG_DEBUG("beginning Calendar sync for Google, account" << accountId);
1099 1100
    bool needCleanSync = false;
    bool lastSyncSuccessful = wasLastSyncSuccessful(accountId, &needCleanSync);
1101
    if (needCleanSync) {
1102 1103 1104
        SOCIALD_LOG_INFO("performing clean sync");
    } else if (!lastSyncSuccessful) {
        SOCIALD_LOG_INFO("last sync was not successful, attempting to recover without clean sync");
1105
    }
1106
    m_serverCalendarIdToCalendarInfo[accountId].clear();
1107
    m_calendarIdToEventObjects[accountId].clear();
1108 1109
    m_syncSucceeded[accountId] = true; // set to false on error
    requestCalendars(accountId, accessToken, needCleanSync);
1110 1111
}

1112
void GoogleCalendarSyncAdaptor::requestCalendars(int accountId, const QString &accessToken, bool needCleanSync, const QString &pageToken)
1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130
{
    QList<QPair<QString, QString> > queryItems;
    queryItems.append(QPair<QString, QString>(QString::fromLatin1("key"), accessToken));
    if (!pageToken.isEmpty()) { // continuation request
        queryItems.append(QPair<QString, QString>(QString::fromLatin1("pageToken"),
                                                  pageToken));
    }

    QUrl url(QLatin1String("https://www.googleapis.com/calendar/v3/users/me/calendarList"));
    QUrlQuery query(url);
    query.setQueryItems(queryItems);
    url.setQuery(query);

    QNetworkRequest request(url);
    request.setRawHeader("GData-Version", "3.0");
    request.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(),
                         QString(QLatin1String("Bearer ") + accessToken).toUtf8());

1131
    QNetworkReply *reply = m_networkAccessManager->get(request);
1132 1133 1134 1135 1136 1137 1138

    // we're requesting data.  Increment the semaphore so that we know we're still busy.
    incrementSemaphore(accountId);

    if (reply) {
        reply->setProperty("accountId", accountId);
        reply->setProperty("accessToken", accessToken);
1139
        reply->setProperty("needCleanSync", QVariant::fromValue<bool>(needCleanSync));
1140 1141 1142 1143 1144
        connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
                this, SLOT(errorHandler(QNetworkReply::NetworkError)));
        connect(reply, SIGNAL(sslErrors(QList<QSslError>)),
                this, SLOT(sslErrorsHandler(QList<QSslError>)));
        connect(reply, SIGNAL(finished()), this, SLOT(calendarsFinishedHandler()));
1145 1146

        setupReplyTimeout(accountId, reply);
1147
    } else {
1148
        SOCIALD_LOG_ERROR("unable to request calendars from Google account with id" << accountId);
1149
        m_syncSucceeded[accountId] = false;
1150 1151 1152 1153 1154 1155 1156 1157 1158
        decrementSemaphore(accountId);
    }
}

void GoogleCalendarSyncAdaptor::calendarsFinishedHandler()
{
    QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
    int accountId = reply->property("accountId").toInt();
    QString accessToken = reply->property("accessToken").toString();
1159
    bool needCleanSync = reply->property("needCleanSync").toBool();
1160 1161 1162 1163 1164
    QByteArray replyData = reply->readAll();
    bool isError = reply->property("isError").toBool();

    disconnect(reply);
    reply->deleteLater();
1165
    removeReplyTimeout(accountId, reply);
1166 1167 1168 1169 1170 1171 1172 1173 1174 1175

    // parse the calendars' metadata from the response.
    bool fetchingNextPage = false;
    bool ok = false;
    QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok);
    if (!isError && ok) {
        // first, check to see if there are more pages of calendars to fetch
        if (parsed.find(QLatin1String("nextPageToken")) != parsed.end()
                && !parsed.value(QLatin1String("nextPageToken")).toVariant().toString().isEmpty()) {
            fetchingNextPage = true;
1176
            requestCalendars(accountId, accessToken, needCleanSync,
1177 1178 1179 1180 1181 1182 1183 1184 1185 1186
                             parsed.value(QLatin1String("nextPageToken")).toVariant().toString());
        }

        // second, parse the calendars' metadata
        QJsonArray items = parsed.value(QStringLiteral("items")).toArray();
        for (int i = 0; i < items.count(); ++i) {
            QJsonObject currCalendar = items.at(i).toObject();
            if (!currCalendar.isEmpty() && currCalendar.find(QStringLiteral("id")) != currCalendar.end()) {
                // we only sync calendars which the user owns (ie, not autogenerated calendars)
                QString accessRole = currCalendar.value(QStringLiteral("accessRole")).toString();
1187
                if (accessRole == QStringLiteral("owner") || accessRole == QStringLiteral("writer")) {
1188 1189 1190 1191
                    GoogleCalendarSyncAdaptor::CalendarInfo currCalendarInfo;
                    currCalendarInfo.color = currCalendar.value(QStringLiteral("backgroundColor")).toString();
                    currCalendarInfo.summary = currCalendar.value(QStringLiteral("summary")).toString();
                    currCalendarInfo.description = currCalendar.value(QStringLiteral("description")).toString();
1192
                    currCalendarInfo.change = NoChange; // we detect the appropriate change type (if required) later.
1193 1194 1195 1196 1197
                    if (accessRole == QStringLiteral("owner")) {
                        currCalendarInfo.access = Owner;
                    } else {
                        currCalendarInfo.access = Writer;
                    }
1198
                    QString currCalendarId = currCalendar.value(QStringLiteral("id")).toString();
1199
                    m_serverCalendarIdToCalendarInfo[accountId].insert(currCalendarId, currCalendarInfo);
1200 1201 1202
                }
            }
        }
1203 1204
    } else {
        // error occurred during request.
1205 1206
        SOCIALD_LOG_ERROR("unable to parse calendar data from request with account" << accountId << "; got:");
        errorDumpStr(QString::fromLatin1(replyData.constData()));
1207
        m_syncSucceeded[accountId] = false;
1208 1209 1210 1211 1212 1213
    }

    if (!fetchingNextPage) {
        // we've finished loading all pages of calendar information
        // we now need to process the loaded information to determine
        // which calendars need to be added/updated/removed locally.
1214
        updateLocalCalendarNotebooks(accountId, accessToken, needCleanSync);
1215 1216 1217 1218 1219 1220 1221
    }

    // we're finished with this request.
    decrementSemaphore(accountId);
}


1222
void GoogleCalendarSyncAdaptor::updateLocalCalendarNotebooks(int accountId, const QString &accessToken, bool needCleanSync)
1223
{
1224 1225 1226 1227 1228
    if (syncAborted()) {
        SOCIALD_LOG_DEBUG("sync aborted, skipping updating local calendar notebooks");
        return;
    }

1229
    // any calendars which exist on the device but not the server need to be purged.
1230
    QStringList calendarsToDelete;
1231
    QStringList deviceCalendarIds;
1232
    foreach (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) {
1233 1234 1235 1236 1237
        // notebook pluginName is of form: google-calendarId
        // where the calendarId comes from the server.
        if (notebook->pluginName().startsWith(QStringLiteral("google-"))
                && notebook->account() == QString::number(accountId)) {
            QString currDeviceCalendarId = notebook->pluginName().mid(7);
1238
            if (m_serverCalendarIdToCalendarInfo[accountId].contains(currDeviceCalendarId)) {
1239
                // the server-side calendar exists on the device.
1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261
                if (needCleanSync) {
                    // we are performing a clean sync cycle.
                    // we will eventually delete and then insert this notebook.
                    SOCIALD_LOG_DEBUG("queueing clean sync of local calendar" << notebook->name() << currDeviceCalendarId << "for Google account:" << accountId);
                    deviceCalendarIds.append(currDeviceCalendarId);
                    m_serverCalendarIdToCalendarInfo[accountId][currDeviceCalendarId].change = GoogleCalendarSyncAdaptor::CleanSync;
                } else {
                    // we don't need to purge it, but we may need to update its summary/color details.
                    deviceCalendarIds.append(currDeviceCalendarId);
                    if (notebook->name() != m_serverCalendarIdToCalendarInfo[accountId].value(currDeviceCalendarId).summary
                            || notebook->color() != m_serverCalendarIdToCalendarInfo[accountId].value(currDeviceCalendarId).color
                            || notebook->description() != m_serverCalendarIdToCalendarInfo[accountId].value(currDeviceCalendarId).description
                            || notebook->isReadOnly()) {
                        // calendar information changed server-side.
                        SOCIALD_LOG_DEBUG("queueing modification of local calendar" << notebook->name() << currDeviceCalendarId << "for Google account:" << accountId);
                        m_serverCalendarIdToCalendarInfo[accountId][currDeviceCalendarId].change = GoogleCalendarSyncAdaptor::Modify;
                    } else {
                        // the calendar information is unchanged server-side.
                        // no need to change anything locally.
                        SOCIALD_LOG_DEBUG("No modification required for local calendar" << notebook->name() << currDeviceCalendarId << "for Google account:" << accountId);
                        m_serverCalendarIdToCalendarInfo[accountId][currDeviceCalendarId].change = GoogleCalendarSyncAdaptor::NoChange;
                    }
1262 1263 1264 1265
                }
            } else {
                // the calendar has been removed from the server.
                // we need to purge it from the device.
1266 1267
                SOCIALD_LOG_DEBUG("queueing removal of local calendar" << notebook->name() << currDeviceCalendarId << "for Google account:" << accountId);
                calendarsToDelete.append(currDeviceCalendarId);
1268 1269 1270 1271 1272
            }
        }
    }

    // any calendarIds which exist on the server but not the device need to be created.
1273
    foreach (const QString &serverCalendarId, m_serverCalendarIdToCalendarInfo[accountId].keys()) {
1274
        if (!deviceCalendarIds.contains(serverCalendarId)) {
1275 1276 1277 1278
            SOCIALD_LOG_DEBUG("queueing addition of local calendar" << serverCalendarId
                              << m_serverCalendarIdToCalendarInfo[accountId].value(serverCalendarId).summary
                              << "for Google account:" << accountId);
            m_serverCalendarIdToCalendarInfo[accountId][serverCalendarId].change = GoogleCalendarSyncAdaptor::Insert;
1279 1280 1281
        }
    }

1282
    SOCIALD_LOG_DEBUG("Syncing calendar events for Google account: " << accountId << " CleanSync: " << needCleanSync);
1283

1284
    foreach (const QString &calendarId, m_serverCalendarIdToCalendarInfo[accountId].keys()) {
1285
        requestEvents(accountId, accessToken, calendarId, needCleanSync);
1286 1287 1288 1289 1290 1291 1292
        m_calendarsBeingRequested.append(calendarId);
    }

    // now we can queue the calendars which need deletion.
    // note: we have to do it after the previous foreach loop, otherwise we'd attempt to retrieve events for them.
    foreach (const QString &currDeviceCalendarId, calendarsToDelete) {
        m_serverCalendarIdToCalendarInfo[accountId][currDeviceCalendarId].change = GoogleCalendarSyncAdaptor::Delete;
1293 1294 1295
    }
}

1296 1297
void GoogleCalendarSyncAdaptor::requestEvents(int accountId, const QString &accessToken, const QString &calendarId,
                                              bool needCleanSync, const QString &pageToken)
1298
{
1299 1300 1301 1302 1303 1304 1305 1306 1307
    // get the last sync date stored into the notebook (if it exists).
    QString updatedMin;
    KDateTime syncDate;
    mKCal::Notebook::Ptr notebook = notebookForCalendarId(accountId, calendarId);
    if (notebook) {
        syncDate = notebook->syncDate();
    }

    if (!needCleanSync && !syncDate.isNull() && syncDate.isValid()) {
1308 1309 1310 1311 1312 1313
        // we will use an updated-min parameter to reduce the amount of data
        // we request from Google.  Note that we do not want to limit it
        // exactly to the syncDate, since we then would not receive enough
        // remote events to determine correct delta from.  We want at least
        // any modifications which also occurred during the PREVIOUS sync period.
        updatedMin = syncDate.addDays(-7).toString();
1314 1315 1316 1317 1318 1319 1320
        SOCIALD_LOG_DEBUG("Previous update timestamp for Google account:" << accountId <<
                          "Calendar Id:" << calendarId <<
                          "- Timestamp:" << syncDate.toString());
    } else if (needCleanSync) {
        SOCIALD_LOG_DEBUG("Clean sync required for Google account:" << accountId <<
                          "Calendar Id:" << calendarId <<
                          "- Ignoring last sync timestamp:" << syncDate.toString());
1321
    } else {
1322 1323 1324
        SOCIALD_LOG_DEBUG("Invalid previous update timestamp for Google account:" << accountId <<
                          "Calendar Id:" << calendarId <<
                          "- Timestamp:" << syncDate.toString());
1325 1326
    }

1327
    QList<QPair<QString, QString> > queryItems;
1328
    queryItems.append(QPair<QString, QString>(QString::fromLatin1("key"), accessToken));
1329
    if (!needCleanSync && !updatedMin.isEmpty()) {
1330 1331
        // we're doing a delta update.  We set the "updatedMin" and request deletions be shown.
        queryItems.append(QPair<QString, QString>(QString::fromLatin1("showDeleted"), QString::fromLatin1("true")));
1332
        queryItems.append(QPair<QString, QString>(QString::fromLatin1("updatedMin"), updatedMin));
1333
    }
1334
    queryItems.append(QPair<QString, QString>(QString::fromLatin1("timeMin"),
1335
                                              QDateTime::currentDateTimeUtc().addYears(-1).toString(Qt::ISODate)));
1336
    queryItems.append(QPair<QString, QString>(QString::fromLatin1("timeMax"),
1337
                                              QDateTime::currentDateTimeUtc().addYears(2).toString(Qt::ISODate)));
1338
    if (!pageToken.isEmpty()) { // continuation request
1339
        queryItems.append(QPair<QString, QString>(QString::fromLatin1("pageToken"), pageToken));
1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351
    }

    QUrl url(QString::fromLatin1("https://www.googleapis.com/calendar/v3/calendars/%1/events").arg(calendarId));
    QUrlQuery query(url);
    query.setQueryItems(queryItems);
    url.setQuery(query);

    QNetworkRequest request(url);
    request.setRawHeader("GData-Version", "3.0");
    request.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(),
                         QString(QLatin1String("Bearer ") + accessToken).toUtf8());

1352
    QNetworkReply *reply = m_networkAccessManager->get(request);
1353 1354 1355 1356 1357 1358 1359 1360

    // we're requesting data.  Increment the semaphore so that we know we're still busy.
    incrementSemaphore(accountId);

    if (reply) {
        reply->setProperty("accountId", accountId);
        reply->setProperty("accessToken", accessToken);
        reply->setProperty("calendarId", calendarId);
1361
        reply->setProperty("needCleanSync", needCleanSync);
1362 1363 1364 1365 1366
        connect(reply, SIGNAL(error(QNetworkReply::NetworkError)),
                this, SLOT(errorHandler(QNetworkReply::NetworkError)));
        connect(reply, SIGNAL(sslErrors(QList<QSslError>)),
                this, SLOT(sslErrorsHandler(QList<QSslError>)));
        connect(reply, SIGNAL(finished()), this, SLOT(eventsFinishedHandler()));
1367

1368
        SOCIALD_LOG_DEBUG("requesting calendar events for Google account:" << accountId << ":" << url.toString());
1369 1370

        setupReplyTimeout(accountId, reply);
1371
    } else {
1372 1373
        SOCIALD_LOG_ERROR("unable to request events for calendar" << calendarId <<
                          "from Google account with id" << accountId);
1374
        m_syncSucceeded[accountId] = false;
1375 1376 1377 1378 1379 1380 1381 1382 1383 1384
        decrementSemaphore(accountId);
    }
}

void GoogleCalendarSyncAdaptor::eventsFinishedHandler()
{
    QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
    int accountId = reply->property("accountId").toInt();
    QString calendarId = reply->property("calendarId").toString();
    QString accessToken = reply->property("accessToken").toString();
1385
    bool needCleanSync = reply->property("needCleanSync").toBool();
1386 1387
    QByteArray replyData = reply->readAll();
    bool isError = reply->property("isError").toBool();
1388
    int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
1389

1390 1391 1392
    QString replyString = QString::fromUtf8(replyData);
    SOCIALD_LOG_TRACE("-------------------------------");
    SOCIALD_LOG_TRACE("Events response for calendar:" << calendarId << "from account:" << accountId);
1393
    SOCIALD_LOG_TRACE("HTTP CODE:" << httpCode);
1394 1395 1396 1397 1398
    Q_FOREACH (QString line, replyString.split('\n', QString::SkipEmptyParts)) {
        SOCIALD_LOG_TRACE(line.replace('\r', ' '));
    }
    SOCIALD_LOG_TRACE("-------------------------------");

1399 1400
    disconnect(reply);
    reply->deleteLater();
1401
    removeReplyTimeout(accountId, reply);
1402 1403 1404

    bool fetchingNextPage = false;
    bool ok = false;
1405
    QString updated;
1406 1407 1408 1409 1410 1411
    QJsonObject parsed = parseJsonObjectReplyData(replyData, &ok);
    if (!isError && ok) {
        // If there are more pages of results to fetch, ensure we fetch them
        if (parsed.find(QLatin1String("nextPageToken")) != parsed.end()
                && !parsed.value(QLatin1String("nextPageToken")).toVariant().toString().isEmpty()) {
            fetchingNextPage = true;
1412
            requestEvents(accountId, accessToken, calendarId, needCleanSync,
1413 1414 1415
                          parsed.value(QLatin1String("nextPageToken")).toVariant().toString());
        }

1416 1417
        updated = parsed.value(QLatin1String("updated")).toVariant().toString();

1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428
        // parse the default reminders data to find the default popup reminder start offset.
        if (parsed.find(QStringLiteral("defaultReminders")) != parsed.end()) {
            QJsonArray defaultReminders = parsed.value(QStringLiteral("defaultReminders")).toArray();
            for (int i = 0; i < defaultReminders.size(); ++i) {
                QJsonObject defaultReminder = defaultReminders.at(i).toObject();
                if (defaultReminder.value(QStringLiteral("method")).toString() == QStringLiteral("popup")) {
                    m_serverCalendarIdToDefaultReminderTimes[accountId][calendarId] = defaultReminder.value(QStringLiteral("minutes")).toInt();
                }
            }
        }

1429 1430 1431 1432 1433 1434 1435 1436 1437 1438
        // Parse the event list
        QJsonArray dataList = parsed.value(QLatin1String("items")).toArray();
        foreach (const QJsonValue &item, dataList) {
            QJsonObject eventData = item.toObject();

            // otherwise, we queue the event for insertion into the database.
            m_calendarIdToEventObjects[accountId].insertMulti(calendarId, eventData);
        }
    } else {
        // error occurred during request.
1439 1440
        SOCIALD_LOG_ERROR("unable to parse event data from request with account" << accountId << "; got:");
        errorDumpStr(QString::fromUtf8(replyData.constData()));
1441
        m_syncSucceeded[accountId] = false;
1442 1443 1444 1445 1446 1447 1448

        if (httpCode == 410) {
            // HTTP 410 GONE is emitted if the syncToken or updatedMin parameters are invalid.
            // We should trigger a clean sync if we hit this error.
            SOCIALD_LOG_ERROR("received 410 GONE from server; marking account for clean sync:" << accountId);
            m_cleanSyncRequired[accountId] = true;
        }
1449 1450 1451 1452 1453 1454
    }

    if (!fetchingNextPage) {
        // we've finished loading all pages of event information
        // we now need to process the loaded information to determine
        // which events need to be added/updated/removed locally.
1455
        QDateTime since = needCleanSync ? QDateTime() : m_prevSinceTimestamp[accountId];
1456
        finishedRequestingRemoteEvents(accountId, accessToken, calendarId, since, updated);
1457 1458
        // note that the updated timestamp string will be empty in the error case,
        // however we only use the updated timestamp string if m_syncSucceeded is true.
1459 1460 1461 1462 1463 1464 1465
    }

    // we're finished this request.  Decrement our busy semaphore.
    decrementSemaphore(accountId);
}


1466 1467
mKCal::Notebook::Ptr GoogleCalendarSyncAdaptor::notebookForCalendarId(int accountId, const QString &calendarId) const
{
1468
    foreach (mKCal::Notebook::Ptr notebook, m_storage->notebooks()) {
1469 1470
        if (notebook->pluginName() == QString::fromLatin1("google-%1").arg(calendarId)
                && notebook->account() == QString::number(accountId)) {
1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488
            return notebook;
        }
    }

    return mKCal::Notebook::Ptr();
}

void GoogleCalendarSyncAdaptor::finishedRequestingRemoteEvents(int accountId, const QString &accessToken,
                                                               const QString &calendarId, const QDateTime &since,
                                                               const QString &updateTimestampStr)
{
    m_calendarsBeingRequested.removeAll(calendarId);
    m_calendarsFinishedRequested.insert(calendarId, updateTimestampStr);
    if (!m_calendarsBeingRequested.isEmpty()) {
        return; // still waiting for more requests to finish.
    }

    if (syncAborted()) {
1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511
        return; // sync was aborted before we received all remote data, and before we could upsync local changes.
    }

    // determine local changes to upsync.
    Q_FOREACH (const QString &finishedCalendarId, m_calendarsFinishedRequested.keys()) {
        // now upsync the local changes to the remote server
        QList<UpsyncChange> changesToUpsync = determineSyncDelta(accountId, accessToken, finishedCalendarId, since);
        if (changesToUpsync.size()) {
            if (syncAborted()) {
                SOCIALD_LOG_DEBUG("skipping upsync of queued upsync changes due to sync being aborted");
            } else if (m_syncSucceeded[accountId] == false) {
                SOCIALD_LOG_DEBUG("skipping upsync of queued upsync changes due to previous error during sync");
            } else {
                SOCIALD_LOG_DEBUG("upsyncing" << changesToUpsync.size() << "local changes to the remote server");
                for (int i = 0; i < changesToUpsync.size(); ++i) {
                    upsyncChanges(changesToUpsync[i].accountId,
                                  changesToUpsync[i].accessToken,
                                  changesToUpsync[i].upsyncType,
                                  changesToUpsync[i].kcalEventId,
                                  changesToUpsync[i].recurrenceId,
                                  changesToUpsync[i].calendarId,
                                  changesToUpsync[i].eventId,
                                  changesToUpsync[i].eventData);
1512
                }
1513
            }
1514 1515 1516
        } else {
            // no local changes to upsync.
            // we can apply the remote changes and we are finished.
1517 1518
        }
    }
1519
}
1520

1521 1522 1523
// Determine the sync delta, and then cache the required downsynced changes and return the required changes to upsync.
QList<GoogleCalendarSyncAdaptor::UpsyncChange> GoogleCalendarSyncAdaptor::determineSyncDelta(int accountId, const QString &accessToken,
                                                                                             const QString &calendarId, const QDateTime &since)
1524 1525 1526
{
    Q_UNUSED(accessToken) // in the future, we might need it to download images/data associated with the event.

1527 1528 1529 1530