diff --git a/src/google/google-contacts/google-contacts.pri b/src/google/google-contacts/google-contacts.pri index 656dc5b..ed7d0d0 100644 --- a/src/google/google-contacts/google-contacts.pri +++ b/src/google/google-contacts/google-contacts.pri @@ -4,14 +4,14 @@ QT += gui SOURCES += \ $$PWD/googletwowaycontactsyncadaptor.cpp \ - $$PWD/googlecontactstream.cpp \ - $$PWD/googlecontactatom.cpp \ + $$PWD/googlepeopleapi.cpp \ + $$PWD/googlepeoplejson.cpp \ $$PWD/googlecontactimagedownloader.cpp HEADERS += \ $$PWD/googletwowaycontactsyncadaptor.h \ - $$PWD/googlecontactstream.h \ - $$PWD/googlecontactatom.h \ + $$PWD/googlepeopleapi.h \ + $$PWD/googlepeoplejson.h \ $$PWD/googlecontactimagedownloader.h INCLUDEPATH += $$PWD diff --git a/src/google/google-contacts/googlecontactatom.cpp b/src/google/google-contacts/googlecontactatom.cpp deleted file mode 100644 index db05bae..0000000 --- a/src/google/google-contacts/googlecontactatom.cpp +++ /dev/null @@ -1,210 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2014 Jolla Ltd. and/or its subsidiary(-ies). - ** Contact: Chris Adams - ** - ** Contributors: Sateesh Kavuri - ** Mani Chandrasekar - ** Chris Adams - ** - ** 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 - ** - ****************************************************************************/ - -#include "googlecontactatom.h" -#include - -GoogleContactAtom::BatchOperationResponse::BatchOperationResponse() - : isError(false) -{ -} - -GoogleContactAtom::GoogleContactAtom() -{ -} - -void GoogleContactAtom::setAuthorEmail(const QString &authorEmail) -{ - mAuthorEmail = authorEmail; -} - -QString GoogleContactAtom::authorEmail() const -{ - return mAuthorEmail; -} - -void GoogleContactAtom::setAuthorName(const QString &authorName) -{ - mAuthorName = authorName; -} - -QString GoogleContactAtom::authorName() const -{ - return mAuthorName; -} - -void GoogleContactAtom::setId(const QString &id) -{ - mId = id; -} - -QString GoogleContactAtom::id() const -{ - return mId; -} - -void GoogleContactAtom::setUpdated(const QString &updated) -{ - mUpdated = updated; -} - -QString GoogleContactAtom::updated() const -{ - return mUpdated; -} - -void GoogleContactAtom::setCategory(const QString &schema, const QString &term) -{ - mCategorySchema = schema; - mCategoryTerm = term; -} - -QString GoogleContactAtom::categorySchema() const -{ - return mCategorySchema; -} - -QString GoogleContactAtom::categoryTerm() const -{ - return mCategoryTerm; -} - -void GoogleContactAtom::setTitle(const QString &title) -{ - mTitle = title; -} - -QString GoogleContactAtom::title() const -{ - return mTitle; -} - -void GoogleContactAtom::setGenerator(const QString &name, const QString &version, const QString &uri) -{ - mGeneratorName = name; - mGeneratorVersion = version; - mGeneratorUri = uri; -} - -void GoogleContactAtom::setContent(const QString ¬e, const QString &type) -{ - Q_UNUSED(note) - Q_UNUSED(type) -} - -QString GoogleContactAtom::generatorName() const -{ - return mGeneratorName; -} - -QString GoogleContactAtom::generatorVersion() const -{ - return mGeneratorVersion; -} - -QString GoogleContactAtom::generatorUri() const -{ - return mGeneratorUri; -} - -void GoogleContactAtom::setTotalResults(int totalResults) -{ - mTotalResults = totalResults; -} - -int GoogleContactAtom::totalResults() const -{ - return mTotalResults; -} - -void GoogleContactAtom::setStartIndex(int startIndex) -{ - mStartIndex = startIndex; -} - -int GoogleContactAtom::startIndex() const -{ - return mStartIndex; -} - -void GoogleContactAtom::setItemsPerPage(int itemsPerPage) -{ - mItemsPerPage = itemsPerPage; -} - -int GoogleContactAtom::itemsPerPage() const -{ - return mItemsPerPage; -} - -void GoogleContactAtom::addBatchOperationResponse(const QString &operationId, GoogleContactAtom::BatchOperationResponse response) -{ - mBatchOperationResponses.insert(operationId, response); -} - -QMap GoogleContactAtom::batchOperationResponses() const -{ - return mBatchOperationResponses; -} - -void GoogleContactAtom::addEntryContact(const QContact &entryContact, const QStringList &unsupportedElements) -{ - mContactList.append(qMakePair(entryContact, unsupportedElements)); -} - -QList > GoogleContactAtom::entryContacts() const -{ - return mContactList; -} - -void GoogleContactAtom::addDeletedEntryContact(const QContact &deletedContact) -{ - mDeletedContactList.append(deletedContact); -} - -QList GoogleContactAtom::deletedEntryContacts() const -{ - return mDeletedContactList; -} - -void GoogleContactAtom::addEntrySystemGroup(const QString &systemGroupId, const QString &systemGroupAtomId, const QString &systemGroupTitle) -{ - mSystemGroupAtomIds.insert(systemGroupId, qMakePair(systemGroupAtomId, systemGroupTitle)); -} - -QMap > GoogleContactAtom::entrySystemGroups() const -{ - return mSystemGroupAtomIds; -} - -void GoogleContactAtom::setNextEntriesUrl(const QString &nextUrl) -{ - mNextEntriesUrl = nextUrl; -} - -QString GoogleContactAtom::nextEntriesUrl() const -{ - return mNextEntriesUrl; -} diff --git a/src/google/google-contacts/googlecontactatom.h b/src/google/google-contacts/googlecontactatom.h deleted file mode 100644 index ffa1328..0000000 --- a/src/google/google-contacts/googlecontactatom.h +++ /dev/null @@ -1,139 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2014 Jolla Ltd. and/or its subsidiary(-ies). - ** Contact: Chris Adams - ** - ** Contributors: Sateesh Kavuri - ** Chris Adams - ** - ** 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 - ** - ****************************************************************************/ - -#ifndef GOOGLECONTACTATOM_H -#define GOOGLECONTACTATOM_H - -#include -#include -#include -#include - -#include - -USE_CONTACTS_NAMESPACE - -class GoogleContactAtom { -public: - GoogleContactAtom(); - - void setAuthorName(const QString &authorName); - QString authorName() const; - - void setAuthorEmail(const QString &authorEmail); - QString authorEmail() const; - - void setId(const QString &id); - QString id() const; - - void setUpdated(const QString &updated); - QString updated() const; - - void setCategory(const QString &schema = QStringLiteral("http://schemas.google.com/g/2005#kind"), - const QString &term = QStringLiteral("http://schemas.google.com/contact/2008#contact")); - QString categorySchema() const; - QString categoryTerm() const; - - void setTitle(const QString &title); - QString title() const; - - void setContent(const QString ¬e, const QString &type = QStringLiteral("text")); - - void setGenerator(const QString &name = QStringLiteral("Contacts"), - const QString &version = QStringLiteral("1.0"), - const QString &uri = QStringLiteral("https://sailfishos.org")); - QString generatorName() const; - QString generatorVersion() const; - QString generatorUri() const; - - void setTotalResults(int totalResults); - int totalResults() const; - - void setStartIndex(int startIndex); - int startIndex() const; - - void setItemsPerPage(int itemsPerPage); - int itemsPerPage() const; - - void addEntryContact(const QContact &contact, const QStringList &unsupportedElements); - QList > entryContacts() const; - void addDeletedEntryContact(const QContact &contact); - QList deletedEntryContacts() const; - - void addEntrySystemGroup(const QString &systemGroupId, const QString &systemGroupAtomId, const QString &systemGroupTitle); - QMap > entrySystemGroups() const; - - void setNextEntriesUrl(const QString &nextUrl); - QString nextEntriesUrl() const; - - class BatchOperationResponse { - public: - BatchOperationResponse(); - QStringList unsupportedElements; - QString operationId; - QString type; - QString code; - QString reason; - QString reasonDescription; - QString contactGuid; - QString etag; - bool isError; - }; - void addBatchOperationResponse(const QString &operationId, BatchOperationResponse response); - QMap batchOperationResponses() const; - -private: - QString mAuthorEmail; - QString mAuthorName; - QString mCategory; - QString mCategorySchema; - QString mCategoryTerm; - QString mContributor; - QString mGeneratorName; - QString mGeneratorVersion; - QString mGeneratorUri; - QString mIcon; - QString mId; - QString mLink; - QString mLogo; - QString mRights; - QString mSubtitle; - QString mTitle; - QString mUpdated; - - int mTotalResults; - int mStartIndex; - int mItemsPerPage; - - QMap mBatchOperationResponses; - - QList mDeletedContactList; - QList > mContactList; - - QMap > mSystemGroupAtomIds; - - QString mNextEntriesUrl; -}; - -#endif // GOOGLECONTACTATOM_H diff --git a/src/google/google-contacts/googlecontactimagedownloader.cpp b/src/google/google-contacts/googlecontactimagedownloader.cpp index d5e7b18..2f592ac 100644 --- a/src/google/google-contacts/googlecontactimagedownloader.cpp +++ b/src/google/google-contacts/googlecontactimagedownloader.cpp @@ -45,7 +45,6 @@ QNetworkReply * GoogleContactImageDownloader::createReply(const QString &url, QString accessToken = metadata.value(IMAGE_DOWNLOADER_TOKEN_KEY).toString(); QNetworkRequest request(url); - request.setRawHeader("GData-Version", "3.0"); request.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), QString(QLatin1String("Bearer ") + accessToken).toUtf8()); return d->networkAccessManager->get(request); diff --git a/src/google/google-contacts/googlecontactstream.cpp b/src/google/google-contacts/googlecontactstream.cpp deleted file mode 100644 index e0de6c0..0000000 --- a/src/google/google-contacts/googlecontactstream.cpp +++ /dev/null @@ -1,1355 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2014 Jolla Ltd. and/or its subsidiary(-ies). - ** Contact: Chris Adams - ** - ** Contributors: Sateesh Kavuri - ** Mani Chandrasekar - ** Chris Adams - ** - ** 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 - ** - ****************************************************************************/ - -#include "googlecontactstream.h" -#include "googlecontactatom.h" -#include "constants_p.h" -#include "trace.h" - -#include - -#include -#include - -namespace { - -void dumpXml(const QByteArray &xml) -{ - // this algorithm doesn't handle a lot of stuff (escaped slashes/angle-brackets, slashes/angle-brackets in text, etc) but it works: - // see < then read until > and that becomes "tag". Print indent, print tag, print newline. If tag didn't contain /> then indent += " " else deindent. - // see anything else, then read until < and that becomes "text". Print indent, print text, print newline. - QString indent; - QString formatted; - QString allData = QString::fromUtf8(xml); - for (QString::const_iterator it = allData.constBegin(); it != allData.constEnd(); ) { - QString text; - QString tag; - bool seenSlash = false, needDeindent = false; - // skip over empty lines - if (*it == QChar('\n') || *it == QChar('\r')) { - it++; - continue; - } - // if found a start of tag, read until the end of the tag - if (*it == QChar('<')) { - while (it != allData.constEnd() && *it != QChar('>')) { - tag += *it; - if (*it == '/') { - if (tag == QStringLiteral(" - if (it != allData.constEnd()) { - tag += *it; - it++; - } - // check to see if it was a self-ending tag - seenSlash = tag.endsWith(QStringLiteral("/>")) - || tag.endsWith(QStringLiteral("/ >")); - // adjust the indentation if required - if (needDeindent && indent.size() >= 4) { - // if the tag we saw was an end-tag, reduce the indentation and then print the tag - indent.chop(4); - formatted += indent + tag + '\n'; - } else if (!seenSlash) { - // if the tag we saw was a begin-tag, print the tag and then increase the indentation - formatted += indent + tag + '\n'; - indent += QStringLiteral(" "); - } else { - // if the tag we saw was a self-ending tag, don't adjust the indentation, print the tag - formatted += indent + tag + '\n'; - } - } else { - // read until the next start of a tag - while (it != allData.constEnd() && *it != QChar('<')) { - text += *it; - it++; - } - formatted += indent + text + '\n'; - } - } - - SOCIALD_LOG_TRACE("---------- Dumping XML data:"); - Q_FOREACH (const QString &line, formatted.split('\n')) { - SOCIALD_LOG_TRACE(line); - } -} - -bool traceOutputEnabled() -{ - const QByteArray loggingLevelByteArray = qgetenv("MSYNCD_LOGGING_LEVEL"); - const QString loggingLevelStr = QString::fromLocal8Bit(loggingLevelByteArray.constData()); - const QByteArray dumpXmlByteArray = qgetenv("MSYNCD_DUMP_XML"); - const QString dumpXmlStr = QString::fromLocal8Bit(dumpXmlByteArray.constData()); - bool ok = false; - int level = loggingLevelStr.toInt(&ok); - int dump = dumpXmlStr.toInt(&ok); - return ok && level >= 8 && dump == 1; -} - -bool saveExtendedDetail(QContact *contact, const QString &detailName, const QVariant &detailData) -{ - QContactExtendedDetail matchedDetail; - for (const QContactExtendedDetail &detail : contact->details()) { - if (detail.name() == detailName) { - matchedDetail = detail; - break; - } - } - - if (matchedDetail.name().isEmpty()) { - matchedDetail.setName(detailName); - } - matchedDetail.setData(detailData); - return contact->saveDetail(&matchedDetail, QContact::IgnoreAccessConstraints); -} - -bool saveContactEtag(QContact *contact, const QString &etag) -{ - return saveExtendedDetail(contact, QStringLiteral("etag"), etag); -} - -} - -GoogleContactStream::GoogleContactStream(bool response, int accountId, const QString &accountEmail, QObject* parent) - : QObject(parent) - , mXmlReader(0) - , mAtom(0) - , mAccountId(accountId) - , mXmlWriter(0) - , mAccountEmail(accountEmail) -{ - if (response == true) { - initResponseFunctionMap(); - } else { - initFunctionMap(); - } -} - -GoogleContactStream::~GoogleContactStream() -{ -} - -GoogleContactAtom *GoogleContactStream::parse(const QByteArray &xmlBuffer) -{ - static bool traceEnabled = traceOutputEnabled(); - if (traceEnabled) { - dumpXml(xmlBuffer); - } - - mXmlReader = new QXmlStreamReader(xmlBuffer); - mAtom = new GoogleContactAtom; - - Q_CHECK_PTR(mXmlReader); - Q_CHECK_PTR(mAtom); - - while (!mXmlReader->atEnd() && !mXmlReader->hasError()) { - if (mXmlReader->readNextStartElement()) { - Handler handler = mAtomFunctionMap.value(mXmlReader->name().toString()); - if (handler) { - (*this.*handler)(); - } - } - } - - delete mXmlReader; - return mAtom; -} - -QByteArray GoogleContactStream::encode(const QMultiMap > &updates) -{ - QByteArray xmlBuffer; - mXmlWriter = new QXmlStreamWriter(&xmlBuffer); - startBatchFeed(); - - QList > removedContacts = updates.values(GoogleContactStream::Remove); - for (int i = 0; i < removedContacts.size(); ++i) { - encodeContactUpdate(removedContacts[i].first, removedContacts[i].second, GoogleContactStream::Remove, true); // batchmode = true - } - - QList > addedContacts = updates.values(GoogleContactStream::Add); - for (int i = 0; i < addedContacts.size(); ++i) { - encodeContactUpdate(addedContacts[i].first, addedContacts[i].second, GoogleContactStream::Add, true); // batchmode = true - } - - QList > modifiedContacts = updates.values(GoogleContactStream::Modify); - for (int i = 0; i < modifiedContacts.size(); ++i) { - encodeContactUpdate(modifiedContacts[i].first, modifiedContacts[i].second, GoogleContactStream::Modify, true); // batchmode = true - } - - endBatchFeed(); - mXmlWriter->writeEndDocument(); - delete mXmlWriter; - return xmlBuffer; -} - -// ---------------------------------------- - -void GoogleContactStream::initAtomFunctionMap() -{ - mAtomFunctionMap.insert("updated", &GoogleContactStream::handleAtomUpdated); - mAtomFunctionMap.insert("category", &GoogleContactStream::handleAtomCategory); - mAtomFunctionMap.insert("author", &GoogleContactStream::handleAtomAuthor); - mAtomFunctionMap.insert("totalResults", &GoogleContactStream::handleAtomOpenSearch); - mAtomFunctionMap.insert("startIndex", &GoogleContactStream::handleAtomOpenSearch); - mAtomFunctionMap.insert("itemsPerPage", &GoogleContactStream::handleAtomOpenSearch); - mAtomFunctionMap.insert("link", &GoogleContactStream::handleAtomLink); - mAtomFunctionMap.insert("entry", &GoogleContactStream::handleAtomEntry); -} - -void GoogleContactStream::initResponseFunctionMap() -{ - initAtomFunctionMap(); - // TODO: move the batch request response handling stuff here. -} - -void GoogleContactStream::initFunctionMap() -{ - initAtomFunctionMap(); - mContactFunctionMap.insert("content", &GoogleContactStream::handleEntryContent); - mContactFunctionMap.insert("updated", &GoogleContactStream::handleEntryUpdated); - mContactFunctionMap.insert("app:edited", &GoogleContactStream::handleEntryUpdated); - mContactFunctionMap.insert("gContact:birthday", &GoogleContactStream::handleEntryBirthday); - mContactFunctionMap.insert("gContact:gender", &GoogleContactStream::handleEntryGender); - mContactFunctionMap.insert("gContact:hobby", &GoogleContactStream::handleEntryHobby); - mContactFunctionMap.insert("gContact:nickname", &GoogleContactStream::handleEntryNickname); - mContactFunctionMap.insert("gContact:occupation", &GoogleContactStream::handleEntryOccupation); - mContactFunctionMap.insert("gContact:website", &GoogleContactStream::handleEntryWebsite); - mContactFunctionMap.insert("gContact:jot", &GoogleContactStream::handleEntryJot); - mContactFunctionMap.insert("gd:comments", &GoogleContactStream::handleEntryComments); - mContactFunctionMap.insert("gd:email", &GoogleContactStream::handleEntryEmail); - mContactFunctionMap.insert("gd:im", &GoogleContactStream::handleEntryIm); - mContactFunctionMap.insert("gd:name", &GoogleContactStream::handleEntryName); - mContactFunctionMap.insert("gd:organization", &GoogleContactStream::handleEntryOrganization); - mContactFunctionMap.insert("gd:phoneNumber", &GoogleContactStream::handleEntryPhoneNumber); - mContactFunctionMap.insert("gd:structuredPostalAddress", &GoogleContactStream::handleEntryStructuredPostalAddress); -} - -// ---------------------------------------- - -void GoogleContactStream::handleAtomUpdated() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->name() == "updated"); - mAtom->setUpdated(mXmlReader->readElementText()); -} - -void GoogleContactStream::handleAtomCategory() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->name() == "category"); - - QXmlStreamAttributes attributes = mXmlReader->attributes(); - QString scheme, term; - if (attributes.hasAttribute("scheme")) { - scheme = attributes.value("scheme").toString(); - } else if (attributes.hasAttribute("term")) { - term = attributes.value("term").toString(); - } - - mAtom->setCategory(scheme, term); -} - -void GoogleContactStream::handleAtomAuthor() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->name() == "author"); - - while (!(mXmlReader->tokenType() == QXmlStreamReader::EndElement && mXmlReader->name() == "author")) { - if (mXmlReader->tokenType() == QXmlStreamReader::StartElement) { - if (mXmlReader->name() == "name") { - mAtom->setAuthorName(mXmlReader->readElementText()); - } else if (mXmlReader->name() == "email") { - mAtom->setAuthorEmail(mXmlReader->readElementText()); - } - } - mXmlReader->readNextStartElement(); - } -} - -void GoogleContactStream::handleAtomOpenSearch() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->prefix() == "openSearch"); - - if (mXmlReader->name() == "totalResults") { - mAtom->setTotalResults(mXmlReader->readElementText().toInt()); - } else if (mXmlReader->name() == "startIndex") { - mAtom->setStartIndex(mXmlReader->readElementText().toInt()); - } else if (mXmlReader->name() == "itemsPerPage") { - mAtom->setItemsPerPage(mXmlReader->readElementText().toInt()); - } -} - -void GoogleContactStream::handleAtomLink() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->name() == "link"); - - if (mXmlReader->attributes().hasAttribute("rel") && (mXmlReader->attributes().value("rel") == "next")) { - mAtom->setNextEntriesUrl(mXmlReader->attributes().value("href").toString()); - } -} - -void GoogleContactStream::handleAtomEntry() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->name() == "entry"); - - // the entry will either be a contact, a group, or a response to a batch update request. - // if it's a group, we need to store some information about it. - QString systemGroupId; - QString systemGroupAtomId; - - // the entry will be a contact if this is a response to a "read" request - QContact entryContact; - QString contactEtag; - QStringList unsupportedElements; - bool isInGroup = false; - bool isDeleted = false; - - QString title; - - // or it will be a series of batch operation success/fail info - // if this xml is the response to a batch update/delete request. - bool isBatchOperationResponse = false; - GoogleContactAtom::BatchOperationResponse response; - - while (!((mXmlReader->tokenType() == QXmlStreamReader::EndElement) && (mXmlReader->name() == "entry"))) { - if (mXmlReader->tokenType() == QXmlStreamReader::StartElement) { - DetailHandler handler = mContactFunctionMap.value(mXmlReader->qualifiedName().toString()); - if (handler) { - QContactDetail convertedDetail = (*this.*handler)(); - if (convertedDetail != QContactDetail()) { - convertedDetail.setValue(QContactDetail__FieldModifiable, true); - entryContact.saveDetail(&convertedDetail, QContact::IgnoreAccessConstraints); - } - } else if (mXmlReader->qualifiedName().toString() == QStringLiteral("gContact:groupMembershipInfo")) { - isInGroup = true; - QString unsupportedElement = handleEntryUnknownElement(); - if (!unsupportedElement.isEmpty()) { - unsupportedElements.append(unsupportedElement); - } - } else if (mXmlReader->qualifiedName().toString() == QStringLiteral("gd:deleted")) { - isDeleted = true; - } else if (mXmlReader->qualifiedName().toString() == QStringLiteral("batch:id")) { - isBatchOperationResponse = true; - handleEntryBatchId(&response); - } else if (mXmlReader->qualifiedName().toString() == QStringLiteral("batch:operation")) { - isBatchOperationResponse = true; - handleEntryBatchOperation(&response); - } else if (mXmlReader->qualifiedName().toString() == QStringLiteral("batch:status")) { - isBatchOperationResponse = true; - handleEntryBatchStatus(&response); - } else if (mXmlReader->qualifiedName().toString() == QStringLiteral("gd:extendedProperty")) { - // It might be an extension property we don't support. - // If we don't support it, we store the element text. - QString unsupportedElement = handleEntryExtendedProperty(); - if (!unsupportedElement.isEmpty()) { - unsupportedElements.append(unsupportedElement); - } - } else if (mXmlReader->qualifiedName().toString() == QStringLiteral("link")) { - // There are several possible links: - // Avatar Photo link - // Self query link - // Edit link - // Batch link etc. - - // If it's an avatar, we grab it as a QContactAvatar detail - QContactAvatar avatar = entryContact.detail(); - bool isAvatar = false; - QString unsupportedElement = handleEntryLink(&avatar, &isAvatar); - if (isAvatar) { - entryContact.saveDetail(&avatar, QContact::IgnoreAccessConstraints); - } - - // Whether it's an avatar or not, we also store the element text. - if (!unsupportedElement.isEmpty()) { - unsupportedElements.append(unsupportedElement); - } - } else if (mXmlReader->name().toString() == QStringLiteral("entry")) { - // read the etag out of the entry. - contactEtag = mXmlReader->attributes().value("gd:etag").toString(); - } else if (mXmlReader->qualifiedName().toString() == QStringLiteral("gContact:systemGroup")) { - systemGroupId = mXmlReader->attributes().value("id").toString(); - } else if (mXmlReader->qualifiedName().toString() == QStringLiteral("id")) { - // either a contact id or a group id. - QContactDetail guidDetail = handleEntryId(&systemGroupAtomId); - entryContact.saveDetail(&guidDetail, QContact::IgnoreAccessConstraints); - } else if (mXmlReader->name().toString() == QStringLiteral("title")) { - title = mXmlReader->readElementText(); - } else { - // This is some XML element which we don't handle. - // We should store it, so that we can send it back when we upload changes. - QString unsupportedElement = handleEntryUnknownElement(); - if (!unsupportedElement.isEmpty()) { - unsupportedElements.append(unsupportedElement); - } - } - } - mXmlReader->readNextStartElement(); - } - - if (!systemGroupId.isEmpty()) { - // this entry was a group - mAtom->addEntrySystemGroup(systemGroupId, systemGroupAtomId, title); - } else { - // this entry was a contact. - // the etag is the "version identifier". - if (!contactEtag.isEmpty()) { - saveContactEtag(&entryContact, contactEtag); - } - - if (isInGroup) { - // Only sync the contact if it is in a "real" group - // as otherwise we get hundreds of "Other Contacts" - // (random email addresses etc). - if (isDeleted) { - mAtom->addDeletedEntryContact(entryContact); - } else { - mAtom->addEntryContact(entryContact, unsupportedElements); - } - } - } - - if (isBatchOperationResponse) { - if (!entryContact.detail().guid().isEmpty()) { - response.contactGuid = entryContact.detail().guid(); - response.etag = contactEtag; - response.unsupportedElements = unsupportedElements; - } - mAtom->addBatchOperationResponse(response.operationId, response); - } -} - -QString GoogleContactStream::handleEntryLink(QContactAvatar *avatar, bool *isAvatar) -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->name() == "link"); - - const QString etag = mXmlReader->attributes().value("gd:etag").toString(); - if (!etag.isEmpty() - && (mXmlReader->attributes().value("rel") == "http://schemas.google.com/contacts/2008/rel#photo")) { - // this is an avatar photo for the contact entry - avatar->setImageUrl(mXmlReader->attributes().value("href").toString()); - avatar->setValue(QContactAvatar::FieldMetaData, etag); - *isAvatar = true; - } - - return handleEntryUnknownElement(); -} - -QString GoogleContactStream::handleEntryExtendedProperty() -{ - Q_ASSERT(mXmlReader->isStartElement()); - return handleEntryUnknownElement(); -} - -QString GoogleContactStream::handleEntryUnknownElement() -{ - Q_ASSERT(mXmlReader->isStartElement()); - - QXmlStreamAttributes attributes = mXmlReader->attributes(); - QString attributesString; - for (int i = 0; i < attributes.size(); ++i) { - QString extra = QStringLiteral(" %1=\"%2\"") - .arg(attributes[i].qualifiedName().toString()) - .arg(attributes[i].value().toString().toHtmlEscaped()); - attributesString.append(extra); - } - - QString unknownElement = QStringLiteral("<%1%2>%3") - .arg(mXmlReader->qualifiedName().toString()) - .arg(attributesString) - .arg(mXmlReader->text().toString()); - - return unknownElement; -} - -void GoogleContactStream::handleEntryBatchStatus(GoogleContactAtom::BatchOperationResponse *response) -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->name() == "status"); - - response->code = mXmlReader->attributes().value("code").toString(); - response->reason = mXmlReader->attributes().value("reason").toString(); - response->reasonDescription = mXmlReader->readElementText(); - response->isError = true; - if (response->code == QStringLiteral("200") // No error. - || response->code == QStringLiteral("201") // Created without error. - || response->code == QStringLiteral("304")) { // Not modified (no change since time specified) - // according to Google Data API these response codes signify success cases. - response->isError = false; - } -} - -void GoogleContactStream::handleEntryBatchOperation(GoogleContactAtom::BatchOperationResponse *response) -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->name() == "operation"); - response->type = mXmlReader->attributes().value("type").toString(); -} - -void GoogleContactStream::handleEntryBatchId(GoogleContactAtom::BatchOperationResponse *response) -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->name() == "id"); - response->operationId = mXmlReader->readElementText(); -} - -QContactDetail GoogleContactStream::handleEntryId(QString *rawId) -{ - *rawId = mXmlReader->readElementText(); - QString idUrl = *rawId; - QContactGuid guid; - guid.setGuid(QStringLiteral("%1:%2").arg(mAccountId).arg(idUrl.remove(0, idUrl.lastIndexOf('/') + 1))); - return guid; -} - -QContactDetail GoogleContactStream::handleEntryContent() -{ - QContactNote note; - QString content = mXmlReader->readElementText(); - note.setNote(content); - return note; -} - -QContactDetail GoogleContactStream::handleEntryBirthday() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gContact:birthday"); - - QContactBirthday birthday; - birthday.setDate(QDate::fromString(mXmlReader->attributes().value("when").toString(), Qt::ISODate)); - return birthday; -} - -QContactDetail GoogleContactStream::handleEntryGender() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gContact:gender"); - - const QString genderStr = mXmlReader->attributes().value("value").toString().toLower(); - QContactGender gender; - if (genderStr.startsWith('m')) { - gender.setGender(QContactGender::GenderMale); - } else if (genderStr.startsWith('f')) { - gender.setGender(QContactGender::GenderFemale); - } else { - gender.setGender(QContactGender::GenderUnspecified); - } - - return gender; -} - -QContactDetail GoogleContactStream::handleEntryHobby() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gContact:hobby"); - - QContactHobby hobby; - hobby.setHobby(mXmlReader->readElementText()); - return hobby; -} - -QContactDetail GoogleContactStream::handleEntryNickname() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gContact:nickname"); - - QContactNickname nickname; - nickname.setNickname(mXmlReader->readElementText()); - return nickname; -} - -QContactDetail GoogleContactStream::handleEntryOccupation() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gContact:occupation"); - - QContactOrganization org; - org.setRole(mXmlReader->readElementText()); - return org; -} - -QContactDetail GoogleContactStream::handleEntryWebsite() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gContact:website"); - - QContactUrl url; - url.setUrl(mXmlReader->attributes().value("href").toString()); - return url; -} - -QContactDetail GoogleContactStream::handleEntryJot() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gContact:jot"); - - QContactNote note; - note.setNote(mXmlReader->readElementText()); - return note; -} - -QContactDetail GoogleContactStream::handleEntryComments() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gd:comments"); - - QContactNote note; - note.setNote(mXmlReader->readElementText()); - return note; -} - -QContactDetail GoogleContactStream::handleEntryEmail() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gd:email"); - - QContactEmailAddress email; - email.setEmailAddress(mXmlReader->attributes().value("address").toString()); - - QString rel = mXmlReader->attributes().hasAttribute("rel") - ? mXmlReader->attributes().value("rel").toString() - : QString(); - - if (rel == QStringLiteral("http://schemas.google.com/g/2005#home")) { - email.setContexts(QContactDetail::ContextHome); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#work")) { - email.setContexts(QContactDetail::ContextWork); - } else if (!rel.isEmpty()) { - email.setContexts(QContactDetail::ContextOther); - } - return email; -} - -QContactDetail GoogleContactStream::handleEntryIm() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gd:im"); - - QContactOnlineAccount onlineAccount; - onlineAccount.setAccountUri(mXmlReader->attributes().value("address").toString()); - //if (mXml->attributes().hasAttribute("protocol")) { - // QString protocolUrl = mXml->attributes().value("protocol").toString(); - // onlineAccount.setProtocol(protocolUrl.right(protocolUrl.lastIndexOf("#"))); - //} - - return onlineAccount; -} - -QContactDetail GoogleContactStream::handleEntryName() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gd:name"); - - QContactName name; - while (!(mXmlReader->tokenType() == QXmlStreamReader::EndElement && mXmlReader->qualifiedName() == "gd:name")) { - if (mXmlReader->tokenType() == QXmlStreamReader::StartElement) { - if (mXmlReader->qualifiedName() == "gd:givenName") { - name.setFirstName(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:additionalName") { - name.setMiddleName(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:familyName") { - name.setLastName(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:namePrefix") { - name.setPrefix(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:nameSuffix") { - name.setSuffix(mXmlReader->readElementText()); - } - } - mXmlReader->readNextStartElement(); - } - - return name; -} - -QContactDetail GoogleContactStream::handleEntryOrganization() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gd:organization"); - - QContactOrganization org; - - while (!(mXmlReader->tokenType() == QXmlStreamReader::EndElement && mXmlReader->qualifiedName() == "gd:organization")) { - if (mXmlReader->tokenType() == QXmlStreamReader::StartElement) { - if (mXmlReader->qualifiedName() == "gd:orgDepartment") { - QStringList dept = org.department(); - dept.append(mXmlReader->readElementText()); - org.setDepartment(dept); - } else if (mXmlReader->qualifiedName() == "gd:orgJobDescription") { - org.setRole(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:orgName") { - org.setName(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:orgSymbol") { - org.setLogoUrl(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:orgTitle") { - org.setTitle(mXmlReader->readElementText()); - } - } - mXmlReader->readNextStartElement(); - } - - return org; -} - -QContactDetail GoogleContactStream::handleEntryPhoneNumber() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gd:phoneNumber"); - - QContactPhoneNumber phone; - QString rel = mXmlReader->attributes().hasAttribute("rel") - ? mXmlReader->attributes().value("rel").toString() - : QString(); - QString number = mXmlReader->readElementText(); - - SOCIALD_LOG_TRACE("received phone type information from Google:" << rel << "for number:" << number); - if (rel == QStringLiteral("http://schemas.google.com/g/2005#home")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeLandline); - phone.setContexts(QContactDetail::ContextHome); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#work")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeLandline); - phone.setContexts(QContactDetail::ContextWork); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#mobile")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); - phone.setContexts(QContactDetail::ContextHome); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#work_mobile")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); - phone.setContexts(QContactDetail::ContextWork); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#home_fax")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeFax); - phone.setContexts(QContactDetail::ContextHome); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#work_fax")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeFax); - phone.setContexts(QContactDetail::ContextWork); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#other_fax")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeFax); - phone.setContexts(QContactDetail::ContextOther); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#pager")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypePager); - phone.setContexts(QContactDetail::ContextHome); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#work_pager")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypePager); - phone.setContexts(QContactDetail::ContextWork); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#tty_tdd")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeModem); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#car")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeCar); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#telex")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeBulletinBoardSystem); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#assistant")) { - phone.setSubTypes(QList() << QContactPhoneNumber::SubTypeAssistant); - } else if (!rel.isEmpty()) { - SOCIALD_LOG_INFO("unhandled phone type information from Google:" << rel << "for number:" << number); - } - - phone.setNumber(number); - return phone; -} - -QContactDetail GoogleContactStream::handleEntryStructuredPostalAddress() -{ - Q_ASSERT(mXmlReader->isStartElement() && mXmlReader->qualifiedName() == "gd:structuredPostalAddress"); - - QContactAddress address; - - QString rel = mXmlReader->attributes().hasAttribute("rel") - ? mXmlReader->attributes().value("rel").toString() - : QString(); - - while (!(mXmlReader->tokenType() == QXmlStreamReader::EndElement && mXmlReader->qualifiedName() == "gd:structuredPostalAddress")) { - if (mXmlReader->tokenType() == QXmlStreamReader::StartElement) { - if (mXmlReader->qualifiedName() == "gd:street") { - address.setStreet(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:pobox") { - address.setPostOfficeBox(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:city") { - address.setLocality(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:region") { - address.setRegion(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:postcode") { - address.setPostcode(mXmlReader->readElementText()); - } else if (mXmlReader->qualifiedName() == "gd:country") { - address.setCountry(mXmlReader->readElementText()); - } - } - mXmlReader->readNextStartElement(); - } - - if (!rel.isEmpty()) { - SOCIALD_LOG_TRACE("received address type information from Google:" << rel << "for address:" << address.street()); - if (rel == QStringLiteral("http://schemas.google.com/g/2005#home")) { - address.setContexts(QContactDetail::ContextHome); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#work")) { - address.setContexts(QContactDetail::ContextWork); - } else if (rel == QStringLiteral("http://schemas.google.com/g/2005#other")) { - address.setContexts(QContactDetail::ContextOther); - } else { - SOCIALD_LOG_INFO("unhandled address type information from Google:" << rel << "for address:" << address.street()); - } - } - - return address; -} - -QContactDetail GoogleContactStream::handleEntryUpdated() -{ - Q_ASSERT(mXmlReader->isStartElement() && - (mXmlReader->qualifiedName() == "updated" || - mXmlReader->qualifiedName() == "app:edited")); - - QDateTime modTs = QDateTime::fromString(mXmlReader->readElementText(), Qt::ISODate); - if (modTs.isValid()) { - QContactTimestamp ts; - ts.setLastModified(modTs); - // We don't actually return a timestamp. - // The qtcontacts-sqlite backend will set one automatically. - } - - return QContactDetail(); -} - -// ---------------------------------------- - -void GoogleContactStream::encodeContactUpdate(const QContact &qContact, - const QStringList &unsupportedElements, - const GoogleContactStream::UpdateType updateType, - const bool batch) -{ - QList allDetails = qContact.details (); - - mXmlWriter->writeStartElement("atom:entry"); - if (batch == true) { - // Etag encoding has to immediately succeed writeStartElement("atom:entry"), - // since etag is an attribute of this element. - encodeEtag(qContact, updateType == GoogleContactStream::Remove || updateType == GoogleContactStream::Modify); - encodeBatchTag(updateType, qContact.id().toString()); - } else { - mXmlWriter->writeAttribute("xmlns:atom", "http://www.w3.org/2005/Atom"); - mXmlWriter->writeAttribute("xmlns:gd", "http://schemas.google.com/g/2005"); - mXmlWriter->writeAttribute("xmlns:gContact", "http://schemas.google.com/contact/2008"); - } - - if (updateType == GoogleContactStream::Remove) { - encodeId(qContact, true); - mXmlWriter->writeEndElement(); - return; - } - - encodeCategory(); - if (updateType == GoogleContactStream::Modify) { - encodeId(qContact, true); - encodeUpdatedTimestamp(qContact); - } - encodeUnknownElements(unsupportedElements); // for an Add, this is just group membership. - - encodeNotes(qContact.details()); - Q_FOREACH (const QContactDetail &detail, allDetails) { - switch(detail.type()) { - case QContactDetail::TypeName: { - encodeName(detail); - } break; - case QContactDetail::TypePhoneNumber: { - encodePhoneNumber(detail); - } break; - case QContactDetail::TypeEmailAddress: { - encodeEmailAddress(detail); - } break; - case QContactDetail::TypeAddress: { - encodeAddress(detail); - } break; - case QContactDetail::TypeUrl: { - encodeUrl(detail); - } break; - case QContactDetail::TypeBirthday: { - encodeBirthday(detail); - } break; - case QContactDetail::TypeNote: { - // ignore, already handled above. - } break; - case QContactDetail::TypeHobby: { - encodeHobby(detail); - } break; - case QContactDetail::TypeOrganization: { - encodeOrganization(detail); - } break; - case QContactDetail::TypeAvatar: { - encodeAvatar(detail, qContact); - } break; - case QContactDetail::TypeAnniversary: { - encodeAnniversary(detail); - } break; - case QContactDetail::TypeNickname: { - encodeNickname(detail); - } break; - case QContactDetail::TypeGender: { - encodeGender(detail); - } break; - case QContactDetail::TypeOnlineAccount: { - encodeOnlineAccount(detail); - } break; - case QContactDetail::TypeFamily: { - encodeFamily(detail); - } break; - // TODO: handle the custom detail fields. - default: { - } break; - } - } - - mXmlWriter->writeEndElement(); -} - -void GoogleContactStream::startBatchFeed() -{ - mXmlWriter->writeStartElement("atom:feed"); - mXmlWriter->writeAttribute("xmlns:atom", "http://www.w3.org/2005/Atom"); - mXmlWriter->writeAttribute("xmlns:gContact", "http://schemas.google.com/contact/2008"); - mXmlWriter->writeAttribute("xmlns:gd", "http://schemas.google.com/g/2005"); - mXmlWriter->writeAttribute("xmlns:batch", "http://schemas.google.com/gdata/batch"); -} - -void GoogleContactStream::endBatchFeed() -{ - mXmlWriter->writeEndElement (); -} - -void GoogleContactStream::encodeBatchTag(const GoogleContactStream::UpdateType type, const QString &batchElementId) -{ - mXmlWriter->writeTextElement("batch:id", batchElementId); - if (type == GoogleContactStream::Add) { - mXmlWriter->writeEmptyElement("batch:operation"); - mXmlWriter->writeAttribute("type", "insert"); - } else if (type == GoogleContactStream::Modify) { - mXmlWriter->writeEmptyElement("batch:operation"); - mXmlWriter->writeAttribute("type", "update"); - } else if (type == GoogleContactStream::Remove) { - mXmlWriter->writeEmptyElement("batch:operation"); - mXmlWriter->writeAttribute("type", "delete"); - } -} - -void GoogleContactStream::encodeCategory() -{ - mXmlWriter->writeEmptyElement("atom:category"); - mXmlWriter->writeAttribute("schema", "http://schemas.google.com/g/2005#kind"); - mXmlWriter->writeAttribute("term", "http://schemas.google.com/contact/2008#contact"); -} - -void GoogleContactStream::encodeId(const QContact &qContact, bool isUpdate) -{ - QString guid = qContact.detail(QContactGuid::Type).value(QContactGuid::FieldGuid).toString(); - if (!guid.isEmpty()) { - QString remoteId = guid.mid(guid.indexOf(":")+1); - if (isUpdate) { - // according to the docs, this should be "base" instead of "full" -- but that actually fails. - if (mAccountEmail.isEmpty()) { - SOCIALD_LOG_ERROR("account email not known - unable to build batch edit id!"); - } else { - mXmlWriter->writeTextElement("atom:id", "http://www.google.com/m8/feeds/contacts/" + mAccountEmail + "/full/" + remoteId); - } - } else { - mXmlWriter->writeTextElement("atom:id", "http://www.google.com/m8/feeds/contacts/default/full/" + remoteId); - } - } -} - -void GoogleContactStream::encodeUpdatedTimestamp(const QContact &qContact) -{ - QContactTimestamp timestamp = qContact.detail(); - QDateTime updatedTimestamp = timestamp.lastModified(); // will be UTC from database. - if (!updatedTimestamp.isValid()) { - updatedTimestamp = QDateTime::currentDateTimeUtc(); - } - - QString updatedStr = QLocale::c().toString(updatedTimestamp, QStringLiteral("yyyy-MM-ddThh:mm:ss.zzzZ")); - mXmlWriter->writeTextElement("updated", updatedStr); -} - -void GoogleContactStream::encodeEtag(const QContact &qContact, bool needed) -{ - QString etag; - for (const QContactExtendedDetail &detail : qContact.details()) { - if (detail.name() == QLatin1String("etag")) { - etag = detail.data().toString(); - break; - } - } - if (!etag.isEmpty()) { - mXmlWriter->writeAttribute("gd:etag", etag); - } else if (needed) { - // we're trying to delete a contact in a batch operation - // but we don't know the etag of the deleted contact. - SOCIALD_LOG_ERROR("etag needed but not available! caller needs to prefill for deletion updates!"); - } -} - -void GoogleContactStream::encodeName(const QContactName &name) -{ - mXmlWriter->writeStartElement("gd:name"); - - const QString firstName = name.firstName(); - const QString lastName = name.lastName(); - - if (!firstName.isEmpty()) - mXmlWriter->writeTextElement("gd:givenName", firstName); - if (!name.middleName().isEmpty()) - mXmlWriter->writeTextElement("gd:additionalName", name.middleName()); - if (!lastName.isEmpty()) - mXmlWriter->writeTextElement("gd:familyName", lastName); - if (!name.prefix().isEmpty()) - mXmlWriter->writeTextElement("gd:namePrefix", name.prefix()); - if (!name.suffix().isEmpty()) - mXmlWriter->writeTextElement("gd:nameSuffix", name.suffix()); - - // Send the formatted fullName to the server. Otherwise if this field is missing, the server - // detects it as a spurious modification. - const QString fullName = (QStringList() - << name.prefix() - << SeasideCache::primaryName(firstName, lastName) - << name.middleName() - << SeasideCache::secondaryName(firstName, lastName) - << name.suffix()).join(' '); - if (!fullName.isEmpty()) - mXmlWriter->writeTextElement("gd:fullName", fullName); - - mXmlWriter->writeEndElement(); -} - -void GoogleContactStream::encodePhoneNumber(const QContactPhoneNumber &phoneNumber) -{ - if (phoneNumber.number().isEmpty()) { - return; - } - - bool isHome = phoneNumber.contexts().contains(QContactDetail::ContextHome); - bool isWork = phoneNumber.contexts().contains(QContactDetail::ContextWork); - int subType = phoneNumber.subTypes().isEmpty() - ? QContactPhoneNumber::SubTypeMobile // default to mobile - : phoneNumber.subTypes().first(); - - QString rel = "http://schemas.google.com/g/2005#"; - switch (subType) { - case QContactPhoneNumber::SubTypeLandline: { - if (isHome) { - rel += QString::fromLatin1("home"); - } else if (isWork) { - rel += QString::fromLatin1("work"); - } else { - rel += QString::fromLatin1("other"); - } - } break; - case QContactPhoneNumber::SubTypeMobile: { - if (isHome) { - rel += QString::fromLatin1("mobile"); - } else if (isWork) { - rel += QString::fromLatin1("work_mobile"); - } else { - rel += QString::fromLatin1("mobile"); // we lose the non-homeness in roundtrip. - } - } break; - case QContactPhoneNumber::SubTypeFax: { - if (isHome) { - rel += QString::fromLatin1("home_fax"); - } else if (isWork) { - rel += QString::fromLatin1("work_fax"); - } else { - rel += QString::fromLatin1("other_fax"); - } - } break; - case QContactPhoneNumber::SubTypePager: { - if (isHome) { - rel += QString::fromLatin1("pager"); - } else if (isWork) { - rel += QString::fromLatin1("work_pager"); - } else { - rel += QString::fromLatin1("pager"); // we lose the non-homeness in roundtrip. - } - } break; - case QContactPhoneNumber::SubTypeModem: { - rel += QString::fromLatin1("tty_tdd"); // we lose context in roundtrip. - } break; - case QContactPhoneNumber::SubTypeCar: { - rel += QString::fromLatin1("car"); // we lose context in roundtrip. - } break; - case QContactPhoneNumber::SubTypeBulletinBoardSystem: { - rel += QString::fromLatin1("telex"); // we lose context in roundtrip. - } break; - case QContactPhoneNumber::SubTypeAssistant: { - rel += QString::fromLatin1("assistant"); - } break; - default: { - rel += QString::fromLatin1("other"); - } break; - } - - mXmlWriter->writeStartElement("gd:phoneNumber"); - mXmlWriter->writeAttribute("rel", rel); - mXmlWriter->writeCharacters(phoneNumber.number()); - mXmlWriter->writeEndElement(); -} - -void GoogleContactStream::encodeEmailAddress(const QContactEmailAddress &emailAddress) -{ - if (!emailAddress.emailAddress().isEmpty()) { - mXmlWriter->writeEmptyElement("gd:email"); - if (emailAddress.contexts().contains(QContactDetail::ContextHome)) { - mXmlWriter->writeAttribute("rel", "http://schemas.google.com/g/2005#home"); - } else if (emailAddress.contexts().contains(QContactDetail::ContextWork)) { - mXmlWriter->writeAttribute("rel", "http://schemas.google.com/g/2005#work"); - } else { - mXmlWriter->writeAttribute("rel", "http://schemas.google.com/g/2005#other"); - } - mXmlWriter->writeAttribute("address", emailAddress.emailAddress()); - } -} - -void GoogleContactStream::encodeAddress(const QContactAddress &address) -{ - mXmlWriter->writeStartElement("gd:structuredPostalAddress"); - - if (address.contexts().contains(QContactDetail::ContextHome)) { - mXmlWriter->writeAttribute("rel", "http://schemas.google.com/g/2005#home"); - } else if (address.contexts().contains(QContactDetail::ContextWork)) { - mXmlWriter->writeAttribute("rel", "http://schemas.google.com/g/2005#work"); - } else { - mXmlWriter->writeAttribute("rel", "http://schemas.google.com/g/2005#other"); - } - - // https://developers.google.com/google-apps/contacts/v3/reference#structuredPostalAddressRestrictions - // we cannot use mailClass attribute (for postal/parcel etc) - if (!address.street().isEmpty()) - mXmlWriter->writeTextElement("gd:street", address.street()); - if (!address.locality().isEmpty()) - mXmlWriter->writeTextElement("gd:city", address.locality()); - if (!address.postOfficeBox().isEmpty()) - mXmlWriter->writeTextElement("gd:pobox", address.postOfficeBox()); - if (!address.region().isEmpty()) - mXmlWriter->writeTextElement("gd:region", address.region()); - if (!address.postcode().isEmpty()) - mXmlWriter->writeTextElement("gd:postcode", address.postcode()); - if (!address.country().isEmpty()) - mXmlWriter->writeTextElement("gd:country", address.country()); - - mXmlWriter->writeEndElement(); -} - -void GoogleContactStream::encodeUrl(const QContactUrl &url) -{ - if (!url.url().isEmpty()) { - mXmlWriter->writeEmptyElement("gContact:website"); - switch (url.subType()) { - case QContactUrl::SubTypeHomePage: { - mXmlWriter->writeAttribute("rel", "home-page"); - } break; - case QContactUrl::SubTypeBlog: { - mXmlWriter->writeAttribute("rel", "blog"); - } break; - default: { - mXmlWriter->writeAttribute("rel", "other"); - } break; - } - mXmlWriter->writeAttribute("href", url.url()); - } -} - -void GoogleContactStream::encodeBirthday(const QContactBirthday &birthday) -{ - if (birthday.date().isValid()) { - mXmlWriter->writeEmptyElement("gContact:birthday"); - mXmlWriter->writeAttribute("when", birthday.date().toString(Qt::ISODate)); - } -} - -void -GoogleContactStream::encodeNotes(const QList ¬es) -{ - // Google Contacts only shows the atom:content data - // in the UI as Notes, while other notes may be stored - // as gContact:jot fields but will not be shown in the UI. - // We cannot store the mapping in order to know which - // note should be stored to which field, so instead, - // we sort the notes alphabetically. - // The first note gets written to atom:content, - // subsequent notes get written to gContact:jot(s). - - QStringList noteStrings; - for (const QContactNote ¬e : notes) { - if (!note.note().trimmed().isEmpty() && !noteStrings.contains(note.note())) { - noteStrings.append(note.note()); - } - } - std::sort(noteStrings.begin(), noteStrings.end()); - - for (int i = 0; i < noteStrings.size(); ++i) { - mXmlWriter->writeStartElement(i == 0 ? QStringLiteral("atom:content") : QStringLiteral("gContact:jot")); - mXmlWriter->writeAttribute(i == 0 ? QStringLiteral("type") : QStringLiteral("rel"), - i == 0 ? QStringLiteral("text") : QStringLiteral("user")); - mXmlWriter->writeCharacters(noteStrings.at(i)); - mXmlWriter->writeEndElement(); - } -} - -void GoogleContactStream::encodeHobby(const QContactHobby &hobby) -{ - if (!hobby.hobby().isEmpty()) { - mXmlWriter->writeTextElement ("gContact:hobby", hobby.hobby()); - } -} - -void GoogleContactStream::encodeGeoLocation(const QContactGeoLocation &geolocation) -{ - Q_UNUSED(geolocation); - SOCIALD_LOG_INFO("skipping geolocation"); -} - -void GoogleContactStream::encodeOrganization(const QContactOrganization &organization) -{ - mXmlWriter->writeStartElement("gd:organization"); - mXmlWriter->writeAttribute("rel", "http://schemas.google.com/g/2005#work"); - if (organization.title().length() > 0) - mXmlWriter->writeTextElement("gd:orgTitle", organization.title()); - if (organization.name().length() > 0) - mXmlWriter->writeTextElement("gd:orgName", organization.name()); - if (organization.department().length() > 0) - mXmlWriter->writeTextElement("gd:orgDepartment", organization.department().join(',')); - mXmlWriter->writeEndElement(); -} - -void GoogleContactStream::encodeAvatar(const QContactAvatar &avatar, const QContact &qContact) -{ - // XXX TODO: determine if it's a new local avatar, if so, push it up. - QUrl imageUrl(avatar.imageUrl()); - if (imageUrl.isLocalFile()) { - SOCIALD_LOG_INFO("have avatar:" << imageUrl << "but not upsyncing avatars"); - mEncodedContactsWithAvatars << qContact.id(); - } -} - -void GoogleContactStream::encodeGender(const QContactGender &gender) -{ - switch(gender.gender()) { - case QContactGender::GenderMale: { - mXmlWriter->writeEmptyElement ("gContact:gender"); - mXmlWriter->writeAttribute ("value", "male"); - } break; - case QContactGender::GenderFemale: { - mXmlWriter->writeEmptyElement ("gContact:gender"); - mXmlWriter->writeAttribute ("value", "female"); - } break; - default: return; - } -} - -void GoogleContactStream::encodeNickname(const QContactNickname &nickname) -{ - if (!nickname.nickname().isEmpty()) { - mXmlWriter->writeTextElement("gContact:nickname", nickname.nickname()); - } -} - -void GoogleContactStream::encodeAnniversary(const QContactAnniversary &anniversary) -{ - if (!anniversary.event().isEmpty() && !anniversary.originalDate().isNull()) { - mXmlWriter->writeStartElement ("gContact:event"); - mXmlWriter->writeAttribute("rel", "anniversary"); - QString label; - switch (anniversary.subType()) { - case QContactAnniversary::SubTypeEngagement: { - label = QString::fromLatin1("engagement"); - } break; - case QContactAnniversary::SubTypeEmployment: { - label = QString::fromLatin1("employment"); - } break; - case QContactAnniversary::SubTypeMemorial: { - label = QString::fromLatin1("memorial"); - } break; - case QContactAnniversary::SubTypeHouse: { - label = QString::fromLatin1("house"); - } break; - case QContactAnniversary::SubTypeWedding: // fall through - default: { - label = QString::fromLatin1("wedding"); - } break; - } - mXmlWriter->writeAttribute("label", label); - mXmlWriter->writeEmptyElement("gd:when"); - mXmlWriter->writeAttribute("startTime", anniversary.originalDate().toString(Qt::ISODate)); - mXmlWriter->writeEndElement(); - } -} - -void GoogleContactStream::encodeOnlineAccount(const QContactOnlineAccount &onlineAccount) -{ - if (onlineAccount.accountUri().isEmpty()) { - return; - } - - QContactOnlineAccount::Protocol protocol = onlineAccount.protocol(); - QString protocolName; - if (protocol == QContactOnlineAccount::ProtocolJabber) { - protocolName = QString::fromLatin1("JABBER"); - } else if (protocol == QContactOnlineAccount::ProtocolAim) { - protocolName = QString::fromLatin1("AIM"); - } else if (protocol == QContactOnlineAccount::ProtocolIcq) { - protocolName = QString::fromLatin1("ICQ"); - } else if (protocol == QContactOnlineAccount::ProtocolMsn) { - protocolName = QString::fromLatin1("MSN"); - } else if (protocol == QContactOnlineAccount::ProtocolQq) { - protocolName = QString::fromLatin1("QQ"); - } else if (protocol == QContactOnlineAccount::ProtocolYahoo) { - protocolName = QString::fromLatin1("YAHOO"); - } else if (protocol == QContactOnlineAccount::ProtocolSkype) { - protocolName = QString::fromLatin1("SKYPE"); - } else { - return; - } - - mXmlWriter->writeEmptyElement ("gd:im"); - mXmlWriter->writeAttribute ("protocol", "http://schemas.google.com/g/2005#" + protocolName); - // FIXME: The 'rel' value should be properly stored and retrieved - mXmlWriter->writeAttribute ("rel", "http://schemas.google.com/g/2005#home"); - mXmlWriter->writeAttribute ("address", onlineAccount.accountUri()); -} - -void GoogleContactStream::encodeFamily(const QContactFamily &family) -{ - if (family.spouse().length() > 0) { - mXmlWriter->writeEmptyElement("gContact:relation"); - mXmlWriter->writeAttribute("rel", "spouse"); - mXmlWriter->writeCharacters(family.spouse()); - } - - Q_FOREACH (const QString member, family.children()) { - mXmlWriter->writeEmptyElement("gContact:relation"); - mXmlWriter->writeAttribute("rel", "child"); - mXmlWriter->writeCharacters(member); - } -} - -void GoogleContactStream::encodeUnknownElements(const QStringList &unknownElements) -{ - // ugly hack to get the QXmlStreamWriter to write a pre-formatted element... - foreach (const QString &unknownElement, unknownElements) { - QString concat; - concat.append(""); - concat.append(""); - concat.append(unknownElement); - concat.append(""); - - QXmlStreamReader tokenizer(concat); - tokenizer.readNextStartElement(); // read past the xml document element start. - QString text = tokenizer.readElementText(); - mXmlWriter->writeStartElement(tokenizer.qualifiedName().toString()); - mXmlWriter->writeAttributes(tokenizer.attributes()); - if (!text.isEmpty()) { - mXmlWriter->writeCharacters(text); - } - mXmlWriter->writeEndElement(); - } -} - diff --git a/src/google/google-contacts/googlecontactstream.h b/src/google/google-contacts/googlecontactstream.h deleted file mode 100644 index 9d7b253..0000000 --- a/src/google/google-contacts/googlecontactstream.h +++ /dev/null @@ -1,174 +0,0 @@ -/**************************************************************************** - ** - ** Copyright (C) 2013-2014 Jolla Ltd. and/or its subsidiary(-ies). - ** Contact: Chris Adams - ** - ** Contributors: Sateesh Kavuri - ** Chris Adams - ** - ** 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 - ** - ****************************************************************************/ - -#ifndef GOOGLECONTACTSTREAM_H -#define GOOGLECONTACTSTREAM_H - -#include "googlecontactatom.h" - -#include -#include - -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -USE_CONTACTS_NAMESPACE - -class GoogleContactStream : public QObject -{ - Q_OBJECT - -public: - enum UpdateType { - Add, - Modify, - Remove - }; - Q_ENUM(UpdateType) - -public: - explicit GoogleContactStream(bool response, int accountId, const QString &accountEmail = QString(), QObject* parent = 0); - ~GoogleContactStream(); - - QByteArray encode(const QMultiMap > &updates); - GoogleContactAtom* parse(const QByteArray &xmlBuffer); - -signals: - void parseDone(bool); - -// Decoding XML stream to QContacts -private: - void initAtomFunctionMap(); - void initResponseFunctionMap(); - void initFunctionMap(); - - // Atom feed elements handler methods - void handleAtomUpdated(); - void handleAtomCategory(); - void handleAtomAuthor(); - void handleAtomOpenSearch(); - void handleAtomLink(); - void handleAtomEntry(); - - // Following are for the response received from the server in case of failures - void handleEntryBatchStatus(GoogleContactAtom::BatchOperationResponse *response); - void handleEntryBatchOperation(GoogleContactAtom::BatchOperationResponse *response); - void handleEntryBatchId(GoogleContactAtom::BatchOperationResponse *response); - - // gContact:xxx schema handler methods - QContactDetail handleEntryContent(); - QContactDetail handleEntryBirthday(); - QContactDetail handleEntryGender(); - QContactDetail handleEntryHobby(); - QContactDetail handleEntryNickname(); - QContactDetail handleEntryOccupation(); - QContactDetail handleEntryWebsite(); - QContactDetail handleEntryJot(); - QContactDetail handleEntryComments(); - QContactDetail handleEntryEmail(); - QContactDetail handleEntryIm(); - QContactDetail handleEntryName(); - QContactDetail handleEntryOrganization(); - QContactDetail handleEntryPhoneNumber(); - QContactDetail handleEntryStructuredPostalAddress(); - QContactDetail handleEntryUpdated(); - - // handle the id specially - QContactDetail handleEntryId(QString *rawId); - - // unknown / unsupported element handler methods - QString handleEntryExtendedProperty(); - QString handleEntryLink(QContactAvatar *avatar, bool *isAvatar); - QString handleEntryUnknownElement(); - - typedef void (GoogleContactStream::*Handler)(); - typedef QContactDetail (GoogleContactStream::*DetailHandler)(); - - QMap mAtomFunctionMap; - QMap mContactFunctionMap; - QXmlStreamReader *mXmlReader; - GoogleContactAtom *mAtom; - int mAccountId; - -// Encoding QContacts to XML stream -private: - void encodeContactUpdate(const QContact &qContact, - const QStringList &unsupportedElements, - const UpdateType updateType, - const bool batch); - void startBatchFeed(); - void endBatchFeed(); - void encodeBatchTag(const UpdateType updateType, const QString &batchElementId); - void encodeId(const QContact &qContact, bool isUpdate = false); - void encodeUpdatedTimestamp(const QContact &qContact); - void encodeEtag(const QContact &qContact, bool needed); - void encodeCategory(); - void encodeName(const QContactName &name); - void encodePhoneNumber(const QContactPhoneNumber &phoneNumber); - void encodeEmailAddress(const QContactEmailAddress &emailAddress); - void encodeAddress(const QContactAddress &address); - void encodeUrl(const QContactUrl &url); - void encodeBirthday(const QContactBirthday &birthday); - void encodeNotes(const QList ¬es); - void encodeHobby(const QContactHobby &hobby); - void encodeGeoLocation(const QContactGeoLocation &geolocation); - void encodeOrganization(const QContactOrganization &organization); - void encodeAvatar(const QContactAvatar &avatar, const QContact &qContact); - void encodeGender(const QContactGender &gender); - void encodeNickname(const QContactNickname &nickname); - void encodeAnniversary(const QContactAnniversary &anniversary); - void encodeOnlineAccount(const QContactOnlineAccount &onlineAccount); - void encodeFamily(const QContactFamily &family); - - void encodeUnknownElements(const QStringList &unknownElements); - - QXmlStreamWriter *mXmlWriter; - QList mEncodedContactsWithAvatars; - QString mAccountEmail; -}; - -#endif // GOOGLECONTACTSTREAM_H diff --git a/src/google/google-contacts/googlepeopleapi.cpp b/src/google/google-contacts/googlepeopleapi.cpp new file mode 100644 index 0000000..7a41aea --- /dev/null +++ b/src/google/google-contacts/googlepeopleapi.cpp @@ -0,0 +1,529 @@ +/**************************************************************************** + ** + ** Copyright (c) 2021 Jolla Ltd. + ** + ** 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 + ** + ****************************************************************************/ + +#include "googlepeopleapi.h" +#include "trace.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +const QString ContentIdCreateContact = QStringLiteral("CreateContact:"); +const QString ContentIdUpdateContact = QStringLiteral("UpdateContact:"); +const QString ContentIdDeleteContact = QStringLiteral("DeleteContact:"); +const QString ContentIdAddContactPhoto = QStringLiteral("AddContactPhoto:"); +const QString ContentIdUpdateContactPhoto = QStringLiteral("UpdateContactPhoto:"); +const QString ContentIdDeleteContactPhoto = QStringLiteral("DeleteContactPhoto:"); +const int MaximumAvatarWidth = 512; + +template +QList jsonArrayToList(const QJsonArray &array) +{ + QList values; + for (auto it = array.constBegin(); it != array.constEnd(); ++it) { + values.append(T::fromJsonObject(it->toObject())); + } + return values; +} + +QJsonObject parseJsonObject(const QByteArray &data) +{ + QJsonParseError err; + + QJsonDocument doc = QJsonDocument::fromJson(data, &err); + if (err.error != QJsonParseError::NoError) { + SOCIALD_LOG_ERROR("JSON parse error:" << err.errorString()); + return QJsonObject(); + } + + return doc.object(); +} + +QFile *newResizedImageFile(const QString &imagePath, int maxWidth) +{ + QImage image; + if (!image.load(imagePath)) { + SOCIALD_LOG_ERROR("Unable to load image file:" << imagePath); + return nullptr; + } + + const QByteArray fileSuffix = QFileInfo(imagePath).suffix().toUtf8(); + if (image.size().width() < maxWidth) { + return nullptr; + } + + QTemporaryFile *temp = new QTemporaryFile; + image = image.scaledToWidth(maxWidth); + temp->setFileTemplate(imagePath); + if (temp->open() && image.save(temp->fileName(), fileSuffix.data())) { + temp->seek(0); + return temp; + } else { + SOCIALD_LOG_ERROR("Unable to save resized image to file:" << temp->fileName()); + } + + delete temp; + return nullptr; +} + +bool writePhotoUpdateBody(QJsonObject *jsonObject, const QContactAvatar &avatar) +{ + if (!avatar.imageUrl().isLocalFile()) { + SOCIALD_LOG_ERROR("Cannot open non-local avatar file:" << avatar.imageUrl()); + return false; + } + + // Reduce the avatar size to minimize the uploaded data. + const QString avatarFileName = avatar.imageUrl().toLocalFile(); + QFile *imageFile = newResizedImageFile(avatarFileName, MaximumAvatarWidth); + if (!imageFile) { + imageFile = new QFile(avatarFileName); + if (!imageFile->open(QFile::ReadOnly)) { + SOCIALD_LOG_ERROR("Unable to open avatar file:" << imageFile->fileName()); + delete imageFile; + return false; + } + } + + jsonObject->insert("photoBytes", QString::fromLatin1(imageFile->readAll().toBase64())); + delete imageFile; + + return true; +} + +QString contentIdForContactOperation(GooglePeopleApi::OperationType operationType, const QContact &contact) +{ + static const QMap contentIdPrefixes = { + { GooglePeopleApi::CreateContact, ContentIdCreateContact }, + { GooglePeopleApi::UpdateContact, ContentIdUpdateContact }, + { GooglePeopleApi::DeleteContact, ContentIdDeleteContact }, + { GooglePeopleApi::AddContactPhoto, ContentIdAddContactPhoto }, + { GooglePeopleApi::UpdateContactPhoto, ContentIdUpdateContactPhoto }, + { GooglePeopleApi::DeleteContactPhoto, ContentIdDeleteContactPhoto }, + }; + + const QString idPrefix = contentIdPrefixes.value(operationType); + if (idPrefix.isEmpty()) { + SOCIALD_LOG_ERROR("contentIdForOperationType(): invalid operation type!"); + return QString(); + } + + return QString("Content-ID: %1%2\n").arg(idPrefix).arg(contact.id().toString()); +} + +void addPartHeaderForContactOperation(QByteArray *bytes, GooglePeopleApi::OperationType operationType, const QContact &contact) +{ + bytes->append("\n" + "--batch_people\n" + "Content-Type: application/http\n" + "Content-Transfer-Encoding: binary\n"); + bytes->append(contentIdForContactOperation(operationType, contact).toUtf8()); + bytes->append("\n"); +} + +} + +GooglePeopleApiRequest::GooglePeopleApiRequest(const QString &accessToken) + : m_accessToken(accessToken) +{ +} + +GooglePeopleApiRequest::~GooglePeopleApiRequest() +{ +} + +QByteArray GooglePeopleApiRequest::writeMultiPartRequest(const QMap > &batch) +{ + QByteArray bytes; + bool hasContent = false; + static const QString supportedPersonFieldList = GooglePeople::Person::supportedPersonFields().join(','); + + // Encode each multi-part request into the overall request. + // Each part contains a Content-ID that indicates the request type, to assist in parsing the + // response when it is received. + + for (auto it = batch.constBegin(); it != batch.constEnd(); ++it) { + switch (it.key()) { + case GooglePeopleApi::UnsupportedOperation: + SOCIALD_LOG_ERROR("Invalid operation type in multi-part batch"); + break; + case GooglePeopleApi::CreateContact: + { + for (const QContact &contact : it.value()) { + const QJsonObject jsonObject = GooglePeople::Person::contactToJsonObject(contact); + if (jsonObject.isEmpty()) { + SOCIALD_LOG_ERROR("No contact data found for contact:" << contact.id()); + } else { + addPartHeaderForContactOperation(&bytes, it.key(), contact); + + const QByteArray body = "\n" + QJsonDocument(jsonObject).toJson(); + bytes += QString("POST /v1/people:createContact?personFields=%1 HTTP/1.1\n") + .arg(supportedPersonFieldList); + bytes += "Content-Type: application/json\n"; + bytes += QString("Content-Length: %1\n").arg(body.size()).toLatin1(); + bytes += "Accept: application/json\n"; + bytes += body; + bytes += "\n"; + hasContent = true; + } + } + break; + } + case GooglePeopleApi::UpdateContact: + { + for (const QContact &contact : it.value()) { + QStringList updatedPersonFieldList; + const QJsonObject jsonObject = GooglePeople::Person::contactToJsonObject( + contact, &updatedPersonFieldList); + if (updatedPersonFieldList.isEmpty()) { + SOCIALD_LOG_INFO("No non-avatar fields have changed in contact:" << contact.id()); + } else if (jsonObject.isEmpty()) { + SOCIALD_LOG_ERROR("No contact data found for contact:" << contact.id()); + } else { + addPartHeaderForContactOperation(&bytes, it.key(), contact); + + const QByteArray body = "\n" + QJsonDocument(jsonObject).toJson(); + bytes += QString("PATCH /v1/%1:updateContact?updatePersonFields=%2&personFields=%3 HTTP/1.1\n") + .arg(GooglePeople::Person::personResourceName(contact)) + .arg(updatedPersonFieldList.join(',')) + .arg(supportedPersonFieldList); + bytes += "Content-Type: application/json\n"; + bytes += QString("Content-Length: %1\n").arg(body.size()).toLatin1(); + bytes += "Accept: application/json\n"; + bytes += body; + bytes += "\n"; + hasContent = true; + } + } + break; + } + case GooglePeopleApi::DeleteContact: + { + for (const QContact &contact : it.value()) { + addPartHeaderForContactOperation(&bytes, it.key(), contact); + + bytes += QString("DELETE /v1/%1:deleteContact HTTP/1.1\n") + .arg(GooglePeople::Person::personResourceName(contact)); + bytes += "Content-Type: application/json\n"; + bytes += "Accept: application/json\n"; + bytes += "\n"; + hasContent = true; + } + break; + } + case GooglePeopleApi::AddContactPhoto: + case GooglePeopleApi::UpdateContactPhoto: + { + for (const QContact &contact : it.value()) { + const QContactAvatar avatar = GooglePeople::Photo::getPrimaryPhoto(contact); + if (avatar.imageUrl().isEmpty()) { + SOCIALD_LOG_ERROR("No avatar found in contact:" << contact); + continue; + } + QJsonObject jsonObject; + if (!writePhotoUpdateBody(&jsonObject, avatar)) { + SOCIALD_LOG_ERROR("Failed to write avatar update details:" << avatar.imageUrl()); + continue; + } + jsonObject.insert("personFields", supportedPersonFieldList); + + addPartHeaderForContactOperation(&bytes, it.key(), contact); + + const QByteArray body = "\n" + QJsonDocument(jsonObject).toJson(); + bytes += QString("PATCH /v1/%1:updateContactPhoto HTTP/1.1\n") + .arg(GooglePeople::Person::personResourceName(contact)); + bytes += "Content-Type: application/json\n"; + bytes += QString("Content-Length: %1\n").arg(body.size()).toLatin1(); + bytes += "Accept: application/json\n"; + bytes += body; + bytes += "\n"; + hasContent = true; + } + break; + } + case GooglePeopleApi::DeleteContactPhoto: + { + for (const QContact &contact : it.value()) { + addPartHeaderForContactOperation(&bytes, it.key(), contact); + + bytes += QString("DELETE /v1/%1:deleteContactPhoto?personFields=%2 HTTP/1.1\n") + .arg(GooglePeople::Person::personResourceName(contact)) + .arg(supportedPersonFieldList); + bytes += QString("Content-ID: %1%2\n") + .arg(ContentIdDeleteContactPhoto) + .arg(contact.id().toString()).toUtf8(); + bytes += "Accept: application/json\n"; + bytes += "\n"; + hasContent = true; + } + break; + } + } + } + + if (!hasContent) { + return QByteArray(); + } + + bytes += "--batch_people--\n\n"; + + return bytes; +} + +//----------- + +void GooglePeopleApiResponse::BatchResponsePart::reset() +{ + contentType.clear(); + contentId.clear(); + bodyStatusLine.clear(); + bodyContentType.clear(); + body.clear(); +} + +void GooglePeopleApiResponse::BatchResponsePart::parse( + GooglePeopleApi::OperationType *operationType, + QString *contactId, + GooglePeople::Person *person, + Error *error) const +{ + static const QString responseToken = QStringLiteral("response-"); + if (!responseToken.startsWith(responseToken)) { + SOCIALD_LOG_ERROR("Unexpected content ID in response:" << contentId); + return; + } + const QString operationInfo = contentId.mid(responseToken.length()); + static const QMap operationTypes = { + { ContentIdCreateContact, GooglePeopleApi::CreateContact }, + { ContentIdUpdateContact, GooglePeopleApi::UpdateContact }, + { ContentIdDeleteContact, GooglePeopleApi::DeleteContact }, + { ContentIdAddContactPhoto, GooglePeopleApi::AddContactPhoto }, + { ContentIdUpdateContactPhoto, GooglePeopleApi::UpdateContactPhoto }, + { ContentIdDeleteContactPhoto, GooglePeopleApi::DeleteContactPhoto }, + }; + + *operationType = GooglePeopleApi::UnsupportedOperation; + for (auto it = operationTypes.constBegin(); it != operationTypes.constEnd(); ++it) { + if (operationInfo.startsWith(it.key())) { + *operationType = it.value(); + *contactId = operationInfo.mid(it.key().length()); + break; + } + } + + const QJsonObject jsonBody = parseJsonObject(body); + const QJsonObject errorObject = jsonBody.value("error").toObject(); + if (!errorObject.isEmpty()) { + error->code = errorObject.value("code").toInt(); + error->message = errorObject.value("message").toString(); + error->status = errorObject.value("status").toString(); + } else { + switch (*operationType) { + case GooglePeopleApi::CreateContact: + case GooglePeopleApi::UpdateContact: + // The JSON response is a Person object + *person = GooglePeople::Person::fromJsonObject(jsonBody); + break; + case GooglePeopleApi::AddContactPhoto: + case GooglePeopleApi::UpdateContactPhoto: + case GooglePeopleApi::DeleteContactPhoto: + { + // The JSON response contains a "person" value that is a Person object + *person = GooglePeople::Person::fromJsonObject(jsonBody.value("person").toObject()); + break; + } + case GooglePeopleApi::DeleteContact: + // JSON response is empty. + break; + case GooglePeopleApi::UnsupportedOperation: + break; + } + } +} + +//----------- + +void GooglePeopleApiResponse::PeopleConnectionsListResponse::getContacts( + int accountId, + const QList &candidateCollections, + QList *addedOrModified, + QList *deleted) const +{ + for (const GooglePeople::Person &person : connections) { + if (person.metadata.deleted) { + if (deleted) { + deleted->append(person.toContact(accountId, candidateCollections)); + } + } else if (addedOrModified) { + addedOrModified->append(person.toContact(accountId, candidateCollections)); + } + } +} + +bool GooglePeopleApiResponse::readResponse( + const QByteArray &data, GooglePeopleApiResponse::ContactGroupsResponse *response) +{ + if (!response) { + return false; + } + + const QJsonObject object = parseJsonObject(data); + response->contactGroups = jsonArrayToList(object.value("contactGroups").toArray()); + response->totalItems = object.value("totalItems").toString().toInt(); + response->nextPageToken = object.value("nextPageToken").toString(); + response->nextSyncToken = object.value("nextSyncToken").toString(); + + return true; +} + +bool GooglePeopleApiResponse::readResponse( + const QByteArray &data, GooglePeopleApiResponse::PeopleConnectionsListResponse *response) +{ + if (!response) { + return false; + } + + const QJsonObject object = parseJsonObject(data); + response->connections = jsonArrayToList(object.value("connections").toArray()); + response->nextPageToken = object.value("nextPageToken").toString(); + response->nextSyncToken = object.value("nextSyncToken").toString(); + response->totalPeople = object.value("totalPeople").toString().toInt(); + response->totalItems = object.value("totalItems").toString().toInt(); + + return true; +} + +bool GooglePeopleApiResponse::readMultiPartResponse( + const QByteArray &data, QList *responseParts) +{ + if (!responseParts) { + return false; + } + + QBuffer buffer; + buffer.setData(data); + if (!buffer.open(QIODevice::ReadOnly)) { + return false; + } + + /* + Example multi-part response body: + + --batch_izedEXuDWnLH5_41NeoKptxfL5sqA2K6 + Content-Type: application/http + Content-ID: response- + + HTTP/1.1 200 OK + Content-Type: application/json; charset=UTF-8 + Vary: Origin + Vary: X-Origin + Vary: Referer + + { + // json body of created contact + } + + --batch_izedEXuDWnLH5_41NeoKptxfL5sqA2K6 + Content-Type: application/http + Content-ID: response- + + HTTP/1.1 400 Bad Request + Vary: Origin + Vary: X-Origin + Vary: Referer + Content-Type: application/json; charset=UTF-8 + + { + "error": { + "code": 400, + "message": "Request person.etag is different than the current person.etag. Clear local cache and get the latest person.", + "status": "FAILED_PRECONDITION" + } + } + + --batch_izedEXuDWnLH5_41NeoKptxfL5sqA2K6-- + */ + + enum PartParseStatus { + ParseHeaders, + ParseBodyHeaders, + ParseBody + }; + + BatchResponsePart currentPart; + PartParseStatus parseStatus = ParseHeaders; + + static const QByteArray contentTypeToken = "Content-Type:"; + static const QByteArray contentIdToken = "Content-ID:"; + + while (!buffer.atEnd()) { + const QByteArray line = buffer.readLine(); + const bool isSeparator = line.startsWith("--batch_"); + + if (parseStatus == ParseHeaders) { + // Parse the headers for this part. + if (isSeparator) { + continue; + } else if (line.startsWith(contentTypeToken)) { + currentPart.contentType = QString::fromUtf8(line.mid(contentTypeToken.length() + 1).trimmed()); + } else if (line.startsWith(contentIdToken)) { + currentPart.contentId = QString::fromUtf8(line.mid(contentIdToken.length() + 1).trimmed()); + } else if (line.trimmed().isEmpty() && !currentPart.contentType.isEmpty()) { + parseStatus = ParseBodyHeaders; + } + } else if (parseStatus == ParseBodyHeaders) { + // Parse the body of this part, which itself contains a separate HTTP response with + // headers and body. + if (line.startsWith("HTTP/")) { + currentPart.bodyStatusLine = line.trimmed(); + } else if (line.startsWith(contentTypeToken)) { + currentPart.bodyContentType = QString::fromUtf8(line.mid(contentTypeToken.length() + 1).trimmed()); + } else if (line.trimmed().isEmpty() && !currentPart.bodyContentType.isEmpty()) { + parseStatus = ParseBody; + } + } else if (parseStatus == ParseBody) { + if (isSeparator) { + // This is the start of another part, or the end of the batch. + responseParts->append(currentPart); + + currentPart.reset(); + parseStatus = ParseHeaders; + + if (line.endsWith("--")) { + break; + } + } else { + currentPart.body += line; + } + } + } + + return true; +} diff --git a/src/google/google-contacts/googlepeopleapi.h b/src/google/google-contacts/googlepeopleapi.h new file mode 100644 index 0000000..98efe92 --- /dev/null +++ b/src/google/google-contacts/googlepeopleapi.h @@ -0,0 +1,114 @@ +/**************************************************************************** + ** + ** Copyright (c) 2021 Jolla Ltd. + ** + ** 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 + ** + ****************************************************************************/ + +#ifndef GOOGLEPEOPLEAPI_H +#define GOOGLEPEOPLEAPI_H + +#include "googlepeoplejson.h" + +#include +#include + +QTCONTACTS_USE_NAMESPACE + +namespace GooglePeopleApi +{ + enum OperationType { + UnsupportedOperation, + CreateContact, + UpdateContact, + DeleteContact, + AddContactPhoto, + UpdateContactPhoto, + DeleteContactPhoto + }; +} + +class GooglePeopleApiRequest +{ +public: + GooglePeopleApiRequest(const QString &accessToken); + ~GooglePeopleApiRequest(); + + static QByteArray writeMultiPartRequest(const QMap > &batch); + + +private: + QString m_accessToken; +}; + + +class GooglePeopleApiResponse +{ +public: + class PeopleConnectionsListResponse + { + public: + QList connections; + QString nextPageToken; + QString nextSyncToken; + int totalPeople = 0; + int totalItems = 0; + + void getContacts(int accountId, + const QList &candidateCollections, + QList *addedOrModified, + QList *deleted) const; + }; + + class ContactGroupsResponse + { + public: + // Note: for this response, memberResourceNames of each group are not populated + QList contactGroups; + int totalItems = 0; + QString nextPageToken; + QString nextSyncToken; + }; + + class BatchResponsePart + { + public: + struct Error { + int code; + QString message; + QString status; + }; + + QString contentType; + QString contentId; + QString bodyStatusLine; + QString bodyContentType; + QByteArray body; + + void reset(); + + void parse(GooglePeopleApi::OperationType *operationType, + QString *contactId, + GooglePeople::Person *person, + Error *error) const; + }; + + static bool readResponse(const QByteArray &data, ContactGroupsResponse *response); + static bool readResponse(const QByteArray &data, PeopleConnectionsListResponse *response); + static bool readMultiPartResponse(const QByteArray &data, QList *responseParts); +}; + +#endif // GOOGLEPEOPLEAPI_H diff --git a/src/google/google-contacts/googlepeoplejson.cpp b/src/google/google-contacts/googlepeoplejson.cpp new file mode 100644 index 0000000..496a1c4 --- /dev/null +++ b/src/google/google-contacts/googlepeoplejson.cpp @@ -0,0 +1,1419 @@ +/**************************************************************************** + ** + ** Copyright (c) 2021 Jolla Ltd. + ** + ** 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 + ** + ****************************************************************************/ + +#include "googlepeoplejson.h" +#include "googlecontactimagedownloader.h" +#include "trace.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + + +namespace { + +QDate jsonObjectToDate(const QJsonObject &object) +{ + const int year = object.value("year").toInt(); + const int month = object.value("month").toInt(); + const int day = object.value("day").toInt(); + + QDate date(year, month, day); + if (!date.isValid()) { + SOCIALD_LOG_ERROR("Cannot read date from JSON:" << object); + } + return date; +} + +QJsonObject jsonObjectFromDate(const QDate &date) +{ + QJsonObject object; + if (date.isValid()) { + object.insert("year", date.year()); + object.insert("month", date.month()); + object.insert("day", date.day()); + } + return object; +} + +template +QList jsonArrayToList(const QJsonArray &array) +{ + QList values; + for (auto it = array.constBegin(); it != array.constEnd(); ++it) { + values.append(T::fromJsonObject(it->toObject())); + } + return values; +} + +template +void addJsonValuesForContact(const QString &propertyName, + const QContact &contact, + QJsonObject *object, + QStringList *addedFields) +{ + bool hasChanges = false; + const QJsonArray array = T::jsonValuesForContact(contact, &hasChanges); + if (!hasChanges) { + return; + } + + object->insert(propertyName, array); + if (addedFields) { + addedFields->append(propertyName); + } +} + +bool saveContactExtendedDetail(QContact *contact, const QString &detailName, const QVariant &detailData) +{ + QContactExtendedDetail matchedDetail; + for (const QContactExtendedDetail &detail : contact->details()) { + if (detail.name() == detailName) { + matchedDetail = detail; + break; + } + } + + if (matchedDetail.name().isEmpty()) { + matchedDetail.setName(detailName); + } + matchedDetail.setData(detailData); + return contact->saveDetail(&matchedDetail, QContact::IgnoreAccessConstraints); +} + +QVariant contactExtendedDetail(const QContact &contact, const QString &detailName) +{ + for (const QContactExtendedDetail &detail : contact.details()) { + if (detail.name() == detailName) { + return detail.data(); + } + } + return QVariant(); +} + +bool saveContactDetail(QContact *contact, QContactDetail *detail) +{ + detail->setValue(QContactDetail__FieldModifiable, true); + return contact->saveDetail(detail, QContact::IgnoreAccessConstraints); +} + +template +bool removeDetails(QContact *contact) +{ + QList details = contact->details(); + for (int i = 0; i < details.count(); ++i) { + T *detail = &details[i]; + if (!contact->removeDetail(detail)) { + SOCIALD_LOG_ERROR("Unable to remove detail:" << detail); + return false; + } + } + return true; +} + +bool shouldAddDetailChanges(const QContactDetail &detail, bool *hasChanges) +{ + const int changeFlags = detail.value(QContactDetail__FieldChangeFlags).toInt(); + if (changeFlags == 0) { + return false; + } + + *hasChanges = true; + + if (changeFlags & QContactDetail__ChangeFlag_IsDeleted) { + return false; + } + + // Detail was added or modified + return true; +} + +} + +GooglePeople::Source GooglePeople::Source::fromJsonObject(const QJsonObject &object) +{ + Source ret; + ret.type = object.value("type").toString(); + ret.id = object.value("id").toString(); + ret.etag = object.value("etag").toString(); + return ret; +} + +GooglePeople::FieldMetadata GooglePeople::FieldMetadata::fromJsonObject(const QJsonObject &object) +{ + FieldMetadata ret; + ret.primary = object.value("primary").toBool(); + ret.verified = object.value("verified").toBool(); + ret.source = Source::fromJsonObject(object.value("source").toObject()); + return ret; +} + +bool GooglePeople::Address::saveContactDetails(QContact *contact, const QList
&values) +{ + removeDetails(contact); + + for (const Address &address : values) { + QList contexts; + if (address.type == QStringLiteral("home")) { + contexts.append(QContactDetail::ContextHome); + } else if (address.type == QStringLiteral("work")) { + contexts.append(QContactDetail::ContextWork); + } else if (address.type == QStringLiteral("other")) { + contexts.append(QContactDetail::ContextOther); + } else { + // address.type is a custom type, so ignore it. If the user does not change it to a + // known type, the type will not be upsynced and the custom type will be preserved. + } + + QContactAddress detail; + if (!contexts.isEmpty()) { + detail.setContexts(contexts); + } + detail.setStreet(address.streetAddress); + detail.setPostOfficeBox(address.poBox); + detail.setLocality(address.city); + detail.setRegion(address.region); + detail.setPostcode(address.postalCode); + detail.setCountry(address.country); + + if (!saveContactDetail(contact, &detail)) { + return false; + } + } + + return true; +} + +GooglePeople::Address GooglePeople::Address::fromJsonObject(const QJsonObject &obj) +{ + Address ret; + ret.metadata = FieldMetadata::fromJsonObject(obj.value("metadata").toObject()); + ret.formattedValue = obj.value("formattedValue").toString(); + ret.type = obj.value("type").toString(); + ret.formattedType = obj.value("formattedType").toString(); + ret.poBox = obj.value("poBox").toString(); + ret.streetAddress = obj.value("streetAddress").toString(); + ret.extendedAddress = obj.value("extendedAddress").toString(); + ret.city = obj.value("city").toString(); + ret.region = obj.value("region").toString(); + ret.postalCode = obj.value("postalCode").toString(); + ret.country = obj.value("country").toString(); + ret.countryCode = obj.value("countryCode").toString(); + return ret; +} + +QJsonArray GooglePeople::Address::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + QJsonArray array; + const QList details = contact.details(); + + for (int i = 0; i < details.count(); ++i) { + const QContactAddress &detail = details.at(i); + if (!shouldAddDetailChanges(detail, hasChanges)) { + continue; + } + + const int context = detail.contexts().value(0, -1); + QString type; + switch (context) { + case QContactDetail::ContextHome: + type = QStringLiteral("home"); + break; + case QContactDetail::ContextWork: + type = QStringLiteral("work"); + break; + case QContactDetail::ContextOther: + type = QStringLiteral("other"); + break; + } + + QJsonObject address; + if (type.isEmpty()) { + // No type set, or the Google field had a custom type set, so don't overwrite it. + } else { + address.insert("type", type); + } + address.insert("poBox", detail.postOfficeBox()); + address.insert("streetAddress", detail.street()); + address.insert("city", detail.locality()); + address.insert("region", detail.region()); + address.insert("postalCode", detail.postcode()); + address.insert("country", detail.country()); + array.append(address); + } + + return array; +} + +bool GooglePeople::Biography::saveContactDetails(QContact *contact, const QList &values) +{ + // Only one biography allowed in a Google contact. + if (values.isEmpty()) { + return true; + } + + QContactNote detail = contact->detail(); + detail.setNote(values.at(0).value); + return saveContactDetail(contact, &detail); +} + +GooglePeople::Biography GooglePeople::Biography::fromJsonObject(const QJsonObject &object) +{ + Biography ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.value = object.value("value").toString(); + return ret; +} + +QJsonArray GooglePeople::Biography::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + // Only one biography allowed in a Google contact. + QJsonArray array; + const QContactNote &detail = contact.detail(); + if (!shouldAddDetailChanges(detail, hasChanges)) { + return array; + } + + QJsonObject note; + note.insert("value", detail.note()); + array.append(note); + + return array; +} + +bool GooglePeople::Birthday::saveContactDetails(QContact *contact, const QList &values) +{ + // Only one birthday allowed in a Google contact. + if (values.isEmpty()) { + return true; + } + + QContactBirthday detail = contact->detail(); + detail.setDate(values.at(0).date); + return saveContactDetail(contact, &detail); +} + +GooglePeople::Birthday GooglePeople::Birthday::fromJsonObject(const QJsonObject &object) +{ + Birthday ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.date = jsonObjectToDate(object.value("date").toObject()); + return ret; +} + +QJsonArray GooglePeople::Birthday::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + // Only one birthday allowed in a Google contact. + QJsonArray array; + const QContactBirthday &detail = contact.detail(); + if (!shouldAddDetailChanges(detail, hasChanges)) { + return array; + } + + QJsonObject birthday; + birthday.insert("date", jsonObjectFromDate(detail.date())); + array.append(birthday); + return array; +} + +bool GooglePeople::EmailAddress::saveContactDetails(QContact *contact, const QList &values) +{ + removeDetails(contact); + + QStringList types; + for (const EmailAddress &emailAddress : values) { + QList contexts; + if (emailAddress.type == QStringLiteral("home")) { + contexts.append(QContactDetail::ContextHome); + } else if (emailAddress.type == QStringLiteral("work")) { + contexts.append(QContactDetail::ContextWork); + } else if (emailAddress.type == QStringLiteral("other")) { + contexts.append(QContactDetail::ContextOther); + } else { + // emailAddress.type is a custom type, so ignore it. If the user does not change it to a + // known type, the type will not be upsynced and the custom type will be preserved. + } + + QContactEmailAddress detail; + if (!contexts.isEmpty()) { + detail.setContexts(contexts); + } + detail.setEmailAddress(emailAddress.value); + if (!saveContactDetail(contact, &detail)) { + return false; + } + + types.append(emailAddress.type); + } + + return true; +} + +GooglePeople::EmailAddress GooglePeople::EmailAddress::fromJsonObject(const QJsonObject &object) +{ + EmailAddress ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.value = object.value("value").toString(); + ret.type = object.value("type").toString(); + ret.formattedType = object.value("formattedType").toString(); + ret.displayName = object.value("displayName").toString(); + return ret; +} + +QJsonArray GooglePeople::EmailAddress::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + QJsonArray array; + const QList details = contact.details(); + + for (int i = 0; i < details.count(); ++i) { + const QContactEmailAddress &detail = details.at(i); + if (!shouldAddDetailChanges(detail, hasChanges)) { + continue; + } + + const int context = detail.contexts().value(0, -1); + QString type; + switch (context) { + case QContactDetail::ContextHome: + type = QStringLiteral("home"); + break; + case QContactDetail::ContextWork: + type = QStringLiteral("work"); + break; + case QContactDetail::ContextOther: + type = QStringLiteral("other"); + break; + } + + QJsonObject email; + if (type.isEmpty()) { + // No type set, or the Google field had a custom type set, so don't overwrite it. + } else { + email.insert("type", type); + } + email.insert("value", detail.emailAddress()); + array.append(email); + } + return array; +} + + +bool GooglePeople::Event::saveContactDetails(QContact *contact, const QList &values) +{ + removeDetails(contact); + + for (const Event &event : values) { + QContactAnniversary detail; + detail.setOriginalDateTime(QDateTime(event.date)); + if (event.type == QStringLiteral("Wedding")) { + detail.setSubType(QContactAnniversary::SubTypeWedding); + } else if (event.type == QStringLiteral("Engagement")) { + detail.setSubType(QContactAnniversary::SubTypeEngagement); + } else if (event.type == QStringLiteral("House")) { + detail.setSubType(QContactAnniversary::SubTypeHouse); + } else if (event.type == QStringLiteral("Employment")) { + detail.setSubType(QContactAnniversary::SubTypeEmployment); + } else if (event.type == QStringLiteral("Memorial")) { + detail.setSubType(QContactAnniversary::SubTypeMemorial); + } else { + // event.type is a custom type, so ignore it. If the user does not change it to a + // known type, the type will not be upsynced and the custom type will be preserved. + } + + if (!saveContactDetail(contact, &detail)) { + return false; + } + } + + return true; +} + +GooglePeople::Event GooglePeople::Event::fromJsonObject(const QJsonObject &object) +{ + Event ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.date = jsonObjectToDate(object.value("date").toObject()); + ret.type = object.value("type").toString(); + return ret; +} + +QJsonArray GooglePeople::Event::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + QJsonArray array; + const QList details = contact.details(); + + for (int i = 0; i < details.count(); ++i) { + const QContactAnniversary &detail = details.at(i); + if (!shouldAddDetailChanges(detail, hasChanges)) { + continue; + } + + QString type; + switch (detail.subType()) { + case QContactAnniversary::SubTypeWedding: + type = QStringLiteral("Wedding"); + break; + case QContactAnniversary::SubTypeEngagement: + type = QStringLiteral("Engagement"); + break; + case QContactAnniversary::SubTypeHouse: + type = QStringLiteral("House"); + break; + case QContactAnniversary::SubTypeEmployment: + type = QStringLiteral("Employment"); + break; + case QContactAnniversary::SubTypeMemorial: + type = QStringLiteral("Memorial"); + break; + default: + break; + } + + QJsonObject event; + if (type.isEmpty()) { + // No type set, or the Google field had a custom type set, so don't overwrite it. + } else { + event.insert("type", type); + } + event.insert("date", jsonObjectFromDate(detail.originalDateTime().date())); + array.append(event); + } + + return array; +} + +bool GooglePeople::Membership::matchesCollection(const QContactCollection &collection, int accountId) const +{ + return collection.extendedMetaData(QStringLiteral("resourceName")).toString() == contactGroupResourceName + && collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() == accountId; +} + +bool GooglePeople::Membership::saveContactDetails( + QContact *contact, + const QList &values, + int accountId, + const QList &candidateCollections) +{ + contact->setCollectionId(QContactCollectionId()); + + QStringList contactGroupResourceNames; + for (const Membership &membership : values) { + if (contact->collectionId().isNull()) { + for (const QContactCollection &collection : candidateCollections) { + if (membership.matchesCollection(collection, accountId)) { + contact->setCollectionId(collection.id()); + break; + } + } + } + + contactGroupResourceNames.append(membership.contactGroupResourceName); + } + + // Preserve contactGroupResourceName values since a Person can belong to multiple contact + // groups but a QContact can only belong to one collection. + if (!saveContactExtendedDetail(contact, QStringLiteral("contactGroupResourceNames"), contactGroupResourceNames)) { + return false; + } + + return true; +} + +GooglePeople::Membership GooglePeople::Membership::fromJsonObject(const QJsonObject &object) +{ + Membership ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + + const QJsonObject contactGroupMembership = object.value("contactGroupMembership").toObject(); + ret.contactGroupResourceName = contactGroupMembership.value("contactGroupResourceName").toString(); + + return ret; +} + +QJsonArray GooglePeople::Membership::jsonValuesForContact(const QContact &contact) +{ + QJsonArray array; + QStringList contactGroupResourceNames = contactExtendedDetail( + contact, QStringLiteral("contactGroupResourceNames")).toStringList(); + + for (const QString &contactGroupResourceName : contactGroupResourceNames) { + QJsonObject membership; + // Add the nested contactGroupMembership object. Don't need to add "contactGroupId" + // property as that is deprecated. + QJsonObject contactGroupMembershipObject; + contactGroupMembershipObject.insert("contactGroupResourceName", contactGroupResourceName); + membership.insert("contactGroupMembership", contactGroupMembershipObject); + + array.append(membership); + } + return array; +} + +bool GooglePeople::Name::saveContactDetails(QContact *contact, const QList &values) +{ + // Only one name allowed in a Google contact. + if (values.isEmpty()) { + return true; + } + + const Name &name = values.at(0); + QContactName detail = contact->detail(); + detail.setFirstName(name.givenName); + detail.setMiddleName(name.middleName); + detail.setLastName(name.familyName); + return saveContactDetail(contact, &detail); +} + +GooglePeople::Name GooglePeople::Name::fromJsonObject(const QJsonObject &object) +{ + Name ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.familyName = object.value("familyName").toString(); + ret.givenName = object.value("givenName").toString(); + ret.middleName = object.value("middleName").toString(); + return ret; +} + +QJsonArray GooglePeople::Name::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + // Only one name allowed in a Google contact. + QJsonArray array; + const QContactName &detail = contact.detail(); + if (!shouldAddDetailChanges(detail, hasChanges)) { + return array; + } + + QJsonObject name; + name.insert("familyName", detail.lastName()); + name.insert("givenName", detail.firstName()); + name.insert("middleName", detail.middleName()); + name.insert("honorificPrefix", detail.prefix()); + name.insert("honorificSuffix", detail.suffix()); + array.append(name); + + return array; +} + +bool GooglePeople::Nickname::saveContactDetails(QContact *contact, const QList &values) +{ + removeDetails(contact); + + for (const Nickname &nickName : values) { + QContactNickname detail; + detail.setNickname(nickName.value); + if (!saveContactDetail(contact, &detail)) { + return false; + } + } + + return true; +} + +GooglePeople::Nickname GooglePeople::Nickname::fromJsonObject(const QJsonObject &object) +{ + Nickname ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.value = object.value("value").toString(); + return ret; +} + +QJsonArray GooglePeople::Nickname::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + QJsonArray array; + const QList details = contact.details(); + + for (const QContactNickname &detail : details) { + if (!shouldAddDetailChanges(detail, hasChanges)) { + continue; + } + QJsonObject nickName; + nickName.insert("value", detail.nickname()); + array.append(nickName); + } + return array; +} + +bool GooglePeople::Organization::saveContactDetails(QContact *contact, const QList &values) +{ + removeDetails(contact); + + for (const Organization &organization : values) { + QContactOrganization detail; + detail.setName(organization.name); + detail.setTitle(organization.title); + detail.setRole(organization.jobDescription); + detail.setDepartment(QStringList(organization.department)); + if (!saveContactDetail(contact, &detail)) { + return false; + } + } + + return true; +} + +GooglePeople::Organization GooglePeople::Organization::fromJsonObject(const QJsonObject &object) +{ + Organization ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.name = object.value("name").toString(); + ret.title = object.value("title").toString(); + ret.jobDescription = object.value("jobDescription").toString(); + ret.department = object.value("department").toString(); + return ret; +} + +QJsonArray GooglePeople::Organization::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + QJsonArray array; + const QList details = contact.details(); + + for (const QContactOrganization &detail : details) { + if (!shouldAddDetailChanges(detail, hasChanges)) { + continue; + } + QJsonObject org; + org.insert("name", detail.name()); + org.insert("title", detail.title()); + org.insert("jobDescription", detail.role()); + org.insert("department", detail.department().value(0)); + array.append(org); + } + + return array; +} + +bool GooglePeople::PhoneNumber::saveContactDetails(QContact *contact, const QList &values) +{ + removeDetails(contact); + + for (const PhoneNumber &phoneNumber : values) { + QContactPhoneNumber detail; + detail.setNumber(phoneNumber.value); + + if (phoneNumber.type == QStringLiteral("home")) { + detail.setContexts(QContactDetail::ContextHome); + } else if (phoneNumber.type == QStringLiteral("work")) { + detail.setContexts(QContactDetail::ContextWork); + } else if (phoneNumber.type == QStringLiteral("mobile")) { + detail.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); + } else if (phoneNumber.type == QStringLiteral("workMobile")) { + detail.setContexts(QContactDetail::ContextWork); + detail.setSubTypes(QList() << QContactPhoneNumber::SubTypeMobile); + } else if (phoneNumber.type == QStringLiteral("homeFax")) { + detail.setContexts(QContactDetail::ContextHome); + detail.setSubTypes(QList() << QContactPhoneNumber::SubTypeFax); + } else if (phoneNumber.type == QStringLiteral("workFax")) { + detail.setContexts(QContactDetail::ContextWork); + detail.setSubTypes(QList() << QContactPhoneNumber::SubTypeFax); + } else if (phoneNumber.type == QStringLiteral("otherFax")) { + detail.setContexts(QContactDetail::ContextOther); + detail.setSubTypes(QList() << QContactPhoneNumber::SubTypeFax); + } else if (phoneNumber.type == QStringLiteral("pager")) { + detail.setSubTypes(QList() << QContactPhoneNumber::SubTypePager); + } else if (phoneNumber.type == QStringLiteral("workPager")) { + detail.setContexts(QContactDetail::ContextWork); + detail.setSubTypes(QList() << QContactPhoneNumber::SubTypePager); + } else if (phoneNumber.type == QStringLiteral("other")) { + detail.setContexts(QContactDetail::ContextOther); + } else { + // phoneNumber.type is a custom type, so ignore it. If the user does not change it to a + // known type, the type will not be upsynced and the custom type will be preserved. + } + + if (!saveContactDetail(contact, &detail)) { + return false; + } + } + + return true; +} + +GooglePeople::PhoneNumber GooglePeople::PhoneNumber::fromJsonObject(const QJsonObject &object) +{ + PhoneNumber ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.value = object.value("value").toString(); + ret.type = object.value("type").toString(); + return ret; +} + +QJsonArray GooglePeople::PhoneNumber::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + QJsonArray array; + const QList details = contact.details(); + + for (int i = 0; i < details.count(); ++i) { + const QContactPhoneNumber &detail = details.at(i); + if (!shouldAddDetailChanges(detail, hasChanges)) { + continue; + } + + QString type; + const int context = detail.contexts().value(0, -1); + if (detail.subTypes().isEmpty()) { + if (context == QContactDetail::ContextHome) { + type = QStringLiteral("home"); + } else if (context == QContactDetail::ContextWork) { + type = QStringLiteral("work"); + } + } else { + const int subType = detail.subTypes().at(0); + switch (subType) { + case QContactPhoneNumber::SubTypeMobile: + type = QStringLiteral("mobile"); + break; + case QContactPhoneNumber::SubTypeFax: + if (context == QContactDetail::ContextHome) { + type = QStringLiteral("homeFax"); + } else if (context == QContactDetail::ContextWork) { + type = QStringLiteral("workFax"); + } else if (context == QContactDetail::ContextOther) { + type = QStringLiteral("otherFax"); + } + break; + case QContactPhoneNumber::SubTypePager: + if (context == QContactDetail::ContextWork) { + type = QStringLiteral("workPager"); + } else { + type = QStringLiteral("pager"); + } + break; + default: + break; + } + } + + QJsonObject phone; + if (type.isEmpty()) { + // No type set, or the Google field had a custom type set, so don't overwrite it. + } else { + phone.insert("type", type); + } + phone.insert("value", detail.number()); + array.append(phone); + } + return array; +} + +bool GooglePeople::PersonMetadata::saveContactDetails(QContact *contact, const PersonMetadata &metadata) +{ + for (const Source &source : metadata.sources) { + QVariantMap sourceInfo; + sourceInfo.insert("type", source.type); + sourceInfo.insert("id", source.id); + sourceInfo.insert("etag", source.etag); + if (!saveContactExtendedDetail(contact, QStringLiteral("source_%1").arg(source.type), sourceInfo)) { + return false; + } + } + return true; +} + +QString GooglePeople::PersonMetadata::etag(const QContact &contact) +{ + const QVariantMap sourceInfo = contactExtendedDetail(contact, QStringLiteral("source_CONTACT")).toMap(); + return sourceInfo.value("etag").toString(); +} + +GooglePeople::PersonMetadata GooglePeople::PersonMetadata::fromJsonObject(const QJsonObject &object) +{ + PersonMetadata ret; + ret.sources = jsonArrayToList(object.value("sources").toArray()); + ret.previousResourceNames = object.value("previousResourceNames").toVariant().toStringList(); + ret.linkedPeopleResourceNames = object.value("linkedPeopleResourceNames").toVariant().toStringList(); + ret.deleted = object.value("deleted").toBool(); + return ret; +} + +QJsonObject GooglePeople::PersonMetadata::toJsonObject(const QContact &contact) +{ + // Only need to add the details for the "CONTACT" source. + QJsonObject metadataObject; + const QVariantMap sourceInfo = contactExtendedDetail( + contact, QStringLiteral("source_CONTACT")).toMap(); + + if (!sourceInfo.isEmpty()) { + QJsonObject sourceObject; + sourceObject.insert("type", sourceInfo.value("type").toString()); + sourceObject.insert("id", sourceInfo.value("id").toString()); + sourceObject.insert("etag", sourceInfo.value("etag").toString()); + + QJsonArray sourcesArray; + sourcesArray.append(QJsonValue(sourceObject)); + + metadataObject.insert("sources", sourcesArray); + } + + return metadataObject; +} + +QContactAvatar GooglePeople::Photo::getPrimaryPhoto(const QContact &contact, + QString *remoteAvatarUrl, + QString *localAvatarFile) +{ + // Use the first avatar as the the primary photo for the contact. + const QContactAvatar avatar = contact.detail(); + if (localAvatarFile) { + *localAvatarFile = avatar.imageUrl().toString(); + } + if (remoteAvatarUrl) { + *remoteAvatarUrl = avatar.videoUrl().toString(); + } + + return avatar; +} + +bool GooglePeople::Photo::saveContactDetails(QContact *contact, const QList &values) +{ + removeDetails(contact); + + const QString guid = contact->detail().guid(); + + for (const Photo &photo : values) { + if (photo.default_) { + // Ignore the Google-generated avatar that simply shows the contact's initials. + continue; + } + + QContactAvatar avatar; + const QString localFilePath = GoogleContactImageDownloader::staticOutputFile(guid, photo.url); + if (localFilePath.isEmpty()) { + SOCIALD_LOG_ERROR("Cannot generate local file name for avatar url:" << photo.url + << "for contact:" << guid); + continue; + } + + avatar.setImageUrl(QUrl(localFilePath)); + avatar.setVideoUrl(QUrl(photo.url)); // ugly hack to store the remote url separately to the local path + + if (!saveContactDetail(contact, &avatar)) { + return false; + } + } + + return true; +} + +GooglePeople::Photo GooglePeople::Photo::fromJsonObject(const QJsonObject &object) +{ + Photo ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.url = object.value("url").toString(); + ret.default_ = object.value("default").toBool(); + return ret; +} + +QJsonArray GooglePeople::Photo::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + QJsonArray array; + const QList details = contact.details(); + + for (const QContactAvatar &detail : details) { + if (!shouldAddDetailChanges(detail, hasChanges)) { + continue; + } + QJsonObject photo; + photo.insert("url", detail.imageUrl().toString()); + array.append(photo); + } + return array; +} + +bool GooglePeople::Url::saveContactDetails(QContact *contact, const QList &values) +{ + removeDetails(contact); + + for (const Url &url : values) { + QContactUrl detail; + detail.setUrl(url.value); + + if (url.type == QStringLiteral("homePage")) { + detail.setSubType(QContactUrl::SubTypeHomePage); + } else if (url.type == QStringLiteral("blog")) { + detail.setSubType(QContactUrl::SubTypeBlog); + } + + if (!saveContactDetail(contact, &detail)) { + return false; + } + } + + return true; +} + +GooglePeople::Url GooglePeople::Url::fromJsonObject(const QJsonObject &object) +{ + Url ret; + ret.metadata = FieldMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.value = object.value("value").toString(); + ret.type = object.value("type").toString(); + ret.formattedType = object.value("formattedType").toString(); + return ret; +} + +QJsonArray GooglePeople::Url::jsonValuesForContact(const QContact &contact, bool *hasChanges) +{ + QJsonArray array; + const QList details = contact.details(); + + for (const QContactUrl &detail : details) { + if (!shouldAddDetailChanges(detail, hasChanges)) { + continue; + } + QJsonObject url; + switch (detail.subType()) { + case QContactUrl::SubTypeHomePage: + url.insert("type", QStringLiteral("homePage")); + break; + case QContactUrl::SubTypeBlog: + url.insert("type", QStringLiteral("blog")); + break; + default: + // No type set, or the Google field had a custom type set, so don't overwrite it. + break; + } + url.insert("value", detail.url()); + array.append(url); + } + return array; +} + +QContact GooglePeople::Person::toContact(int accountId, + const QList &candidateCollections) const +{ + QContact contact; + saveToContact(&contact, accountId, candidateCollections); + return contact; +} + +bool GooglePeople::Person::saveToContact(QContact *contact, + int accountId, + const QList &candidateCollections) const +{ + if (!contact) { + SOCIALD_LOG_ERROR("saveToContact() failed: invalid contact!"); + return false; + } + + QContactGuid guid = contact->detail(); + if (guid.guid().isEmpty()) { + guid.setGuid(guidForPerson(accountId, resourceName)); + if (!contact->saveDetail(&guid, QContact::IgnoreAccessConstraints)) { + return false; + } + } + + PersonMetadata::saveContactDetails(contact, metadata); + Address::saveContactDetails(contact, addresses); + Biography::saveContactDetails(contact, biographies); + Birthday::saveContactDetails(contact, birthdays); + EmailAddress::saveContactDetails(contact, emailAddresses); + Event::saveContactDetails(contact, events); + Membership::saveContactDetails(contact, memberships, accountId, candidateCollections); + Name::saveContactDetails(contact, names); + Nickname::saveContactDetails(contact, nicknames); + Organization::saveContactDetails(contact, organizations); + PhoneNumber::saveContactDetails(contact, phoneNumbers); + Photo::saveContactDetails(contact, photos); + Url::saveContactDetails(contact, urls); + + return true; +} + +GooglePeople::Person GooglePeople::Person::fromJsonObject(const QJsonObject &object) +{ + Person ret; + ret.resourceName = object.value("resourceName").toString(); + ret.metadata = PersonMetadata::fromJsonObject(object.value("metadata").toObject()); + ret.addresses = jsonArrayToList
(object.value("addresses").toArray()); + ret.biographies = jsonArrayToList(object.value("biographies").toArray()); + ret.birthdays = jsonArrayToList(object.value("birthdays").toArray()); + ret.emailAddresses = jsonArrayToList(object.value("emailAddresses").toArray()); + ret.events = jsonArrayToList(object.value("events").toArray()); + ret.memberships = jsonArrayToList(object.value("memberships").toArray()); + ret.names = jsonArrayToList(object.value("names").toArray()); + ret.nicknames = jsonArrayToList(object.value("nicknames").toArray()); + ret.organizations = jsonArrayToList(object.value("organizations").toArray()); + ret.phoneNumbers = jsonArrayToList(object.value("phoneNumbers").toArray()); + ret.photos = jsonArrayToList(object.value("photos").toArray()); + ret.urls = jsonArrayToList(object.value("urls").toArray()); + return ret; +} + +GooglePeople::ContactGroupMetadata GooglePeople::ContactGroupMetadata::fromJsonObject(const QJsonObject &obj) +{ + ContactGroupMetadata ret; + const QString updateTime = obj.value("updateTime").toString(); + if (!updateTime.isEmpty()) { + ret.updateTime = QDateTime::fromString(updateTime, Qt::ISODate); + } + ret.deleted = obj.value("deleted").toBool(); + return ret; +} + +QJsonObject GooglePeople::Person::contactToJsonObject(const QContact &contact, + QStringList *addedFields) +{ + QJsonObject person; + + // Add resourceName + QString resourceName = personResourceName(contact); + if (!resourceName.isEmpty()) { + person.insert("resourceName", resourceName); + } + + // Add metadata including etag + QJsonObject metadataObject = PersonMetadata::toJsonObject(contact); + if (!metadataObject.isEmpty()) { + person.insert("metadata", metadataObject); + } + + // Add other fields. + // photos are not added here, as they can only modified in the Google People API via + // updateContactPhoto(), and cannot be passed in createContact() and updateContact(). + addJsonValuesForContact
(QStringLiteral("addresses"), + contact, &person, addedFields); + addJsonValuesForContact(QStringLiteral("biographies"), + contact, &person, addedFields); + addJsonValuesForContact(QStringLiteral("birthdays"), + contact, &person, addedFields); + addJsonValuesForContact(QStringLiteral("emailAddresses"), + contact, &person, addedFields); + addJsonValuesForContact(QStringLiteral("events"), + contact, &person, addedFields); + addJsonValuesForContact(QStringLiteral("names"), + contact, &person, addedFields); + addJsonValuesForContact(QStringLiteral("nicknames"), + contact, &person, addedFields); + addJsonValuesForContact(QStringLiteral("organizations"), + contact, &person, addedFields); + addJsonValuesForContact(QStringLiteral("phoneNumbers"), + contact, &person, addedFields); + addJsonValuesForContact(QStringLiteral("urls"), + contact, &person, addedFields); + + if (contact.id().isNull()) { + // The membership (collection) of a contact never changes, so it only needs to be added + // if this is a new contact. + person.insert(QStringLiteral("memberships"), + Membership::jsonValuesForContact(contact)); + if (addedFields) { + addedFields->append(QStringLiteral("memberships")); + } + } + + return person; +} + +QString GooglePeople::Person::personResourceName(const QContact &contact) +{ + const QString guid = contact.detail().guid(); + if (!guid.isEmpty()) { + const int index = guid.indexOf(':'); + if (index >= 0) { + return guid.mid(index + 1); + } + } + return QString(); +} + +QStringList GooglePeople::Person::supportedPersonFields() +{ + static QStringList fields; + if (fields.isEmpty()) { + fields << QStringLiteral("metadata"); + fields << QStringLiteral("addresses"); + fields << QStringLiteral("biographies"); + fields << QStringLiteral("birthdays"); + fields << QStringLiteral("emailAddresses"); + fields << QStringLiteral("events"); + fields << QStringLiteral("memberships"); + fields << QStringLiteral("names"); + fields << QStringLiteral("nicknames"); + fields << QStringLiteral("organizations"); + fields << QStringLiteral("phoneNumbers"); + fields << QStringLiteral("photos"); + fields << QStringLiteral("urls"); + } + return fields; +} + +QString GooglePeople::Person::guidForPerson(int accountId, const QString &resourceName) +{ + return QStringLiteral("%1:%2").arg(accountId).arg(resourceName); +} + +bool GooglePeople::ContactGroup::isMyContactsGroup() const +{ + return resourceName == QStringLiteral("contactGroups/myContacts"); +} + +QContactCollection GooglePeople::ContactGroup::toCollection(int accountId) const +{ + QContactCollection collection; + collection.setMetaData(QContactCollection::KeyName, formattedName); + collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QCoreApplication::applicationName()); + collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, accountId); + collection.setExtendedMetaData(QStringLiteral("resourceName"), resourceName); + collection.setExtendedMetaData(QStringLiteral("groupType"), groupType); + + return collection; +} + +bool GooglePeople::ContactGroup::isMyContactsCollection(const QContactCollection &collection, int accountId) +{ + return collection.extendedMetaData("resourceName").toString() == QStringLiteral("contactGroups/myContacts") + && (accountId == 0 + || collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() == accountId); +} + +GooglePeople::ContactGroup GooglePeople::ContactGroup::fromJsonObject(const QJsonObject &obj) +{ + ContactGroup ret; + ret.resourceName = obj.value("resourceName").toString(); + ret.etag = obj.value("etag").toString(); + ret.contactGroupMetadata = ContactGroupMetadata::fromJsonObject(obj.value("contactGroupMetadata").toObject()); + ret.groupType = obj.value("groupType").toString(); + ret.name = obj.value("name").toString(); + ret.formattedName = obj.value("formattedName").toString(); + ret.memberResourceNames = obj.value("memberResourceNames").toVariant().toStringList(); + ret.memberCount = obj.value("memberCount").toInt(); + return ret; +} + +#define DEBUG_VALUE_ONLY(propertyName) \ + debug.nospace() << #propertyName << "=" << value.propertyName + +#define DEBUG_VALUE(propertyName) \ + DEBUG_VALUE_ONLY(propertyName) << ", "; +#define DEBUG_VALUE_LAST(propertyName) \ + DEBUG_VALUE_ONLY(propertyName) << ")"; +#define DEBUG_VALUE_INDENT(propertyName) \ + debug.nospace() << "\n ";\ + DEBUG_VALUE(propertyName); +#define DEBUG_VALUE_INDENT_LAST(propertyName) \ + debug.nospace() << "\n ";\ + DEBUG_VALUE_LAST(propertyName); + +QDebug operator<<(QDebug debug, const GooglePeople::Source &value) +{ + debug.nospace() << "Source("; + DEBUG_VALUE(type) + DEBUG_VALUE_LAST(id); + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::FieldMetadata &value) +{ + debug.nospace() << "FieldMetadata("; + DEBUG_VALUE(primary) + DEBUG_VALUE(verified) + DEBUG_VALUE_LAST(source) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Address &value) +{ + debug.nospace() << "Address("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(formattedValue) + DEBUG_VALUE(type) + DEBUG_VALUE(formattedType) + DEBUG_VALUE(poBox) + DEBUG_VALUE(streetAddress) + DEBUG_VALUE(extendedAddress) + DEBUG_VALUE(city) + DEBUG_VALUE(region) + DEBUG_VALUE(postalCode) + DEBUG_VALUE(country) + DEBUG_VALUE_LAST(countryCode) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Biography &value) +{ + debug.nospace() << "Biography("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(value) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Birthday &value) +{ + debug.nospace() << "Birthday("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(date) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::EmailAddress &value) +{ + debug.nospace() << "EmailAddress("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(value) + DEBUG_VALUE(type) + DEBUG_VALUE(formattedType) + DEBUG_VALUE_LAST(displayName) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Event &value) +{ + debug.nospace() << "Event("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(date) + DEBUG_VALUE_LAST(type) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Membership &value) +{ + debug.nospace() << "Membership("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(contactGroupResourceName) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Name &value) +{ + debug.nospace() << "Name("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(familyName) + DEBUG_VALUE(givenName) + DEBUG_VALUE_LAST(middleName) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Nickname &value) +{ + debug.nospace() << "Nickname("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(value) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Organization &value) +{ + debug.nospace() << "Organization("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(name) + DEBUG_VALUE(title) + DEBUG_VALUE(jobDescription) + DEBUG_VALUE_LAST(department) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::PhoneNumber &value) +{ + debug.nospace() << "PhoneNumber("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(value) + DEBUG_VALUE(type) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::PersonMetadata &value) +{ + debug.nospace() << "PersonMetadata("; + DEBUG_VALUE(sources) + DEBUG_VALUE(previousResourceNames) + DEBUG_VALUE(linkedPeopleResourceNames) + DEBUG_VALUE_LAST(deleted) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Photo &value) +{ + debug.nospace() << "Photo("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(url) + DEBUG_VALUE_LAST(default_) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Url &value) +{ + debug.nospace() << "Url("; + DEBUG_VALUE(metadata) + DEBUG_VALUE(value) + DEBUG_VALUE(type) + DEBUG_VALUE_LAST(formattedType) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::Person &value) +{ + debug.nospace() << "\nPerson("; + DEBUG_VALUE_INDENT(resourceName) + DEBUG_VALUE_INDENT(metadata) + DEBUG_VALUE_INDENT(addresses) + DEBUG_VALUE_INDENT(biographies) + DEBUG_VALUE_INDENT(birthdays) + DEBUG_VALUE_INDENT(emailAddresses) + DEBUG_VALUE_INDENT(memberships) + DEBUG_VALUE_INDENT(names) + DEBUG_VALUE_INDENT(nicknames) + DEBUG_VALUE_INDENT(organizations) + DEBUG_VALUE_INDENT(phoneNumbers) + DEBUG_VALUE_INDENT(photos) + DEBUG_VALUE_INDENT_LAST(urls) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::ContactGroupMetadata &value) +{ + debug.nospace() << "ContactGroupMetadata("; + DEBUG_VALUE(updateTime) + DEBUG_VALUE_LAST(deleted) + return debug.maybeSpace(); +} + +QDebug operator<<(QDebug debug, const GooglePeople::ContactGroup &value) +{ + debug.nospace() << "\nContactGroup("; + DEBUG_VALUE_INDENT(resourceName) + DEBUG_VALUE_INDENT(etag) + DEBUG_VALUE_INDENT(contactGroupMetadata) + DEBUG_VALUE_INDENT(groupType) + DEBUG_VALUE_INDENT(name) + DEBUG_VALUE_INDENT(formattedName) + DEBUG_VALUE_INDENT(memberResourceNames) + DEBUG_VALUE_INDENT_LAST(memberCount) + return debug.maybeSpace(); +} diff --git a/src/google/google-contacts/googlepeoplejson.h b/src/google/google-contacts/googlepeoplejson.h new file mode 100644 index 0000000..f1b5c26 --- /dev/null +++ b/src/google/google-contacts/googlepeoplejson.h @@ -0,0 +1,396 @@ +/**************************************************************************** + ** + ** Copyright (c) 2021 Jolla Ltd. + ** + ** 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 + ** + ****************************************************************************/ + +#ifndef GOOGLEPEOPLEJSON_H +#define GOOGLEPEOPLEJSON_H + +#include +#include +#include +#include + +#include +#include + +QTCONTACTS_USE_NAMESPACE + +namespace GooglePeople +{ + class Source + { + public: + QString type; + QString id; + QString etag; + + /* Ignored fields: + QString updateTime; + ProfileMetadata profileMetadata; + */ + + static Source fromJsonObject(const QJsonObject &obj); + }; + + class FieldMetadata + { + public: + bool primary = false; + bool verified = false; + Source source; + + static FieldMetadata fromJsonObject(const QJsonObject &obj); + }; + + class Address + { + public: + FieldMetadata metadata; + QString formattedValue; + QString type; + QString formattedType; + QString poBox; + QString streetAddress; + QString extendedAddress; + QString city; + QString region; + QString postalCode; + QString country; + QString countryCode; + + static bool saveContactDetails(QContact *contact, const QList
&values); + static Address fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class Biography + { + public: + FieldMetadata metadata; + QString value; + + /* Ignored fields: + QString contentType; + */ + + static bool saveContactDetails(QContact *contact, const QList &values); + static Biography fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class Birthday + { + public: + FieldMetadata metadata; + QDate date; + + /* Ignored fields: + QString text; + */ + + static bool saveContactDetails(QContact *contact, const QList &values); + static Birthday fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class EmailAddress + { + public: + FieldMetadata metadata; + QString value; + QString type; + QString formattedType; + QString displayName; + + static bool saveContactDetails(QContact *contact, const QList &values); + static EmailAddress fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class Event + { + public: + FieldMetadata metadata; + QDate date; + QString type; + + /* Ignored fields: + QString formattedType; + */ + + static bool saveContactDetails(QContact *contact, const QList &values); + static Event fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class Membership + { + public: + FieldMetadata metadata; + QString contactGroupResourceName; + + /* Ignored fields: + DomainMembership domainMembership; + */ + + bool matchesCollection(const QContactCollection &collection, int accountId) const; + + static bool saveContactDetails(QContact *contact, + const QList &values, + int accountId, + const QList &candidateCollections); + static Membership fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact); + }; + + class Name + { + public: + FieldMetadata metadata; + QString familyName; + QString givenName; + QString middleName; + + /* Ignored fields: + QString displayName; + QString displayNameLastFirst; + QString unstructuredName; + QString phoneticFullName; + QString phoneticFamilyName; + QString phoneticGivenName; + QString phoneticMiddleName; + QString honorificPrefix; + QString honorificSuffix; + QString phoneticHonorificPrefix; + QString phoneticHonorificSuffix; + */ + + static bool saveContactDetails(QContact *contact, const QList &values); + static Name fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class Nickname + { + public: + FieldMetadata metadata; + QString value; + + /* Ignored fields: + QString type; + */ + + static bool saveContactDetails(QContact *contact, const QList &values); + static Nickname fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class Organization + { + public: + FieldMetadata metadata; + QString name; + QString title; + QString jobDescription; + QString department; + + /* Ignored fields: + QString type; + QString formattedType; + QDate startDate; + QDate endDate; + QString phoneticName; + QString symbol; + QString domain; + QString location; + */ + + static bool saveContactDetails(QContact *contact, const QList &values); + static Organization fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class PhoneNumber + { + public: + FieldMetadata metadata; + QString value; + QString type; + + /* Ignored fields: + QString canonicalForm; + QString formattedType; + */ + + static bool saveContactDetails(QContact *contact, const QList &values); + static PhoneNumber fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class PersonMetadata + { + public: + QList sources; + QStringList previousResourceNames; + QStringList linkedPeopleResourceNames; + bool deleted = false; + + static QString etag(const QContact &contact); + + static bool saveContactDetails(QContact *contact, const PersonMetadata &value); + static PersonMetadata fromJsonObject(const QJsonObject &obj); + static QJsonObject toJsonObject(const QContact &contact); + }; + + class Photo + { + public: + FieldMetadata metadata; + QString url; + bool default_ = false; + + static QContactAvatar getPrimaryPhoto(const QContact &contact, + QString *remoteAvatarUrl = nullptr, + QString *localAvatarFile = nullptr); + + static bool saveContactDetails(QContact *contact, const QList &values); + static Photo fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class Url + { + public: + FieldMetadata metadata; + QString value; + QString type; + QString formattedType; + + static bool saveContactDetails(QContact *contact, const QList &values); + static Url fromJsonObject(const QJsonObject &obj); + static QJsonArray jsonValuesForContact(const QContact &contact, bool *hasChanges); + }; + + class Person + { + public: + QString resourceName; + PersonMetadata metadata; + QList
addresses; + QList biographies; + QList birthdays; + QList emailAddresses; + QList events; + QList memberships; + QList names; + QList nicknames; + QList organizations; + QList phoneNumbers; + QList photos; + QList urls; + + /* Ignored fields: + QString etag; + QList ageRanges; + QList calendarUrls; + QList clientData; + QList coverPhotos; + QList externalIds; + QList fileAses; + QList genders; + QList imClients; + QList interests; + QList locales; + QList locations; + QList miscKeywords; + QList occupations; + QList relations; + QList sipAddresses; + QList skills; + QList userDefined; + */ + + inline bool isValid() const { return !resourceName.isEmpty(); } + + QContact toContact(int accountId, + const QList &candidateCollections) const; + bool saveToContact(QContact *contact, + int accountId, + const QList &candidateCollections) const; + + static Person fromJsonObject(const QJsonObject &obj); + static QJsonObject contactToJsonObject(const QContact &contact, + QStringList *updatedFields = nullptr); + + static QString personResourceName(const QContact &contact); + static QStringList supportedPersonFields(); + + private: + static QString guidForPerson(int accountId, const QString &resourceName); + }; + + class ContactGroupMetadata + { + public: + QDateTime updateTime; + bool deleted = false; + + static ContactGroupMetadata fromJsonObject(const QJsonObject &obj); + }; + + class ContactGroup + { + public: + QString resourceName; + QString etag; + ContactGroupMetadata contactGroupMetadata; + QString groupType; + QString name; + QString formattedName; + QStringList memberResourceNames; + int memberCount = 0; + + bool isMyContactsGroup() const; + QContactCollection toCollection(int accountId) const; + + static bool isMyContactsCollection(const QContactCollection &collection, int accountId = 0); + static ContactGroup fromJsonObject(const QJsonObject &obj); + }; +} + +QDebug operator<<(QDebug debug, const GooglePeople::Source &value); +QDebug operator<<(QDebug debug, const GooglePeople::FieldMetadata &value); +QDebug operator<<(QDebug debug, const GooglePeople::Address &value); +QDebug operator<<(QDebug debug, const GooglePeople::Biography &value); +QDebug operator<<(QDebug debug, const GooglePeople::Birthday &value); +QDebug operator<<(QDebug debug, const GooglePeople::EmailAddress &value); +QDebug operator<<(QDebug debug, const GooglePeople::Event &value); +QDebug operator<<(QDebug debug, const GooglePeople::Membership &value); +QDebug operator<<(QDebug debug, const GooglePeople::Name &value); +QDebug operator<<(QDebug debug, const GooglePeople::Nickname &value); +QDebug operator<<(QDebug debug, const GooglePeople::Organization &value); +QDebug operator<<(QDebug debug, const GooglePeople::PhoneNumber &value); +QDebug operator<<(QDebug debug, const GooglePeople::PersonMetadata &value); +QDebug operator<<(QDebug debug, const GooglePeople::Photo &value); +QDebug operator<<(QDebug debug, const GooglePeople::Url &value); +QDebug operator<<(QDebug debug, const GooglePeople::Person &value); +QDebug operator<<(QDebug debug, const GooglePeople::ContactGroupMetadata &value); +QDebug operator<<(QDebug debug, const GooglePeople::ContactGroup &value); + +#endif // GOOGLEPEOPLEJSON_H diff --git a/src/google/google-contacts/googletwowaycontactsyncadaptor.cpp b/src/google/google-contacts/googletwowaycontactsyncadaptor.cpp index c998488..25cecf1 100644 --- a/src/google/google-contacts/googletwowaycontactsyncadaptor.cpp +++ b/src/google/google-contacts/googletwowaycontactsyncadaptor.cpp @@ -20,8 +20,6 @@ ****************************************************************************/ #include "googletwowaycontactsyncadaptor.h" -#include "googlecontactstream.h" -#include "googlecontactatom.h" #include "googlecontactimagedownloader.h" #include "constants_p.h" @@ -35,52 +33,29 @@ #include #include #include -#include -#include -#include -#include -#include -#include #include +#include +#include #include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include #include #include -#define SOCIALD_GOOGLE_MAX_CONTACT_ENTRY_RESULTS 50 - static const char *IMAGE_DOWNLOADER_TOKEN_KEY = "url"; -static const char *IMAGE_DOWNLOADER_ACCOUNT_ID_KEY = "account_id"; static const char *IMAGE_DOWNLOADER_IDENTIFIER_KEY = "identifier"; namespace { -const QString MyContactsCollectionName = QStringLiteral("Contacts"); -const QString CollectionKeyMyContacts = QStringLiteral("MyContacts"); -const QString CollectionKeyLastSync = QStringLiteral("last-sync-time"); -const QString CollectionKeyAtomId = QStringLiteral("atom-id"); -const QString UnsupportedElementsKey = QStringLiteral("unsupportedElements"); -const QString EtagKey = QStringLiteral("etag"); +const QString CollectionKeySyncToken = QStringLiteral("syncToken"); +const QString CollectionKeySyncTokenDate = QStringLiteral("syncTokenDate"); QContactCollection findCollection(const QContactManager &contactManager, int accountId) { const QList collections = contactManager.collections(); for (const QContactCollection &collection : collections) { - if (collection.extendedMetaData(CollectionKeyMyContacts).toBool() - && collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() == accountId) { + if (GooglePeople::ContactGroup::isMyContactsCollection(collection, accountId)) { return collection; } } @@ -97,38 +72,6 @@ int indexOfContact(const QList &contacts, const QContactId &contactId) return -1; } -QString contactEtag(const QContact &contact) -{ - for (const QContactExtendedDetail &detail : contact.details()) { - if (detail.name() == QLatin1String("etag")) { - return detail.data().toString(); - } - } - return QString(); -} - -bool saveExtendedDetail(QContact *contact, const QString &detailName, const QVariant &detailData) -{ - QContactExtendedDetail matchedDetail; - for (const QContactExtendedDetail &detail : contact->details()) { - if (detail.name() == detailName) { - matchedDetail = detail; - break; - } - } - - if (matchedDetail.name().isEmpty()) { - matchedDetail.setName(detailName); - } - matchedDetail.setData(detailData); - return contact->saveDetail(&matchedDetail, QContact::IgnoreAccessConstraints); -} - -QString collectionAtomId(const QContactCollection &collection) -{ - return collection.extendedMetaData(CollectionKeyAtomId).toString(); -} - } //------------------------- @@ -136,37 +79,39 @@ QString collectionAtomId(const QContactCollection &collection) GoogleContactSqliteSyncAdaptor::GoogleContactSqliteSyncAdaptor(int accountId, GoogleTwoWayContactSyncAdaptor *parent) : QtContactsSqliteExtensions::TwoWayContactSyncAdaptor(accountId, qAppName(), *parent->m_contactManager) , q(parent) - , m_accountId(accountId) { - m_collection = findCollection(contactManager(), m_accountId); - if (m_collection.id().isNull()) { - SOCIALD_LOG_DEBUG("No MyContacts collection saved yet for account:" << m_accountId); - } else { - SOCIALD_LOG_DEBUG("Found MyContacts collection" << m_collection.id() << "for account:" << m_accountId); - } } GoogleContactSqliteSyncAdaptor::~GoogleContactSqliteSyncAdaptor() { } -int GoogleContactSqliteSyncAdaptor::accountId() const +bool GoogleContactSqliteSyncAdaptor::isLocallyDeletedGuid(const QString &guid) const { - return m_accountId; + if (guid.isEmpty()) { + return false; + } + + const TwoWayContactSyncAdaptorPrivate::ContactChanges &localChanges(d->m_localContactChanges[q->m_collection.id()]); + for (const QContact &removedContact : localChanges.removedContacts) { + if (guid == removedContact.detail().guid()) { + return true; + } + } + + return false; } bool GoogleContactSqliteSyncAdaptor::determineRemoteCollections() { - if (collectionAtomId(m_collection).isEmpty()) { - // we need to determine the atom id of the My Contacts group - // because we upload newly added contacts to that group. - SOCIALD_LOG_TRACE("performing request to determine atom id of My Contacts group with account" << m_accountId); - q->requestData(m_accountId, 0, QString(), QDateTime(), GoogleTwoWayContactSyncAdaptor::ContactGroupRequest); + if (q->m_collection.id().isNull()) { + SOCIALD_LOG_TRACE("performing request to find My Contacts group with account" << q->m_accountId); + q->requestData(GoogleTwoWayContactSyncAdaptor::ContactGroupRequest); } else { // we can just sync changes immediately - SOCIALD_LOG_TRACE("atom id of My Contacts group already known:" << collectionAtomId(m_collection) - << "requesting contact sync deltas with account" << m_accountId); - remoteCollectionsDetermined(QList() << m_collection); + SOCIALD_LOG_TRACE("requesting contact sync deltas with account" << q->m_accountId + << "for collection" << q->m_collection.id()); + remoteCollectionsDetermined(QList() << q->m_collection); } return true; @@ -180,11 +125,8 @@ bool GoogleContactSqliteSyncAdaptor::deleteRemoteCollection(const QContactCollec bool GoogleContactSqliteSyncAdaptor::determineRemoteContacts(const QContactCollection &collection) { - q->requestData(m_accountId, - 0, - QString(), - collection.extendedMetaData(CollectionKeyLastSync).toDateTime(), - GoogleTwoWayContactSyncAdaptor::ContactRequest, + Q_UNUSED(collection) + q->requestData(GoogleTwoWayContactSyncAdaptor::ContactRequest, GoogleTwoWayContactSyncAdaptor::DetermineRemoteContacts); return true; } @@ -196,17 +138,21 @@ bool GoogleContactSqliteSyncAdaptor::determineRemoteContactChanges(const QContac const QList &localUnmodifiedContacts, QContactManager::Error *error) { + Q_UNUSED(collection) Q_UNUSED(localAddedContacts) Q_UNUSED(localModifiedContacts) Q_UNUSED(localDeletedContacts) Q_UNUSED(localUnmodifiedContacts) Q_UNUSED(error) - q->requestData(m_accountId, - 0, - QString(), - collection.extendedMetaData(CollectionKeyLastSync).toDateTime(), - GoogleTwoWayContactSyncAdaptor::ContactRequest, + if (q->m_connectionsListParams.syncToken.isEmpty()) { + // Notify the two-way sync adaptor that this is a full sync rather than a delta sync, so + // that it will call determineRemoteContacts() to fetch all contacts for the collection. + *error = QContactManager::NotSupportedError; + return false; + } + + q->requestData(GoogleTwoWayContactSyncAdaptor::ContactRequest, GoogleTwoWayContactSyncAdaptor::DetermineRemoteContactChanges); return true; } @@ -216,8 +162,9 @@ bool GoogleContactSqliteSyncAdaptor::storeLocalChangesRemotely(const QContactCol const QList &modifiedContacts, const QList &deletedContacts) { - const QDateTime since = collection.extendedMetaData(CollectionKeyLastSync).toDateTime(); - q->upsyncLocalChanges(since, addedContacts, modifiedContacts, deletedContacts, m_accountId); + Q_UNUSED(collection) + + q->upsyncLocalChanges(addedContacts, modifiedContacts, deletedContacts); return true; } @@ -228,30 +175,19 @@ void GoogleContactSqliteSyncAdaptor::storeRemoteChangesLocally(const QContactCol { Q_UNUSED(collection) - TwoWayContactSyncAdaptor::storeRemoteChangesLocally(m_collection, addedContacts, modifiedContacts, deletedContacts); + TwoWayContactSyncAdaptor::storeRemoteChangesLocally(q->m_collection, addedContacts, modifiedContacts, deletedContacts); } void GoogleContactSqliteSyncAdaptor::syncFinishedSuccessfully() { - SOCIALD_LOG_DEBUG("Sync finished OK"); - - // If this is the first sync, TWCSA will have saved the collection and given it a valid id, so - // update m_collection so that any post-sync operations (e.g. saving of queued avatar downloads) - // will refer to a valid collection. - const QContactCollection savedCollection = findCollection(contactManager(), m_accountId); - if (savedCollection.id().isNull()) { - SOCIALD_LOG_DEBUG("Error: cannot find saved My Contacts collection!"); - } else { - m_collection.setId(savedCollection.id()); - } + SOCIALD_LOG_INFO("Sync finished OK"); - // and trigger downloading avatars. - q->downloadAvatars(m_accountId); + q->syncFinished(); } void GoogleContactSqliteSyncAdaptor::syncFinishedWithError() { - SOCIALD_LOG_DEBUG("Sync finished with error"); + SOCIALD_LOG_ERROR("Sync finished with error"); } //------------------------------------- @@ -280,21 +216,29 @@ QString GoogleTwoWayContactSyncAdaptor::syncServiceName() const void GoogleTwoWayContactSyncAdaptor::sync(const QString &dataTypeString, int accountId) { - // check if we need to perform a complete clean sync + m_accountId = accountId; + + // Detect if this account was previously synced with the legacy Google Contacts API. If so, + // remove all contacts and do a fresh sync with the Google People API. + const QList collections = m_contactManager->collections(); + for (const QContactCollection &collection : collections) { + if (collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt() == accountId + && collection.extendedMetaData(QStringLiteral("atom-id")).isValid()) { + SOCIALD_LOG_INFO("Removing contacts synced with legacy Google Contacts API"); + purgeAccount(accountId); + } + } + + // Remove legacy settings file QString settingsFileName = QString::fromLatin1("%1/%2/gcontacts.ini") .arg(PRIVILEGED_DATA_DIR) .arg(QString::fromLatin1(SYNC_DATABASE_DIR)); - QSettings settingsFile(settingsFileName, QSettings::IniFormat); - bool doneCleanSync = settingsFile.value(QString::fromLatin1("%1-cleansync").arg(accountId), QVariant::fromValue(false)).toBool(); - if (!doneCleanSync) { - SOCIALD_LOG_INFO("Performing clean sync of Google contacts from account:" << accountId); - purgeAccount(accountId); // purge all data for the account before syncing - settingsFile.setValue(QString::fromLatin1("%1-cleansync").arg(accountId), QVariant::fromValue(true)); - settingsFile.sync(); - } + QFile::remove(settingsFileName); + + m_sqliteSync = new GoogleContactSqliteSyncAdaptor(accountId, this); // assume we can make up to 99 requests per sync, before being throttled. - m_apiRequestsRemaining[accountId] = 99; + m_apiRequestsRemaining = 99; // call superclass impl. GoogleDataTypeSyncAdaptor::sync(dataTypeString, accountId); @@ -307,98 +251,93 @@ void GoogleTwoWayContactSyncAdaptor::purgeDataForOldAccount(int oldId, SocialNet void GoogleTwoWayContactSyncAdaptor::beginSync(int accountId, const QString &accessToken) { - Accounts::Account *account = Accounts::Account::fromId(m_accountManager, accountId, this); - if (!account) { - SOCIALD_LOG_ERROR("unable to load Google account" << accountId); + if (accountId != m_accountId) { + SOCIALD_LOG_ERROR("Cannot begin sync, expected account id" << m_accountId << "but got" << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); return; } - account->selectService(Accounts::Service()); - QString emailAddress = account->valueAsString(QStringLiteral("default_credentials_username")); - if (emailAddress.isEmpty()) { - emailAddress = account->valueAsString(QStringLiteral("name")); - } - account->deleteLater(); - if (emailAddress.isEmpty()) { - SOCIALD_LOG_ERROR("unable to determine email address for Google account" << accountId); - setStatus(SocialNetworkSyncAdaptor::Error); - return; - } + m_accessToken = accessToken; - // clear our cache lists if necessary. - m_remoteAdds[accountId].clear(); - m_remoteMods[accountId].clear(); - m_remoteDels[accountId].clear(); - m_localAdds[accountId].clear(); - m_localMods[accountId].clear(); - m_localDels[accountId].clear(); - m_accessTokens[accountId] = accessToken; - m_emailAddresses[accountId] = emailAddress; - - GoogleContactSqliteSyncAdaptor *sqliteSync = m_sqliteSync.value(accountId); - if (sqliteSync) { - delete sqliteSync; + // Find the Google contacts collection, if previously synced. + m_collection = findCollection(*m_contactManager, accountId); + if (m_collection.id().isNull()) { + SOCIALD_LOG_DEBUG("No MyContacts collection saved yet for account:" << accountId); + } else { + loadCollection(m_collection); + SOCIALD_LOG_DEBUG("Found MyContacts collection" << m_collection.id() + << "for account:" << accountId); } - sqliteSync = new GoogleContactSqliteSyncAdaptor(accountId, this); - if (!sqliteSync->m_collection.id().isNull()) { - loadCollection(sqliteSync->m_collection); + // Initialize the people.connections.list() parameters + QString syncToken; + if (!m_collection.id().isNull()) { + syncToken = m_collection.extendedMetaData(CollectionKeySyncToken).toString(); + const QDateTime syncTokenDate = QDateTime::fromString( + m_collection.extendedMetaData(CollectionKeySyncTokenDate).toString(), + Qt::ISODate); + // Google sync token expires after 7 days. If it's almost expired, request a new sync token + // during this sync session. + if (syncTokenDate.isValid() + && syncTokenDate.daysTo(QDateTime::currentDateTimeUtc()) >= 6) { + SOCIALD_LOG_INFO("Will request new syncToken during this sync session"); + syncToken.clear(); + } } - - sqliteSync->m_syncDateTime = QDateTime::currentDateTimeUtc(); - - if (!sqliteSync->startSync()) { - sqliteSync->deleteLater(); - SOCIALD_LOG_ERROR("unable to start sync - aborting sync contacts with account:" << accountId); + m_connectionsListParams.requestSyncToken = true; + m_connectionsListParams.syncToken = syncToken; + m_connectionsListParams.personFields = GooglePeople::Person::supportedPersonFields().join(','); + + // Start the sync + if (!m_sqliteSync->startSync()) { + m_sqliteSync->deleteLater(); + SOCIALD_LOG_ERROR("unable to start sync - aborting sync contacts with account:" << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); - return; } - - m_sqliteSync[accountId] = sqliteSync; } -void GoogleTwoWayContactSyncAdaptor::requestData(int accountId, int startIndex, const QString &continuationRequest, const QDateTime &syncTimestamp, DataRequestType requestType, ContactChangeNotifier contactChangeNotifier) +void GoogleTwoWayContactSyncAdaptor::requestData( + DataRequestType requestType, + ContactChangeNotifier contactChangeNotifier, + const QString &pageToken) { - const QString accessToken = m_accessTokens[accountId]; QUrl requestUrl; - if (continuationRequest.isEmpty()) { - QUrlQuery urlQuery; - if (requestType == ContactGroupRequest) { - requestUrl = QUrl(QStringLiteral("https://www.google.com/m8/feeds/groups/default/full")); - } else { - requestUrl = QUrl(QStringLiteral("https://www.google.com/m8/feeds/contacts/default/full/")); - if (!syncTimestamp.isNull()) { // delta query - urlQuery.addQueryItem("updated-min", syncTimestamp.toString(Qt::ISODate)); - urlQuery.addQueryItem("showdeleted", QStringLiteral("true")); - } + QUrlQuery urlQuery; + if (requestType == ContactGroupRequest) { + requestUrl = QUrl(QStringLiteral("https://people.googleapis.com/v1/contactGroups")); + // Currently we do not add a syncToken for group requests, as we always fetch the complete + // list. + } else { + requestUrl = QUrl(QStringLiteral("https://people.googleapis.com/v1/people/me/connections")); + if (m_connectionsListParams.requestSyncToken) { + urlQuery.addQueryItem(QStringLiteral("requestSyncToken"), QStringLiteral("true")); } - if (startIndex >= 1) { - urlQuery.addQueryItem ("start-index", QString::number(startIndex)); + if (!m_connectionsListParams.syncToken.isEmpty()) { + urlQuery.addQueryItem(QStringLiteral("syncToken"), + m_connectionsListParams.syncToken); } - urlQuery.addQueryItem("max-results", QString::number(SOCIALD_GOOGLE_MAX_CONTACT_ENTRY_RESULTS)); - requestUrl.setQuery(urlQuery); - } else { - requestUrl = QUrl(continuationRequest); + urlQuery.addQueryItem(QStringLiteral("personFields"), + m_connectionsListParams.personFields); + } + if (!pageToken.isEmpty()) { + urlQuery.addQueryItem(QStringLiteral("pageToken"), pageToken); } + requestUrl.setQuery(urlQuery); QNetworkRequest req(requestUrl); - req.setRawHeader("GData-Version", "3.0"); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), - QString(QLatin1String("Bearer ") + accessToken).toUtf8()); + QString(QLatin1String("Bearer ") + m_accessToken).toUtf8()); - SOCIALD_LOG_TRACE("requesting" << requestUrl << "with start index" << startIndex << "with account" << accountId); + SOCIALD_LOG_TRACE("requesting" << requestUrl << "with account" << m_accountId); // we're requesting data. Increment the semaphore so that we know we're still busy. - incrementSemaphore(accountId); + incrementSemaphore(m_accountId); + QNetworkReply *reply = m_networkAccessManager->get(req); if (reply) { - reply->setProperty("accountId", accountId); - reply->setProperty("accessToken", accessToken); - reply->setProperty("continuationRequest", continuationRequest); - reply->setProperty("lastSyncTimestamp", syncTimestamp); - reply->setProperty("startIndex", startIndex); + reply->setProperty("requestType", requestType); reply->setProperty("contactChangeNotifier", contactChangeNotifier); + reply->setProperty("accountId", m_accountId); if (requestType == ContactGroupRequest) { connect(reply, &QNetworkReply::finished, this, &GoogleTwoWayContactSyncAdaptor::groupsFinishedHandler); @@ -410,12 +349,12 @@ void GoogleTwoWayContactSyncAdaptor::requestData(int accountId, int startIndex, this, &GoogleTwoWayContactSyncAdaptor::errorHandler); connect(reply, &QNetworkReply::sslErrors, this, &GoogleTwoWayContactSyncAdaptor::sslErrorsHandler); - m_apiRequestsRemaining[accountId] = m_apiRequestsRemaining[accountId] - 1; - setupReplyTimeout(accountId, reply); + m_apiRequestsRemaining -= 1; + setupReplyTimeout(m_accountId, reply); } else { - SOCIALD_LOG_ERROR("unable to request data from Google account with id" << accountId); + SOCIALD_LOG_ERROR("unable to request data from Google account with id" << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); } } @@ -423,445 +362,419 @@ void GoogleTwoWayContactSyncAdaptor::groupsFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray data = reply->readAll(); - int startIndex = reply->property("startIndex").toInt(); - int accountId = reply->property("accountId").toInt(); - QDateTime lastSyncTimestamp = reply->property("lastSyncTimestamp").toDateTime(); bool isError = reply->property("isError").toBool(); reply->deleteLater(); - removeReplyTimeout(accountId, reply); + removeReplyTimeout(m_accountId, reply); if (isError) { - SOCIALD_LOG_ERROR("error occurred when performing groups request for Google account" << accountId); + SOCIALD_LOG_ERROR("error occurred when performing groups request for Google account" << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); return; } else if (data.isEmpty()) { - SOCIALD_LOG_ERROR("no groups data in reply from Google with account" << accountId); + SOCIALD_LOG_ERROR("no groups data in reply from Google with account" << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); return; } - GoogleContactStream parser(false, accountId); - GoogleContactAtom *atom = parser.parse(data); - - if (!atom) { - SOCIALD_LOG_ERROR("unable to parse groups data from reply from Google using account with id" << accountId); + GooglePeopleApiResponse::ContactGroupsResponse response; + if (!GooglePeopleApiResponse::readResponse(data, &response)) { + SOCIALD_LOG_ERROR("unable to parse groups data from reply from Google using account with id" << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); return; } - const QMap > entrySystemGroups = atom->entrySystemGroups(); - SOCIALD_LOG_TRACE("received information about" << entrySystemGroups.size() << "groups for account" << accountId); - - auto it = entrySystemGroups.find(QStringLiteral("Contacts")); - if (it != entrySystemGroups.constEnd()) { - // we have found the atom id of the group we need to upload new contacts to. - const QString myContactsGroupAtomId = it.value().first; - const QString myContactsGroupAtomTitle = it.value().second; - - QContactCollection collection; - collection.setMetaData(QContactCollection::KeyName, myContactsGroupAtomTitle); - collection.setMetaData(QContactCollection::KeyDescription, QStringLiteral("Google - Contacts")); - collection.setMetaData(QContactCollection::KeyColor, QStringLiteral("tomato")); - collection.setMetaData(QContactCollection::KeySecondaryColor, QStringLiteral("royalblue")); - collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_APPLICATIONNAME, QCoreApplication::applicationName()); - collection.setExtendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID, accountId); - collection.setExtendedMetaData(CollectionKeyMyContacts, true); - - if (myContactsGroupAtomId.isEmpty()) { - // We don't consider this a fatal error, - // instead, we just refuse to upsync new contacts. - SOCIALD_LOG_INFO("the My Contacts group was found, but atom id not parsed correctly for account:" << accountId); - } else { - collection.setExtendedMetaData(CollectionKeyAtomId, myContactsGroupAtomId); - SOCIALD_LOG_TRACE("found atom id" << myContactsGroupAtomId - << "for My Contacts group; continuing contact sync with account" << accountId); + SOCIALD_LOG_TRACE("received information about" << response.contactGroups.size() + << "groups for account" << m_accountId); + + GooglePeople::ContactGroup myContactsGroup; + for (auto it = response.contactGroups.constBegin(); it != response.contactGroups.constEnd(); ++it) { + if (it->isMyContactsGroup()) { + myContactsGroup = *it; + break; } + } + if (!myContactsGroup.resourceName.isEmpty()) { // we can now continue with contact sync. - m_sqliteSync[accountId]->m_collection = collection; - m_sqliteSync[accountId]->remoteCollectionsDetermined(QList() << collection); - - } else if (!atom->nextEntriesUrl().isEmpty()) { + m_collection = myContactsGroup.toCollection(m_accountId); + m_sqliteSync->remoteCollectionsDetermined(QList() << m_collection); + } else if (!response.nextPageToken.isEmpty()) { // request more groups if they exist. - startIndex += SOCIALD_GOOGLE_MAX_CONTACT_ENTRY_RESULTS; - requestData(accountId, startIndex, atom->nextEntriesUrl(), lastSyncTimestamp, ContactGroupRequest); + requestData(ContactGroupRequest, NoContactChangeNotifier, response.nextPageToken); } else { - SOCIALD_LOG_INFO("Cannot find My Contacts group when syncing Google contacts for account:" << accountId); - m_sqliteSync[accountId]->remoteCollectionsDetermined(QList()); + SOCIALD_LOG_INFO("Cannot find My Contacts group when syncing Google contacts for account:" << m_accountId); + m_sqliteSync->remoteCollectionsDetermined(QList()); } - delete atom; - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); } void GoogleTwoWayContactSyncAdaptor::contactsFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); + + if (reply->error() == QNetworkReply::ProtocolInvalidOperationError) { + QNetworkReply *reply = qobject_cast(sender()); + if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 400) { + SOCIALD_LOG_INFO("Will request new sync token, got error from server:" + << reply->readAll()); + DataRequestType requestType = static_cast( + reply->property("requestType").toInt()); + ContactChangeNotifier contactChangeNotifier = static_cast( + reply->property("contactChangeNotifier").toInt()); + m_connectionsListParams.requestSyncToken = true; + m_connectionsListParams.syncToken.clear(); + requestData(requestType, contactChangeNotifier); + decrementSemaphore(m_accountId); + return; + } + } + QByteArray data = reply->readAll(); - int startIndex = reply->property("startIndex").toInt(); - int accountId = reply->property("accountId").toInt(); - QString accessToken = reply->property("accessToken").toString(); - QDateTime lastSyncTimestamp = reply->property("lastSyncTimestamp").toDateTime(); ContactChangeNotifier contactChangeNotifier = static_cast(reply->property("contactChangeNotifier").toInt()); bool isError = reply->property("isError").toBool(); reply->deleteLater(); - removeReplyTimeout(accountId, reply); + removeReplyTimeout(m_accountId, reply); if (isError) { - SOCIALD_LOG_ERROR("error occurred when performing contacts request for Google account" << accountId); + SOCIALD_LOG_ERROR("error occurred when performing contacts request for Google account" + << m_accountId + << ", network error was:" << reply->error() << reply->errorString() + << "HTTP code:" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); setStatus(SocialNetworkSyncAdaptor::Error); - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); return; } else if (data.isEmpty()) { - SOCIALD_LOG_ERROR("no contact data in reply from Google with account" << accountId); + SOCIALD_LOG_ERROR("no contact data in reply from Google with account" << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); return; } - GoogleContactStream parser(false, accountId); - GoogleContactAtom *atom = parser.parse(data); - - if (!atom) { - SOCIALD_LOG_ERROR("unable to parse contacts data from reply from Google using account with id" << accountId); + GooglePeopleApiResponse::PeopleConnectionsListResponse response; + if (!GooglePeopleApiResponse::readResponse(data, &response)) { + SOCIALD_LOG_ERROR("unable to parse contacts data from reply from Google using account with id" + << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); return; } - SOCIALD_LOG_TRACE("received information about" << - atom->entryContacts().size() << "add/mod contacts and " << - atom->deletedEntryContacts().size() << "del contacts" << - "for account" << accountId); + if (!response.nextSyncToken.isEmpty()) { + SOCIALD_LOG_INFO("Received sync token for people.connections.list():" + << response.nextSyncToken); + const QString dateString = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); + m_collection.setExtendedMetaData(CollectionKeySyncToken, response.nextSyncToken); + m_collection.setExtendedMetaData(CollectionKeySyncTokenDate, dateString); + } - GoogleContactSqliteSyncAdaptor *sqliteSync = m_sqliteSync[accountId]; + QList remoteAddModContacts; + QList remoteDelContacts; + response.getContacts(m_accountId, + QList() << m_collection, + &remoteAddModContacts, + &remoteDelContacts); - // for each remote contact, there are some associated XML elements which - // could not be stored in QContactDetail form (eg, link URIs etc). - // build up some datastructures to help us retrieve that information - // when we need it. - const QList > remoteAddModContacts = atom->entryContacts(); - for (const QPair &remoteAddModContact : remoteAddModContacts) { - QContact c = remoteAddModContact.first; - c.setCollectionId(sqliteSync->m_collection.id()); + SOCIALD_LOG_TRACE("received information about" + << remoteAddModContacts.size() << "add/mod contacts and " + << remoteDelContacts.size() << "del contacts" + << "for account" << m_accountId); + for (QContact c : remoteAddModContacts) { const QString guid = c.detail().guid(); // get the saved etag - const QString newEtag = contactEtag(c); + const QString newEtag = GooglePeople::PersonMetadata::etag(c); if (newEtag.isEmpty()) { SOCIALD_LOG_ERROR("No etag found for contact:" << guid); - } else if (newEtag == m_contactEtags[accountId].value(guid)) { + } else if (newEtag == m_contactEtags.value(guid)) { // the etags match, so no remote changes have occurred. // most likely this is a spurious change, however it - // may be the case that we have not yet transformed - // and/or downloaded the avatar for this contact. Check this. - const QContactAvatar avatar = c.detail(); - const QString remoteImageUrl = avatar.imageUrl().toString(); - const QString localFileName = remoteImageUrl.isEmpty() - ? QString() - : avatar.imageUrl().isLocalFile() - ? avatar.imageUrl().toString() - : GoogleContactImageDownloader::staticOutputFile(guid, remoteImageUrl); - // compare to the db contact value. - const QString contactAvatarFile = m_avatarImageUrls[accountId].value(guid); - const QUrl contactAvatarUrl(contactAvatarFile); // scheme will be empty if it's a local file path. - const bool dbContactAvatarIsLocalFile = contactAvatarUrl.scheme().isEmpty() || contactAvatarUrl.isLocalFile(); - if ((!contactAvatarFile.isEmpty() && dbContactAvatarIsLocalFile) - || (contactAvatarFile.isEmpty() && localFileName.isEmpty())) { - // the db contact has already transformed the avatar detail - if (!localFileName.isEmpty() && !QFile::exists(localFileName)) { - // but the avatar image has not yet been downloaded. - SOCIALD_LOG_DEBUG("Remote modification spurious except for missing avatar"); - m_contactAvatars[accountId].insert(guid, remoteImageUrl); // enqueue outstanding avatar. - m_avatarEtags[accountId][guid] = avatar.value(QContactAvatar::FieldMetaData).toString(); - } + // may be the case that we have not yet downloaded the + // avatar for this contact. Check this. + QString remoteAvatarUrl; + QString localAvatarFile; + const QContactAvatar avatar = GooglePeople::Photo::getPrimaryPhoto(c, &remoteAvatarUrl, &localAvatarFile); + + if (!localAvatarFile.isEmpty() && !QFile::exists(localAvatarFile)) { + // the avatar image has not yet been downloaded. + SOCIALD_LOG_DEBUG("Remote modification spurious except for missing avatar" << guid); + m_contactAvatars.insert(guid, remoteAvatarUrl); // enqueue outstanding avatar. + } + if (m_connectionsListParams.syncToken.isEmpty()) { + // This is a fresh sync, so keep the modification. + SOCIALD_LOG_DEBUG("Remote modification for contact:" << guid << "is not spurious, keeping it (this is a fresh sync)"); + } else { + // This is a delta sync and the modification is spurious, so discard the contact. SOCIALD_LOG_DEBUG("Disregarding spurious remote modification for contact:" << guid); continue; - } else { - // the value in the db contact has not been transformed to a local file path. - // the remote modification is spurious, but treat it as real in order - // to cause avatar transformation for the contact. - SOCIALD_LOG_DEBUG("Remote modification required for avatar transform for contact:" << guid); - } - } - - // save the unsupportedElements data - if (!remoteAddModContact.second.isEmpty()) { - if (!saveExtendedDetail(&c, UnsupportedElementsKey, remoteAddModContact.second)) { - SOCIALD_LOG_ERROR("Unable to save unsupported elements data" << remoteAddModContact.second - << "to contact" << c.detail().guid()); } } // put contact into added or modified list - const QMap::iterator contactIdIter = m_contactIds[accountId].find(guid); - if (contactIdIter == m_contactIds[accountId].end()) { - m_remoteAdds[accountId].append(c); + const QHash::iterator contactIdIter = m_contactIds.find(guid); + if (contactIdIter == m_contactIds.end()) { + if (m_sqliteSync->isLocallyDeletedGuid(guid)) { + SOCIALD_LOG_TRACE("New remote contact" << guid << "was locally deleted, ignoring"); + } else { + m_remoteAdds.append(c); + SOCIALD_LOG_TRACE("New remote contact" << guid); + } } else { c.setId(QContactId::fromString(contactIdIter.value())); - m_remoteMods[accountId].append(c); + m_remoteMods.append(c); + SOCIALD_LOG_TRACE("Found modified contact " << guid << ", etag now" << newEtag); } } - const QList remoteDelContacts = atom->deletedEntryContacts(); - for (QContact c : remoteDelContacts) { + for (auto it = remoteDelContacts.begin(); it != remoteDelContacts.end(); ++it) { + QContact c = *it; const QString guid = c.detail().guid(); - const QString idStr = m_contactIds[accountId].value(guid); + const QString idStr = m_contactIds.value(guid); if (idStr.isEmpty()) { SOCIALD_LOG_ERROR("Unable to find deleted contact with guid: " << guid); } else { c.setId(QContactId::fromString(idStr)); - c.setCollectionId(sqliteSync->m_collection.id()); - m_contactAvatars[accountId].remove(guid); // just in case the avatar was outstanding. - m_remoteDels[accountId].append(c); + m_contactAvatars.remove(guid); // just in case the avatar was outstanding. + m_remoteDels.append(c); } } - if (!atom->nextEntriesUrl().isEmpty()) { + if (!response.nextPageToken.isEmpty()) { // request more if they exist. - startIndex += SOCIALD_GOOGLE_MAX_CONTACT_ENTRY_RESULTS; - SOCIALD_LOG_TRACE("more contact sync information is available server-side; performing another request with account" << accountId); - requestData(accountId, startIndex, atom->nextEntriesUrl(), lastSyncTimestamp, ContactRequest, contactChangeNotifier); + SOCIALD_LOG_TRACE("more contact sync information is available server-side; performing another request with account" << m_accountId); + requestData(ContactRequest, contactChangeNotifier, response.nextPageToken); } else { // we're finished downloading the remote changes - we should sync local changes up. - SOCIALD_LOG_INFO("Google contact sync with account" << accountId << + SOCIALD_LOG_INFO("Google contact sync with account" << m_accountId << "got remote changes: A/M/R:" - << m_remoteAdds[accountId].count() - << m_remoteMods[accountId].count() - << m_remoteDels[accountId].count()); + << m_remoteAdds.count() + << m_remoteMods.count() + << m_remoteDels.count()); - continueSync(accountId, accessToken, contactChangeNotifier); + continueSync(contactChangeNotifier); } - delete atom; - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); } -void GoogleTwoWayContactSyncAdaptor::continueSync(int accountId, const QString &accessToken, ContactChangeNotifier contactChangeNotifier) +void GoogleTwoWayContactSyncAdaptor::continueSync(ContactChangeNotifier contactChangeNotifier) { - Q_UNUSED(accessToken) - // early out in case we lost connectivity if (syncAborted()) { - SOCIALD_LOG_ERROR("aborting sync of account" << accountId); + SOCIALD_LOG_ERROR("aborting sync of account" << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); // note: don't decrement here - it's done by contactsFinishedHandler(). return; } - // transform the avatars of the remote contacts before storing to the local database - transformContactAvatars(m_remoteAdds[accountId], accountId, m_accessTokens[accountId]); - transformContactAvatars(m_remoteMods[accountId], accountId, m_accessTokens[accountId]); + // avatars of the added and modified contacts will need to be downloaded + for (int i = 0; i < m_remoteAdds.size(); ++i) { + addAvatarToDownload(&m_remoteAdds[i]); + } + for (int i = 0; i < m_remoteMods.size(); ++i) { + addAvatarToDownload(&m_remoteMods[i]); + } // now store the changes locally - SOCIALD_LOG_TRACE("storing remote changes locally for account" << accountId); - GoogleContactSqliteSyncAdaptor *sqliteSync = m_sqliteSync[accountId]; + SOCIALD_LOG_TRACE("storing remote changes locally for account" << m_accountId); + if (contactChangeNotifier == DetermineRemoteContactChanges) { - sqliteSync->remoteContactChangesDetermined(sqliteSync->m_collection, - m_remoteAdds[accountId], - m_remoteMods[accountId], - m_remoteDels[accountId]); + m_sqliteSync->remoteContactChangesDetermined(m_collection, + m_remoteAdds, + m_remoteMods, + m_remoteDels); } else { - sqliteSync->remoteContactsDetermined(sqliteSync->m_collection, m_remoteAdds[accountId] + m_remoteMods[accountId]); + m_sqliteSync->remoteContactsDetermined(m_collection, m_remoteAdds + m_remoteMods); } } -void GoogleTwoWayContactSyncAdaptor::upsyncLocalChanges(const QDateTime &localSince, - const QList &locallyAdded, +void GoogleTwoWayContactSyncAdaptor::upsyncLocalChanges(const QList &locallyAdded, const QList &locallyModified, - const QList &locallyDeleted, - int accId) + const QList &locallyDeleted) { QSet alreadyEncoded; // shouldn't be necessary, as determineLocalChanges should already ensure distinct result sets. for (const QContact &c : locallyDeleted) { const QString &guid = c.detail().guid(); - m_localDels[accId].append(c); - m_contactAvatars[accId].remove(guid); // just in case the avatar was outstanding. - alreadyEncoded.insert(guid); + if (!guid.isEmpty()) { + m_localDels.append(c); + m_contactAvatars.remove(guid); // just in case the avatar was outstanding. + alreadyEncoded.insert(guid); + } else { + SOCIALD_LOG_INFO("Ignore locally-deleted contact" << c.id() + << ", was not uploaded to server prior to local deletion"); + } } for (const QContact &c : locallyAdded) { const QString guid = c.detail().guid(); if (!alreadyEncoded.contains(guid)) { - m_localAdds[accId].append(c); - alreadyEncoded.insert(guid); + m_localAdds.append(c); + if (!guid.isEmpty()) { + alreadyEncoded.insert(guid); + } + + QString remoteAvatarUrl; + QString localAvatarFile; + GooglePeople::Photo::getPrimaryPhoto(c, &remoteAvatarUrl, &localAvatarFile); + if (remoteAvatarUrl.isEmpty() && !localAvatarFile.isEmpty()) { + // The avatar was created locally and needs to be uploaded. + SOCIALD_LOG_TRACE("Will upsync avatar for new contact" << guid); + m_localAvatarAdds.append(c); + } } } for (const QContact &c : locallyModified) { - if (!alreadyEncoded.contains(c.detail().guid())) { - m_localMods[accId].append(c); + const QString guid = c.detail().guid(); + if (!alreadyEncoded.contains(guid)) { + m_localMods.append(c); + + // Determine the type of avatar change to be uploaded. + QString remoteAvatarUrl; + QString localAvatarFile; + const QContactAvatar avatar = GooglePeople::Photo::getPrimaryPhoto(c, &remoteAvatarUrl, &localAvatarFile); + const int changeFlag = avatar.value(QContactDetail__FieldChangeFlags).toInt(); + + if (changeFlag & QContactDetail__ChangeFlag_IsDeleted) { + SOCIALD_LOG_TRACE("Will upsync avatar deletion for contact" << guid); + m_localAvatarDels.append(c); + } else if ((changeFlag & QContactDetail__ChangeFlag_IsAdded) + || (changeFlag & QContactDetail__ChangeFlag_IsModified)) { + if (localAvatarFile.isEmpty()) { + SOCIALD_LOG_TRACE("Will upsync avatar deletion for contact" << guid); + m_localAvatarDels.append(c); + } else { + SOCIALD_LOG_TRACE("Will upsync avatar modification for contact" << guid); + // This is a local file, so upload it. The server will generate a remote image + // url for it and provide the url in the response, that we then can download. + // Note that the contact is added to m_localAvatarMods and not m_localAvatarAdds + // even if it is a new avatar file, because this is for an existing contact, + // not a new contact. + m_localAvatarMods.append(c); + } + } } } - m_batchUpdateIndexes[accId].clear(); + m_batchUpdateIndexes.clear(); - SOCIALD_LOG_INFO("Google account:" << accId << - "upsyncing local A/M/R:" << locallyAdded.count() << "/" << locallyModified.count() << "/" << locallyDeleted.count() << - "since:" << localSince.toString(Qt::ISODate)); + SOCIALD_LOG_INFO("Google account:" << m_accountId << + "upsyncing local contact A/M/R:" + << m_localAdds.count() << "/" + << m_localMods.count() << "/" + << m_localDels.count() + << "and local avatar A/M/R:" + << m_localAvatarAdds.count() << "/" + << m_localAvatarMods.count() << "/" + << m_localAvatarDels.count()); - upsyncLocalChangesList(accId); + upsyncLocalChangesList(); } -bool GoogleTwoWayContactSyncAdaptor::batchRemoteChanges(int accountId, - BatchedUpdate *batchedUpdate, +bool GoogleTwoWayContactSyncAdaptor::batchRemoteChanges(BatchedUpdate *batchedUpdate, QList *contacts, - GoogleContactStream::UpdateType updateType) + GooglePeopleApi::OperationType updateType) { - int batchUpdateIndex = m_batchUpdateIndexes[accountId].value(updateType, contacts->count() - 1); + int batchUpdateIndex = m_batchUpdateIndexes.value(updateType, contacts->count() - 1); while (batchUpdateIndex >= 0 && batchUpdateIndex < contacts->count()) { const QContact &contact = contacts->at(batchUpdateIndex--); - m_batchUpdateIndexes[accountId][updateType] = batchUpdateIndex; - - QStringList extraXmlElements; - for (const QContactExtendedDetail &detail : contact.details()) { - if (detail.name() == UnsupportedElementsKey) { - extraXmlElements = detail.data().toStringList(); - break; - } - } - - if (updateType == GoogleContactStream::Add) { - // new contacts need to be inserted into the My Contacts group - GoogleContactSqliteSyncAdaptor *sqliteSync = m_sqliteSync[accountId]; - QString myContactsGroupAtomId = collectionAtomId(sqliteSync->m_collection); - if (myContactsGroupAtomId.isEmpty()) { - SOCIALD_LOG_INFO("skipping upload of locally added contact" << contact.id().toString() << - "to account" << accountId << "due to unknown My Contacts group atom id"); + m_batchUpdateIndexes[updateType] = batchUpdateIndex; + batchedUpdate->batch[updateType].append(contact); + batchedUpdate->batchCount++; + + if (batchUpdateIndex <= 0) { + const QByteArray encodedContactUpdates = + GooglePeopleApiRequest::writeMultiPartRequest(batchedUpdate->batch); + if (encodedContactUpdates.isEmpty()) { + SOCIALD_LOG_INFO("No data changes found, no non-avatar changes to upsync for contact" + << contact.id() << "guid" << contact.detail().guid()); } else { - extraXmlElements.append(QStringLiteral("").arg(myContactsGroupAtomId)); - batchedUpdate->batch.insertMulti(updateType, qMakePair(contact, extraXmlElements)); - batchedUpdate->batchCount++; + SOCIALD_LOG_TRACE("storing a batch of" << batchedUpdate->batchCount + << "local changes to remote server for account" << m_accountId); } - } else { - batchedUpdate->batch.insertMulti(updateType, qMakePair(contact, extraXmlElements)); - batchedUpdate->batchCount++; - } - - if (batchedUpdate->batchCount == SOCIALD_GOOGLE_MAX_CONTACT_ENTRY_RESULTS - || batchUpdateIndex <= 0) { - GoogleContactStream encoder(false, accountId, m_emailAddresses[accountId]); - QByteArray encodedContactUpdates = encoder.encode(batchedUpdate->batch); - SOCIALD_LOG_TRACE("storing a batch of" << batchedUpdate->batchCount - << "local changes to remote server for account" << accountId); batchedUpdate->batch.clear(); batchedUpdate->batchCount = 0; - storeToRemote(accountId, m_accessTokens[accountId], encodedContactUpdates); - return true; + if (!encodedContactUpdates.isEmpty()) { + storeToRemote(encodedContactUpdates); + return true; + } } } return false; } -void GoogleTwoWayContactSyncAdaptor::upsyncLocalChangesList(int accountId) +void GoogleTwoWayContactSyncAdaptor::upsyncLocalChangesList() { bool postedData = false; if (!m_accountSyncProfile || m_accountSyncProfile->syncDirection() != Buteo::SyncProfile::SYNC_DIRECTION_FROM_REMOTE) { // two-way sync is the default setting. Upsync the changes. BatchedUpdate batch; - postedData = batchRemoteChanges(accountId, &batch, &m_localMods[accountId], GoogleContactStream::Modify); if (!postedData) { - postedData = batchRemoteChanges(accountId, &batch, &m_localAdds[accountId], GoogleContactStream::Add); + postedData = batchRemoteChanges(&batch, &m_localAdds, GooglePeopleApi::CreateContact); + } + if (!postedData) { + postedData = batchRemoteChanges(&batch, &m_localMods, GooglePeopleApi::UpdateContact); + } + if (!postedData) { + postedData = batchRemoteChanges(&batch, &m_localDels, GooglePeopleApi::DeleteContact); + } + if (!postedData) { + // The avatar additions must be sent after the CreateContact calls, so that we have a + // valid Person resourceName to attach to the UpdateContactPhoto call. + postedData = batchRemoteChanges(&batch, &m_localAvatarAdds, GooglePeopleApi::AddContactPhoto); + } + if (!postedData) { + postedData = batchRemoteChanges(&batch, &m_localAvatarMods, GooglePeopleApi::UpdateContactPhoto); } if (!postedData) { - postedData = batchRemoteChanges(accountId, &batch, &m_localDels[accountId], GoogleContactStream::Remove); + postedData = batchRemoteChanges(&batch, &m_localAvatarDels, GooglePeopleApi::DeleteContactPhoto); } } else { - SOCIALD_LOG_INFO("skipping upload of local contacts changes due to profile direction setting for account" << accountId); + SOCIALD_LOG_INFO("skipping upload of local contacts changes due to profile direction setting for account" << m_accountId); } if (!postedData) { SOCIALD_LOG_INFO("All upsync requests sent"); - // Nothing left to upsync. Save the etags and other response data from the server. - for (auto it = m_contactUpsyncResponses[accountId].constBegin(); - it != m_contactUpsyncResponses[accountId].constEnd(); ++it) { - const QContactId contactId = QContactId::fromString(it.key()); - const int addListIndex = indexOfContact(m_localAdds[accountId], contactId); - int modListIndex = -1; - if (addListIndex < 0) { - modListIndex = indexOfContact(m_localMods[accountId], contactId); - } - if (addListIndex < 0 && modListIndex < 0) { - SOCIALD_LOG_ERROR("Cannot save details, contact " << contactId << " not found in added/modified contacts"); - continue; - } - - QContact &c = addListIndex >= 0 - ? m_localAdds[accountId][addListIndex] - : m_localMods[accountId][modListIndex]; - const ContactUpsyncResponse &response = it.value(); - - if (c.detail().guid() != response.guid) { - QContactGuid guid; - guid.setGuid(response.guid); - if (!c.saveDetail(&guid)) { - SOCIALD_LOG_ERROR("Unable to save guid " << response.guid - << " to contact " << contactId); - } - } - if (!saveExtendedDetail(&c, EtagKey, response.etag)) { - SOCIALD_LOG_ERROR("Unable to save etag " << response.etag - << " to contact" << contactId); - } - if (!saveExtendedDetail(&c, UnsupportedElementsKey, response.unsupportedElements)) { - SOCIALD_LOG_ERROR("Unable to save unsupported elements data" << response.unsupportedElements - << "to contact" << contactId); - } - } - - // Save the sync timestamp. - GoogleContactSqliteSyncAdaptor *sqliteSync = m_sqliteSync.value(accountId); - if (!sqliteSync->m_syncDateTime.isValid()) { - SOCIALD_LOG_ERROR("Last sync time is not set for account " << accountId); - } else { - sqliteSync->m_collection.setExtendedMetaData(CollectionKeyLastSync, sqliteSync->m_syncDateTime); - SOCIALD_LOG_INFO("Saved sync timestamp: " << sqliteSync->m_syncDateTime); - } - + // Nothing left to upsync. // notify TWCSA that the upsync is complete. - m_sqliteSync[accountId]->localChangesStoredRemotely(sqliteSync->m_collection, - m_localAdds[accountId], - m_localMods[accountId]); + m_sqliteSync->localChangesStoredRemotely(m_collection, m_localAdds, m_localMods); } } -void GoogleTwoWayContactSyncAdaptor::storeToRemote(int accountId, const QString &accessToken, const QByteArray &encodedContactUpdates) +void GoogleTwoWayContactSyncAdaptor::storeToRemote(const QByteArray &encodedContactUpdates) { - QUrl requestUrl(QUrl(QString(QLatin1String("https://www.google.com/m8/feeds/contacts/default/full/batch")))); + QUrl requestUrl(QLatin1String("https://people.googleapis.com/batch")); QNetworkRequest req(requestUrl); - req.setRawHeader("GData-Version", "3.0"); req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), - QString(QLatin1String("Bearer ") + accessToken).toUtf8()); + QString(QLatin1String("Bearer ") + m_accessToken).toUtf8()); + req.setRawHeader(QString(QLatin1String("Authorization")).toUtf8(), + QString(QLatin1String("Bearer ") + m_accessToken).toUtf8()); req.setRawHeader(QString(QLatin1String("Content-Type")).toUtf8(), - QString(QLatin1String("application/atom+xml; charset=UTF-8; type=feed")).toUtf8()); + QString(QLatin1String("multipart/mixed; boundary=\"batch_people\"")).toUtf8()); req.setHeader(QNetworkRequest::ContentLengthHeader, encodedContactUpdates.size()); - req.setRawHeader(QString(QLatin1String("If-Match")).toUtf8(), - QString(QLatin1String("*")).toUtf8()); // we're posting data. Increment the semaphore so that we know we're still busy. - incrementSemaphore(accountId); + incrementSemaphore(m_accountId); QNetworkReply *reply = m_networkAccessManager->post(req, encodedContactUpdates); if (reply) { - reply->setProperty("accountId", accountId); - reply->setProperty("accessToken", accessToken); connect(reply, &QNetworkReply::finished, this, &GoogleTwoWayContactSyncAdaptor::postFinishedHandler); connect(reply, static_cast(&QNetworkReply::error), this, &GoogleTwoWayContactSyncAdaptor::postErrorHandler); connect(reply, &QNetworkReply::sslErrors, this, &GoogleTwoWayContactSyncAdaptor::postErrorHandler); - m_apiRequestsRemaining[accountId] = m_apiRequestsRemaining[accountId] - 1; - setupReplyTimeout(accountId, reply); + m_apiRequestsRemaining -= 1; + setupReplyTimeout(m_accountId, reply); } else { - SOCIALD_LOG_ERROR("unable to post contacts to Google account with id" << accountId); + SOCIALD_LOG_ERROR("unable to post contacts to Google account with id" << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); } } @@ -869,53 +782,142 @@ void GoogleTwoWayContactSyncAdaptor::postFinishedHandler() { QNetworkReply *reply = qobject_cast(sender()); QByteArray response = reply->readAll(); - int accountId = reply->property("accountId").toInt(); reply->deleteLater(); - removeReplyTimeout(accountId, reply); + removeReplyTimeout(m_accountId, reply); if (reply->property("isError").toBool()) { - SOCIALD_LOG_ERROR("error occurred posting contact data to google with account" << accountId << "," << + SOCIALD_LOG_ERROR("error occurred posting contact data to google with account" << m_accountId << "," << "got response:" << QString::fromUtf8(response)); setStatus(SocialNetworkSyncAdaptor::Error); - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); + return; + } + + QList operationResponses; + if (!GooglePeopleApiResponse::readMultiPartResponse(response, &operationResponses)) { + SOCIALD_LOG_ERROR("unable to read response for batch operation with Google account" << m_accountId); + setStatus(SocialNetworkSyncAdaptor::Error); + decrementSemaphore(m_accountId); return; } - GoogleContactStream parser(false, accountId); - GoogleContactAtom *atom = parser.parse(response); - QMap operationResponses = atom->batchOperationResponses(); + const QList collections { m_collection }; bool errorOccurredInBatch = false; - foreach (const GoogleContactAtom::BatchOperationResponse &response, operationResponses) { - if (response.isError) { - errorOccurredInBatch = true; - SOCIALD_LOG_DEBUG("batch operation error:\n" - " id: " << response.operationId << "\n" - " type: " << response.type << "\n" - " code: " << response.code << "\n" - " reason: " << response.reason << "\n" - " descr: " << response.reasonDescription << "\n"); - } else { - // Save etag and other data to save them into the contact later - if (!response.etag.isEmpty()) { - ContactUpsyncResponse responseInfo = { response.unsupportedElements, response.contactGuid, response.etag }; - m_contactUpsyncResponses[accountId].insert(response.operationId, responseInfo); + + for (const GooglePeopleApiResponse::BatchResponsePart &response : operationResponses) { + GooglePeopleApi::OperationType operationType; + QString contactIdString; + GooglePeople::Person person; + GooglePeopleApiResponse::BatchResponsePart::Error error; + response.parse(&operationType, &contactIdString, &person, &error); + + if (!error.status.isEmpty()) { + if (error.code == 404 + && (operationType == GooglePeopleApi::DeleteContact + || operationType == GooglePeopleApi::DeleteContactPhoto)) { + // Couldn't find the remote contact or photo to be deleted; perhaps some previous + // change was not synced as expected. This is not a problem as we will just delete + // it locally. + SOCIALD_LOG_INFO("Unable to delete contact or photo on the server, will just delete it locally." + << "id:" << contactIdString + << "resource:" << person.resourceName); + } else { + errorOccurredInBatch = true; + SOCIALD_LOG_ERROR("batch operation error:\n" + " contentId: " << response.contentId << "\n" + " error.code: " << error.code << "\n" + " error.message: " << error.message << "\n" + " error.status: " << error.status << "\n"); + } + } + + if (errorOccurredInBatch) { + // The sync will finish with an error. Keep looking for other possible errors, but + // don't process any more responses. + continue; + } + + SOCIALD_LOG_TRACE("Process response for batched request" << response.contentId + << "status =" << response.bodyStatusLine + << "body len =" << response.body.length()); + if (!person.resourceName.isEmpty()) { + SOCIALD_LOG_DEBUG("Batched response contains Person(resourceName =" + << person.resourceName << ")"); + } + + // Save contact etag and other details into the added/modified lists so that the + // updated details are saved into the database later. + QList *contactList = nullptr; + switch (operationType) { + case GooglePeopleApi::CreateContact: + case GooglePeopleApi::AddContactPhoto: + contactList = &m_localAdds; + break; + case GooglePeopleApi::UpdateContact: + case GooglePeopleApi::UpdateContactPhoto: + case GooglePeopleApi::DeleteContactPhoto: + contactList = &m_localMods; + break; + case GooglePeopleApi::DeleteContact: + // Nothing to do, the response body will be empty. + break; + case GooglePeopleApi::UnsupportedOperation: + break; + } + + if (contactList) { + if (!person.isValid()) { + SOCIALD_LOG_ERROR("Cannot read Person object!"); + SOCIALD_LOG_TRACE("Response data was:" << response.body); + continue; + } + + const QContactId contactId = QContactId::fromString(contactIdString); + const int listIndex = indexOfContact(*contactList, contactId); + if (listIndex < 0) { + SOCIALD_LOG_ERROR("Cannot save details, contact" << contactId.toString() + << " not found in added/modified contacts"); + continue; + } + + QContact *contact = &((*contactList)[listIndex]); + if (!person.saveToContact(contact, m_accountId, collections)) { + SOCIALD_LOG_ERROR("Cannot save added/modified details for contact" + << contactId.toString()); + continue; + } + + if (operationType == GooglePeopleApi::CreateContact) { + // The contact has now been assigned a resourceName from the Google server. + // If the contact has an avatar to be uploaded in a later batch, update the + // guid for the contact in m_localAvatarAdds to ensure the resourceName is + // valid when the avatar is uploaded. + const int avatarAddIndex = indexOfContact(m_localAvatarAdds, contact->id()); + if (avatarAddIndex >= 0) { + QContactGuid guid = contact->detail(); + m_localAvatarAdds[avatarAddIndex].saveDetail(&guid); + } + } else if (operationType == GooglePeopleApi::AddContactPhoto + || operationType == GooglePeopleApi::UpdateContactPhoto) { + // When a contact photo is uploaded to the server, the person's "photos" is + // updated with a new remote url for the avatar; add this url to the list of + // avatars to be downloaded later. + addAvatarToDownload(contact); } } } if (errorOccurredInBatch) { - SOCIALD_LOG_ERROR("error occurred during batch operation with Google account" << accountId); + SOCIALD_LOG_ERROR("error occurred during batch operation with Google account" << m_accountId); setStatus(SocialNetworkSyncAdaptor::Error); - decrementSemaphore(accountId); - return; + } else { + // continue with more, if there were more than one page of updates to post. + upsyncLocalChangesList(); } - // continue with more, if there were more than one page of updates to post. - upsyncLocalChangesList(accountId); - // finished with this request, so decrementing semaphore. - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); } void GoogleTwoWayContactSyncAdaptor::postErrorHandler() @@ -923,36 +925,48 @@ void GoogleTwoWayContactSyncAdaptor::postErrorHandler() sender()->setProperty("isError", QVariant::fromValue(true)); } -void GoogleTwoWayContactSyncAdaptor::downloadAvatars(int accountId) +void GoogleTwoWayContactSyncAdaptor::syncFinished() { + // If this is the first sync, TWCSA will have saved the collection and given it a valid id, so + // update collection so that any post-sync operations (e.g. saving of queued avatar downloads) + // will refer to a valid collection. + if (m_collection.id().isNull()) { + const QContactCollection savedCollection = findCollection(*m_contactManager, m_accountId); + if (savedCollection.id().isNull()) { + SOCIALD_LOG_ERROR("Error: cannot find saved My Contacts collection!"); + } else { + m_collection.setId(savedCollection.id()); + } + } + // Attempt to download any outstanding avatars. - queueOutstandingAvatars(accountId, m_accessTokens[accountId]); + queueOutstandingAvatars(); } -void GoogleTwoWayContactSyncAdaptor::queueOutstandingAvatars(int accountId, const QString &accessToken) +void GoogleTwoWayContactSyncAdaptor::queueOutstandingAvatars() { int queuedCount = 0; - for (QMap::const_iterator it = m_contactAvatars[accountId].constBegin(); - it != m_contactAvatars[accountId].constEnd(); ++it) { - if (!it.value().isEmpty() && queueAvatarForDownload(accountId, accessToken, it.key(), it.value())) { + for (QHash::const_iterator it = m_contactAvatars.constBegin(); + it != m_contactAvatars.constEnd(); ++it) { + if (!it.value().isEmpty() && queueAvatarForDownload(it.key(), it.value())) { queuedCount++; } } - SOCIALD_LOG_DEBUG("queued" << queuedCount << "outstanding avatars for download for account" << accountId); + SOCIALD_LOG_TRACE("queued" << queuedCount << "outstanding avatars for download for account" + << m_accountId); } -bool GoogleTwoWayContactSyncAdaptor::queueAvatarForDownload(int accountId, const QString &accessToken, const QString &contactGuid, const QString &imageUrl) +bool GoogleTwoWayContactSyncAdaptor::queueAvatarForDownload(const QString &contactGuid, const QString &imageUrl) { - if (m_apiRequestsRemaining[accountId] > 0 && !m_queuedAvatarsForDownload[accountId].contains(contactGuid)) { - m_apiRequestsRemaining[accountId] = m_apiRequestsRemaining[accountId] - 1; - m_queuedAvatarsForDownload[accountId][contactGuid] = imageUrl; + if (m_apiRequestsRemaining > 0 && !m_queuedAvatarsForDownload.contains(contactGuid)) { + m_apiRequestsRemaining -= 1; + m_queuedAvatarsForDownload[contactGuid] = imageUrl; QVariantMap metadata; - metadata.insert(IMAGE_DOWNLOADER_ACCOUNT_ID_KEY, accountId); - metadata.insert(IMAGE_DOWNLOADER_TOKEN_KEY, accessToken); + metadata.insert(IMAGE_DOWNLOADER_TOKEN_KEY, m_accessToken); metadata.insert(IMAGE_DOWNLOADER_IDENTIFIER_KEY, contactGuid); - incrementSemaphore(accountId); + incrementSemaphore(m_accountId); QMetaObject::invokeMethod(m_workerObject, "queue", Qt::QueuedConnection, Q_ARG(QString, imageUrl), Q_ARG(QVariantMap, metadata)); return true; @@ -961,76 +975,56 @@ bool GoogleTwoWayContactSyncAdaptor::queueAvatarForDownload(int accountId, const return false; } -void GoogleTwoWayContactSyncAdaptor::transformContactAvatars(QList &remoteContacts, int accountId, const QString &) +bool GoogleTwoWayContactSyncAdaptor::addAvatarToDownload(QContact *contact) { // The avatar detail from the remote contact will be of the form: - // https://www.google.com/m8/feeds/photos/media/user@gmail.com/userId - // We need to: - // 1) transform this to a local filename. - // 2) determine if the local file exists. - // 3) if not, trigger downloading the avatar. - - for (int i = 0; i < remoteContacts.size(); ++i) { - QContact &curr(remoteContacts[i]); - - // We only deal with the first avatar from the contact. If it has multiple, - // then later avatars will not be transformed. TODO: fix this. - // We also only bother to do this for contacts with a GUID, as we don't - // store locally any contact without one. - const QString contactGuid = curr.detail().guid(); - if (contactGuid.isEmpty()) { - continue; - } + // https://.googleusercontent.com//photo.jpg" + // (The server will generate a new URL whenever the photo content changes, so there is no need + // to store a photo etag to track changes.) + // If the remote URL has changed, or the file has not been downloaded, then add it to the + // list of pending avatar downloads. + + if (!contact) { + return false; + } - QContactAvatar avatar = curr.detail(); - const QString remoteImageUrl = avatar.imageUrl().toString(); + const QString contactGuid = contact->detail().guid(); + if (contactGuid.isEmpty()) { + return false; + } - if (remoteImageUrl.isEmpty()) { - // If the contact previously had an avatar, remove it. - const QString savedLocalFile = m_avatarImageUrls[accountId].value(contactGuid); - if (!savedLocalFile.isEmpty()) { - QFile::remove(savedLocalFile); - } - } else { - const bool isLocalFile = avatar.imageUrl().scheme().isEmpty() || avatar.imageUrl().isLocalFile(); - const QString localFileName = isLocalFile - ? avatar.imageUrl().toString() - : GoogleContactImageDownloader::staticOutputFile(contactGuid, remoteImageUrl); - const QString prevAvatarEtag = m_avatarEtags[accountId].value(contactGuid); - const QString newAvatarEtag = avatar.value(QContactAvatar::FieldMetaData).toString(); - const bool isNewAvatar = prevAvatarEtag.isEmpty(); - const bool isModifiedAvatar = !isNewAvatar && prevAvatarEtag != newAvatarEtag; - const bool isMissingFile = !QFile::exists(localFileName); - - if (!isNewAvatar && !isModifiedAvatar && !isMissingFile && isLocalFile) { - // Shouldn't happen as we won't get an avatar in the atom if it didn't change. - continue; - } + QString remoteAvatarUrl; + QString localAvatarFile; + const QContactAvatar avatar = GooglePeople::Photo::getPrimaryPhoto( + *contact, &remoteAvatarUrl, &localAvatarFile); - // We have a remote avatar which we need to download. - if (!isLocalFile) { - QFile::remove(localFileName); + const QPair prevAvatar = m_previousAvatarUrls.value(contactGuid); + const QString prevRemoteAvatarUrl = prevAvatar.first; + const QString prevLocalAvatarFile = prevAvatar.second; - // Save the avatar detail even though the image is not yet downloaded. It is - // downloaded after the sync transaction is written to the database. - avatar.setImageUrl(QUrl::fromLocalFile(localFileName)); - if (!curr.saveDetail(&avatar, QContact::IgnoreAccessConstraints)) { - SOCIALD_LOG_ERROR("Unable to transform avatar detail"); - } + const bool isNewAvatar = prevRemoteAvatarUrl.isEmpty(); + const bool isModifiedAvatar = !isNewAvatar && prevRemoteAvatarUrl != remoteAvatarUrl; + const bool isMissingFile = !QFile::exists(localAvatarFile); - // queue outstanding avatar for download once all upsyncs are complete - m_contactAvatars[accountId].insert(contactGuid, remoteImageUrl); - m_avatarEtags[accountId][contactGuid] = newAvatarEtag; - } - } + if (!isNewAvatar && !isModifiedAvatar && !isMissingFile) { + // No need to download the file. + return false; + } + + if (!prevLocalAvatarFile.isEmpty()) { + QFile::remove(prevLocalAvatarFile); } + + // queue outstanding avatar for download once all upsyncs are complete + m_contactAvatars.insert(contactGuid, remoteAvatarUrl); + + return true; } void GoogleTwoWayContactSyncAdaptor::imageDownloaded(const QString &url, const QString &path, const QVariantMap &metadata) { // Load finished, update the avatar, decrement semaphore - int accountId = metadata.value(IMAGE_DOWNLOADER_ACCOUNT_ID_KEY).toInt(); QString contactGuid = metadata.value(IMAGE_DOWNLOADER_IDENTIFIER_KEY).toString(); // Empty path signifies that an error occurred. @@ -1038,11 +1032,11 @@ void GoogleTwoWayContactSyncAdaptor::imageDownloaded(const QString &url, const Q SOCIALD_LOG_ERROR("Unable to download avatar" << url); } else { // no longer outstanding. - m_contactAvatars[accountId].remove(contactGuid); - m_queuedAvatarsForDownload[accountId].remove(contactGuid); + m_contactAvatars.remove(contactGuid); + m_queuedAvatarsForDownload.remove(contactGuid); } - decrementSemaphore(accountId); + decrementSemaphore(m_accountId); } void GoogleTwoWayContactSyncAdaptor::purgeAccount(int pid) @@ -1083,11 +1077,11 @@ void GoogleTwoWayContactSyncAdaptor::purgeAccount(int pid) << QContactDetail::TypeAvatar); const QList savedContacts = m_contactManager->contacts(collectionFilter, QList(), fetchHint); for (const QContact &contact : savedContacts) { - const QContactAvatar avatar = contact.detail(); - const QString imageUrl = avatar.imageUrl().toString(); - if (!imageUrl.isEmpty()) { - if (!QFile::remove(imageUrl)) { - SOCIALD_LOG_ERROR("Failed to remove avatar:" << imageUrl); + const QList avatars = contact.details(); + for (const QContactAvatar &avatar : avatars) { + const QString localFilePath = avatar.imageUrl().toString(); + if (!localFilePath.isEmpty() && !QFile::remove(localFilePath)) { + SOCIALD_LOG_ERROR("Failed to remove avatar:" << localFilePath); } } } @@ -1114,7 +1108,8 @@ void GoogleTwoWayContactSyncAdaptor::purgeAccount(int pid) void GoogleTwoWayContactSyncAdaptor::finalize(int accountId) { - if (m_accessTokens[accountId].isEmpty() + if (accountId != m_accountId + || m_accessToken.isEmpty() || syncAborted() || status() == SocialNetworkSyncAdaptor::Error) { // account failure occurred before sync process was started, @@ -1146,10 +1141,10 @@ void GoogleTwoWayContactSyncAdaptor::finalCleanup() QList purgeAccountIds; QList currentAccountIds; QList uaids = m_accountManager->accountList(); - foreach (uint uaid, uaids) { + Q_FOREACH (uint uaid, uaids) { currentAccountIds.append(static_cast(uaid)); } - foreach (int currId, currentAccountIds) { + for (int currId : currentAccountIds) { Accounts::Account *act = Accounts::Account::fromId(m_accountManager, currId, this); if (act) { if (act->providerName() == QString(QLatin1String("google"))) { @@ -1163,7 +1158,7 @@ void GoogleTwoWayContactSyncAdaptor::finalCleanup() // find all account ids from which contacts have been synced const QList collections = m_contactManager->collections(); for (const QContactCollection &collection : collections) { - if (collection.metaData(QContactCollection::KeyName).toString() == MyContactsCollectionName) { + if (GooglePeople::ContactGroup::isMyContactsCollection(collection)) { const int purgeId = collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); if (purgeId && !googleAccountIds.contains(purgeId) && !purgeAccountIds.contains(purgeId)) { // this account no longer exists, and needs to be purged. @@ -1175,7 +1170,7 @@ void GoogleTwoWayContactSyncAdaptor::finalCleanup() // purge all data for those account ids which no longer exist. if (purgeAccountIds.size()) { SOCIALD_LOG_INFO("finalCleanup() purging contacts from" << purgeAccountIds.size() << "non-existent Google accounts"); - foreach (int purgeId, purgeAccountIds) { + for (int purgeId : purgeAccountIds) { purgeAccount(purgeId); } } @@ -1183,19 +1178,12 @@ void GoogleTwoWayContactSyncAdaptor::finalCleanup() void GoogleTwoWayContactSyncAdaptor::loadCollection(const QContactCollection &collection) { - const int accountId = collection.extendedMetaData(COLLECTION_EXTENDEDMETADATA_KEY_ACCOUNTID).toInt(); - QContactCollectionFilter collectionFilter; collectionFilter.setCollectionId(collection.id()); QContactFetchHint noRelationships; noRelationships.setOptimizationHints(QContactFetchHint::NoRelationships); QList savedContacts = m_contactManager->contacts(collectionFilter, QList(), noRelationships); - m_contactEtags[accountId].clear(); - m_contactIds[accountId].clear(); - m_avatarEtags[accountId].clear(); - m_contactUpsyncResponses[accountId].clear(); - for (const QContact &contact : savedContacts) { const QString contactGuid = contact.detail().guid(); if (contactGuid.isEmpty()) { @@ -1204,20 +1192,18 @@ void GoogleTwoWayContactSyncAdaptor::loadCollection(const QContactCollection &co } // m_contactEtags - const QString etag = contactEtag(contact); + const QString etag = GooglePeople::PersonMetadata::etag(contact); if (!etag.isEmpty()) { - m_contactEtags[accountId][contactGuid] = etag; + m_contactEtags[contactGuid] = etag; } // m_contactIds - m_contactIds[accountId][contactGuid] = contact.id().toString(); + m_contactIds[contactGuid] = contact.id().toString(); - // m_avatarEtags // m_avatarImageUrls - QContactAvatar avatar = contact.detail(); - if (!avatar.isEmpty()) { - m_avatarEtags[accountId][contactGuid] = avatar.value(QContactAvatar::FieldMetaData).toString(); - m_avatarImageUrls[accountId][contactGuid] = avatar.imageUrl().toString(); - } + QString remoteAvatarUrl; + QString localAvatarFile; + GooglePeople::Photo::getPrimaryPhoto(contact, &remoteAvatarUrl, &localAvatarFile); + m_previousAvatarUrls.insert(contactGuid, qMakePair(remoteAvatarUrl,localAvatarFile)); } } diff --git a/src/google/google-contacts/googletwowaycontactsyncadaptor.h b/src/google/google-contacts/googletwowaycontactsyncadaptor.h index 91e8fb9..c99ef9e 100644 --- a/src/google/google-contacts/googletwowaycontactsyncadaptor.h +++ b/src/google/google-contacts/googletwowaycontactsyncadaptor.h @@ -23,7 +23,7 @@ #define GOOGLETWOWAYCONTACTSYNCADAPTOR_H #include "googledatatypesyncadaptor.h" -#include "googlecontactstream.h" +#include "googlepeopleapi.h" #include @@ -46,7 +46,7 @@ class GoogleContactSqliteSyncAdaptor : public QObject, public QtContactsSqliteEx GoogleContactSqliteSyncAdaptor(int accountId, GoogleTwoWayContactSyncAdaptor *parent); ~GoogleContactSqliteSyncAdaptor(); - int accountId() const; + bool isLocallyDeletedGuid(const QString &guid) const; virtual bool determineRemoteCollections() override; virtual bool deleteRemoteCollection(const QContactCollection &collection) override; @@ -71,12 +71,8 @@ class GoogleContactSqliteSyncAdaptor : public QObject, public QtContactsSqliteEx virtual void syncFinishedSuccessfully() override; virtual void syncFinishedWithError() override; - QContactCollection m_collection; - QDateTime m_syncDateTime; - private: GoogleTwoWayContactSyncAdaptor *q; - int m_accountId = 0; }; class GoogleTwoWayContactSyncAdaptor : public GoogleDataTypeSyncAdaptor @@ -101,91 +97,82 @@ class GoogleTwoWayContactSyncAdaptor : public GoogleDataTypeSyncAdaptor virtual QString syncServiceName() const override; virtual void sync(const QString &dataTypeString, int accountId) override; - void requestData(int accountId, - int startIndex, - const QString &continuationRequest, - const QDateTime &syncTimestamp, - DataRequestType requestType, - ContactChangeNotifier contactChangeNotifier = NoContactChangeNotifier); - void upsyncLocalChanges(const QDateTime &localSince, - const QList &locallyAdded, + void requestData(DataRequestType requestType, + ContactChangeNotifier contactChangeNotifier = NoContactChangeNotifier, + const QString &pageToken = QString()); + void upsyncLocalChanges(const QList &locallyAdded, const QList &locallyModified, - const QList &locallyDeleted, - int accountId); + const QList &locallyDeleted); - void downloadAvatars(int accountId); - - QContactManager *m_contactManager = nullptr; + void syncFinished(); protected: // implementing GoogleDataTypeSyncAdaptor interface - void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode); - void beginSync(int accountId, const QString &accessToken); - void finalize(int accountId); - void finalCleanup(); + void purgeDataForOldAccount(int oldId, SocialNetworkSyncAdaptor::PurgeMode mode) override; + void beginSync(int accountId, const QString &accessToken) override; + void finalize(int accountId) override; + void finalCleanup() override; private: + friend class GoogleContactSqliteSyncAdaptor; + class BatchedUpdate { public: - QMultiMap > batch; + QMap > batch; int batchCount = 0; }; - void determineRemoteChanges(const QDateTime &remoteSince, int accountId); void groupsFinishedHandler(); void contactsFinishedHandler(); - void continueSync(int accountId, - const QString &accessToken, - GoogleTwoWayContactSyncAdaptor::ContactChangeNotifier contactChangeNotifier); - void upsyncLocalChangesList(int accountId); - bool batchRemoteChanges(int accountId, BatchedUpdate *batchedUpdate, - QList *contacts, GoogleContactStream::UpdateType updateType); - void storeToRemote(int accountId, - const QString &accessToken, - const QByteArray &encodedContactUpdates); - void queueOutstandingAvatars(int accountId, const QString &accessToken); - bool queueAvatarForDownload(int accountId, const QString &accessToken, const QString &contactGuid, const QString &imageUrl); - void transformContactAvatars(QList &remoteContacts, int accountId, const QString &accessToken); - void downloadContactAvatarImage(int accountId, const QString &accessToken, const QUrl &imageUrl, const QString &filename); + void continueSync(GoogleTwoWayContactSyncAdaptor::ContactChangeNotifier contactChangeNotifier); + void upsyncLocalChangesList(); + bool batchRemoteChanges(BatchedUpdate *batchedUpdate, + QList *contacts, + GooglePeopleApi::OperationType updateType); + void storeToRemote(const QByteArray &encodedContactUpdates); + void queueOutstandingAvatars(); + bool queueAvatarForDownload(const QString &contactGuid, const QString &imageUrl); + bool addAvatarToDownload(QContact *contact); void imageDownloaded(const QString &url, const QString &path, const QVariantMap &metadata); - void loadCollection(const QContactCollection &collection); void purgeAccount(int pid); void postFinishedHandler(); void postErrorHandler(); - struct ContactUpsyncResponse { - QStringList unsupportedElements; - QString guid; - QString etag; - }; + QList m_remoteAdds; + QList m_remoteMods; + QList m_remoteDels; + QList m_localAdds; + QList m_localMods; + QList m_localDels; + QList m_localAvatarAdds; + QList m_localAvatarMods; + QList m_localAvatarDels; + + QHash m_contactEtags; // contact guid -> contact etag + QHash m_contactIds; // contact guid -> contact id + QHash m_contactAvatars; // contact guid -> remote avatar path + QHash > m_previousAvatarUrls; + QHash m_batchUpdateIndexes; + QHash m_queuedAvatarsForDownload; // contact guid -> remote avatar path + QContactManager *m_contactManager = nullptr; + GoogleContactSqliteSyncAdaptor *m_sqliteSync = nullptr; GoogleContactImageDownloader *m_workerObject = nullptr; - QMap m_sqliteSync; - QMap m_accessTokens; - QMap m_emailAddresses; - - QMap > m_remoteAdds; - QMap > m_remoteMods; - QMap > m_remoteDels; - QMap > m_localAdds; - QMap > m_localMods; - QMap > m_localDels; - - QMap > m_contactEtags; // contact guid -> contact etag - QMap > m_contactIds; // contact guid -> contact id - QMap > m_contactAvatars; // contact guid -> remote avatar path - QMap > m_avatarEtags; - QMap > m_avatarImageUrls; - QMap > m_contactUpsyncResponses; // contact id -> response info - QMap > m_batchUpdateIndexes; + QContactCollection m_collection; + QString m_accessToken; - QMap m_apiRequestsRemaining; - QMap > m_queuedAvatarsForDownload; // contact guid -> remote avatar path + struct PeopleConnectionsListParameters { + bool requestSyncToken; + QString syncToken; + QString personFields; + } m_connectionsListParams; + int m_accountId = 0; + int m_apiRequestsRemaining = 0; bool m_allowFinalCleanup = false; };