2
* This file is part of buteo-sync-plugins-contacts package
4
* Copyright (C) 2013 Jolla Ltd. and/or its subsidiary(-ies).
7
* Contributors: Sateesh Kavuri <sateesh.kavuri@gmail.com>
8
* Mani Chandrasekar <maninc@gmail.com>
9
* Renato Araujo Oliveira Filho <renato.filho@canonical.com>
11
* This library is free software; you can redistribute it and/or
12
* modify it under the terms of the GNU Lesser General Public License
13
* version 2.1 as published by the Free Software Foundation.
15
* This library is distributed in the hope that it will be useful, but
16
* WITHOUT ANY WARRANTY; without even the implied warranty of
17
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
18
* Lesser General Public License for more details.
20
* You should have received a copy of the GNU Lesser General Public
21
* License along with this library; if not, write to the Free Software
22
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
27
#include "GRemoteSource.h"
28
#include "GTransport.h"
30
#include "GContactStream.h"
31
#include "GContactImageDownloader.h"
32
#include "GContactImageUploader.h"
33
#include "buteosyncfw_p.h"
35
#include <UContactsBackend.h>
36
#include <UContactsCustomDetail.h>
38
#include <ProfileEngineDefs.h>
39
#include <ProfileManager.h>
41
#include <QtContacts/QContact>
42
#include <QtContacts/QContactGuid>
43
#include <QtContacts/QContact>
45
QTCONTACTS_USE_NAMESPACE
47
GRemoteSource::GRemoteSource(QObject *parent)
48
: UAbstractRemoteSource(parent),
49
mTransport(new GTransport),
50
mState(GRemoteSource::STATE_IDLE),
54
connect(mTransport.data(),
55
SIGNAL(finishedRequest()),
56
SLOT(networkRequestFinished()));
58
connect(mTransport.data(),
60
SLOT(networkError(int)));
63
GRemoteSource::~GRemoteSource()
67
bool GRemoteSource::init(const QVariantMap &properties)
70
LOG_DEBUG("Creating HTTP transport");
72
if (mState != GRemoteSource::STATE_IDLE) {
73
LOG_WARNING("Init called with wrong state" << mState);
77
mAccountName = properties.value("ACCOUNT-NAME").toString();
78
mAuthToken = properties.value("AUTH-TOKEN").toString();
79
mSyncTarget = properties.value("SYNC-TARGET").toString();
80
mRemoteUri = properties.value(Buteo::KEY_REMOTE_DATABASE).toString();
82
mPendingBatchOps.clear();
83
mState = GRemoteSource::STATE_IDLE;
85
if (mRemoteUri.isEmpty()) {
86
// Set to the default value
87
mRemoteUri = QStringLiteral("https://www.google.com/m8/feeds/contacts/default/full/");
90
LOG_DEBUG("Setting remote URI to" << mRemoteUri);
91
mTransport->setUrl(mRemoteUri);
93
QString proxyHost = properties.value(Buteo::KEY_HTTP_PROXY_HOST).toString();
94
// Set proxy, if available
95
if (!proxyHost.isEmpty()) {
96
QString proxyPort = properties.value(Buteo::KEY_HTTP_PROXY_PORT).toString();
97
mTransport->setProxy(proxyHost, proxyPort);
98
LOG_DEBUG("Proxy host:" << proxyHost);
99
LOG_DEBUG("Proxy port:" << proxyPort);
101
LOG_DEBUG("Not using proxy");
107
void GRemoteSource::abort()
109
disconnect(mTransport.data());
110
mState = STATE_ABORTED;
113
void GRemoteSource::fetchContacts(const QDateTime &since, bool includeDeleted, bool fetchAvatar)
116
if (mState != GRemoteSource::STATE_IDLE) {
117
LOG_WARNING("GRemote source is not in idle state, current state is" << mState);
121
mFetchAvatars = fetchAvatar;
122
mState = GRemoteSource::STATE_FETCHING_CONTACTS;
123
fetchRemoteContacts(since, includeDeleted, 1);
126
void GRemoteSource::fetchAvatars(QList<QContact> *contacts)
128
// keep downloader object live while GRemoteSource exists to avoid removing
129
// the temporary files used to store avatars.
130
// The files will be removed when the object get destroyed
131
GContactImageDownloader *downloader = new GContactImageDownloader(mAuthToken, this);
132
QMap<QUrl, QPair<QContactAvatar, QContact*> > avatars;
134
for(int i=0; i < contacts->size(); i++) {
135
QContact &c = (*contacts)[i];
136
foreach (const QContactAvatar &avatar, c.details<QContactAvatar>()) {
137
if (!avatar.imageUrl().isLocalFile()) {
138
LOG_DEBUG("Download avatar:" << avatar.imageUrl());
139
avatars.insert(avatar.imageUrl(), qMakePair(avatar, &c));
140
downloader->push(avatar.imageUrl());
147
QMap<QUrl, QUrl> downloaded = downloader->donwloaded();
148
foreach (const QUrl &avatarUrl, avatars.keys()) {
149
QPair<QContactAvatar, QContact*> &p = avatars[avatarUrl];
150
LOG_DEBUG("Replace avatar image:" << p.first.imageUrl() << downloaded.value(avatarUrl));
151
p.first.setImageUrl(downloaded.value(avatarUrl));
152
p.second->saveDetail(&p.first);
156
void GRemoteSource::uploadAvatars(QList<QContact> *contacts)
158
GContactImageUploader uploader(mAuthToken, mAccountName);
160
foreach(const QContact &c, *contacts) {
161
QString localId = UContactsBackend::getLocalId(c);
162
LOG_DEBUG("Will upload avatar for:" << localId);
163
if (mLocalIdToAvatar.contains(localId)) {
164
QContactExtendedDetail rEtag =
165
UContactsCustomDetail::getCustomField(c,
166
UContactsCustomDetail::FieldContactAvatarETag);
168
QPair<QString, QUrl> avatar = mLocalIdToAvatar.value(localId);
169
LOG_DEBUG("Current avatar:"
170
<< "\n\tlocal-etag:" << avatar.first
171
<< "\n\tremote-etag:" << rEtag.data().toString()
172
<< "\n\tlocal-url:" << avatar.second);
174
// check if the remote etag has changed
175
if (avatar.second.isLocalFile() &&
176
(avatar.first.isEmpty() || (avatar.first != rEtag.data().toString()))) {
177
QString remoteId = UContactsBackend::getRemoteId(c);
178
LOG_DEBUG("Avatar revision changed:"
179
<< "\n\tRemote version:" << rEtag.data().toString()
180
<< "\n\tLocal version:" << avatar.first);
181
LOG_DEBUG("Uploade avatar:" << remoteId << avatar.second);
182
uploader.push(remoteId, avatar.second);
183
} else if (!avatar.second.isLocalFile()) {
184
LOG_DEBUG("Contact avatar is not local" << avatar.second);
186
LOG_DEBUG("Avatar did not change");
189
LOG_WARNING("Local id not found found on avatar map:" << localId);
195
QMap<QString, GContactImageUploader::UploaderReply> result = uploader.result();
196
for(int i =0; i < contacts->size(); i++) {
197
QContact &c = (*contacts)[i];
199
GContactImageUploader::UploaderReply reply = result.value(UContactsBackend::getRemoteId(c));
201
// update contact e-tag if necessary
202
if (!reply.newEtag.isEmpty()) {
203
UContactsCustomDetail::setCustomField(c,
204
UContactsCustomDetail::FieldContactETag,
208
// update avatar e-tag if necessary
209
if (!reply.newAvatarEtag.isEmpty()) {
210
UContactsCustomDetail::setCustomField(c,
211
UContactsCustomDetail::FieldContactAvatarETag,
212
reply.newAvatarEtag);
215
QString localId = UContactsBackend::getLocalId(c);
216
// copy local url to new remote avatar
217
QContactAvatar avatar = c.detail<QContactAvatar>();
218
avatar.setImageUrl(mLocalIdToAvatar.value(localId).second);
219
c.saveDetail(&avatar);
223
void GRemoteSource::saveContactsNonBatch(const QList<QContact> contacts)
226
if (mState != GRemoteSource::STATE_IDLE) {
227
LOG_WARNING("GRemote source is not in idle state, current state is" << mState);
231
mState = GRemoteSource::STATE_BATCH_RUNNING;
232
foreach (const QContact &contact, contacts) {
233
QString remoteId = UContactsBackend::getRemoteId(contact);
234
if (remoteId.isEmpty()) {
235
mPendingBatchOps.insertMulti(GoogleContactStream::Add,
236
qMakePair(contact, QStringList()));
238
mPendingBatchOps.insertMulti(GoogleContactStream::Modify,
239
qMakePair(contact, QStringList()));
243
batchOperationContinue();
246
void GRemoteSource::removeContactsNonBatch(const QList<QContact> contacts)
249
if (mState != GRemoteSource::STATE_IDLE) {
250
LOG_WARNING("GRemote source is not in idle state, current state is" << mState);
254
mState = GRemoteSource::STATE_BATCH_RUNNING;
255
foreach (const QContact &contact, contacts) {
256
mPendingBatchOps.insertMulti(GoogleContactStream::Remove,
257
qMakePair(contact, QStringList()));
260
batchOperationContinue();
263
void GRemoteSource::batch(const QList<QContact> &contactsToCreate,
264
const QList<QContact> &contactsToUpdate,
265
const QList<QContact> &contactsToRemove)
268
if (mState != GRemoteSource::STATE_IDLE) {
269
LOG_WARNING("GRemote source is not in idle state, current state is" << mState);
270
emit transactionCommited(QList<QContact>(),
273
QMap<QString, int>(),
278
mLocalIdToAvatar.clear();
279
mState = GRemoteSource::STATE_BATCH_RUNNING;
281
foreach (const QContact &contact, contactsToCreate) {
282
QString localID = UContactsBackend::getLocalId(contact);
284
UContactsCustomDetail::getCustomField(contact,
285
UContactsCustomDetail::FieldContactAvatarETag).data().toString();
287
mLocalIdToAvatar.insert(QString("qtcontacts:galera::%1").arg(localID),
288
qMakePair(avatarEtag, contact.detail<QContactAvatar>().imageUrl()));
289
mPendingBatchOps.insertMulti(GoogleContactStream::Add,
290
qMakePair(contact, QStringList()));
293
foreach (const QContact &contact, contactsToUpdate) {
294
QString localID = UContactsBackend::getLocalId(contact);
296
UContactsCustomDetail::getCustomField(contact,
297
UContactsCustomDetail::FieldContactAvatarETag).data().toString();
299
mLocalIdToAvatar.insert(QString("qtcontacts:galera::%1").arg(localID),
300
qMakePair(avatarEtag, contact.detail<QContactAvatar>().imageUrl()));
301
mPendingBatchOps.insertMulti(GoogleContactStream::Modify,
302
qMakePair(contact, QStringList()));
305
foreach (const QContact &contact, contactsToRemove) {
306
mPendingBatchOps.insertMulti(GoogleContactStream::Remove,
307
qMakePair(contact, QStringList()));
310
batchOperationContinue();
314
GRemoteSource::batchOperationContinue()
318
if (mState == GRemoteSource::STATE_ABORTED) {
319
LOG_WARNING("Operation aborted");
323
int limit = qMin(mPendingBatchOps.size(), GConfig::MAX_RESULTS);
324
// no pending batch ops
326
LOG_DEBUG ("No pending operations");
327
emit transactionCommited(QList<QContact>(),
330
QMap<QString, int>(),
334
QMultiMap<GoogleContactStream::UpdateType, QPair<QContact, QStringList> > batchPage;
335
QPair<QContact, QStringList> value;
337
while (batchPage.size() < limit) {
338
GoogleContactStream::UpdateType type;
340
if (!mPendingBatchOps.values(GoogleContactStream::Add).isEmpty()) {
341
type = GoogleContactStream::Add;
342
} else if (!mPendingBatchOps.values(GoogleContactStream::Modify).isEmpty()) {
343
type = GoogleContactStream::Modify;
344
} else if (!mPendingBatchOps.values(GoogleContactStream::Remove).isEmpty()) {
345
type = GoogleContactStream::Remove;
348
value = mPendingBatchOps.values(type).first();
349
mPendingBatchOps.remove(type, value);
350
batchPage.insertMulti(type, value);
353
GoogleContactStream encoder(false, mAccountName);
354
QByteArray encodedContacts = encoder.encode(batchPage);
357
mTransport->setUrl(mRemoteUri + "batch");
358
mTransport->setGDataVersionHeader();
359
mTransport->setAuthToken(mAuthToken);
360
mTransport->setData(encodedContacts);
361
mTransport->addHeader("Content-Type", "application/atom+xml; charset=UTF-8; type=feed");
362
LOG_TRACE("POST DATA:" << encodedContacts);
363
mTransport->request(GTransport::POST);
366
int GRemoteSource::parseErrorReponse(const GoogleContactAtom::BatchOperationResponse &response)
368
if ((response.code == "404") && (response.type == "update")) {
369
// contact not found on remote side:
370
return QContactManager::DoesNotExistError;
373
return QContactManager::UnspecifiedError;
376
void GRemoteSource::emitTransactionCommited(const QList<QContact> &created,
377
const QList<QContact> &changed,
378
const QList<QContact> &removed,
379
const QMap<QString, int> &errorMap,
380
Sync::SyncStatus status)
383
LOG_INFO("ADDED:" << created.size() <<
384
"CHANGED" << changed.size() <<
385
"REMOVED" << removed.size());
387
if (!created.isEmpty()) {
388
emit contactsCreated(created, status);
391
if (!changed.isEmpty()) {
392
emit contactsChanged(changed, status);
395
QStringList removedIds;
396
foreach(const QContact &c, removed) {
397
removedIds << UContactsCustomDetail::getCustomField(c, UContactsCustomDetail::FieldRemoteId).data().toString();
400
if (!removedIds.isEmpty()) {
401
emit contactsRemoved(removedIds, status);
404
emit transactionCommited(created, changed, removedIds, errorMap, status);
408
GRemoteSource::fetchRemoteContacts(const QDateTime &since, bool includeDeleted, int startIndex)
411
if (mState == GRemoteSource::STATE_ABORTED) {
412
LOG_WARNING("Operation aborted");
417
o Get etag value from local file system (this is a soft etag)
418
o Connect finishedRequest to parseResults & network error slots
419
o Use mTransport to perform network fetch
421
if (since.isValid()) {
422
mTransport->setUpdatedMin(since);
425
if (startIndex > 1) {
426
mTransport->setStartIndex(startIndex);
429
mTransport->setMaxResults(GConfig::MAX_RESULTS);
430
if (includeDeleted) {
431
mTransport->setShowDeleted();
434
// TODO: only fetch contacts from "My Contacts" group for now
435
// we should implement support for all groups
436
mTransport->setGroupFilter(mAccountName, GConfig::GROUP_MY_CONTACTS_ID);
438
mTransport->setGDataVersionHeader();
439
mTransport->addHeader(QByteArray("Authorization"),
440
QString("Bearer " + mAuthToken).toUtf8());
441
mTransport->request(GTransport::GET);
445
* The state machine is pretty much maintained in this method.
446
* Maybe it is better to create a separate class that can handle
447
* the sync state. It can act of Qt signals that will be emitted
451
GRemoteSource::networkRequestFinished()
455
if (mState == GRemoteSource::STATE_ABORTED) {
456
LOG_WARNING("Operation aborted");
460
// o Error - if network error, set the sync results with the code
463
// o If success, invoke the mParser->parse ()
464
Sync::SyncStatus syncStatus = Sync::SYNC_ERROR;
465
GTransport::HTTP_REQUEST_TYPE requestType = mTransport->requestType();
466
if (mTransport->hasReply()) {
467
QByteArray data = mTransport->replyBody();
469
if (data.isNull () || data.isEmpty()) {
470
LOG_INFO("Nothing returned from server");
471
syncStatus = Sync::SYNC_CONNECTION_ERROR;
472
goto operationFailed;
475
GoogleContactStream parser(false);
476
GoogleContactAtom *atom = parser.parse(data);
478
LOG_CRITICAL("NULL atom object. Something wrong with parsing");
479
goto operationFailed;
482
if ((requestType == GTransport::POST) ||
483
(requestType == GTransport::PUT)) {
484
QList<QContact> addedContacts;
485
QList<QContact> modContacts;
486
QList<QContact> delContacts;
487
QMap<QString, int> errorMap;
489
LOG_DEBUG("@@@PREVIOUS REQUEST TYPE=POST");
490
QMap<QString, GoogleContactAtom::BatchOperationResponse> operationResponses = atom->batchOperationResponses();
491
QMap<QString, QString> batchOperationRemoteIdToType;
492
QMap<QString, QString> batchOperationRemoteToLocalId;
494
LOG_DEBUG("RESPONSE SIZE:" << operationResponses.size());
495
foreach (const GoogleContactAtom::BatchOperationResponse &response, operationResponses) {
496
if (response.isError) {
497
LOG_CRITICAL("batch operation error:\n"
498
" id: " << response.operationId << "\n"
499
" type: " << response.type << "\n"
500
" code: " << response.code << "\n"
501
" reason: " << response.reason << "\n"
502
" descr: " << response.reasonDescription << "\n");
503
errorMap.insert(response.operationId, parseErrorReponse(response));
505
LOG_DEBUG("RESPONSE" << response.contactGuid << response.type);
506
batchOperationRemoteToLocalId.insert(response.contactGuid, response.operationId);
507
batchOperationRemoteIdToType.insert(response.contactGuid, response.type);
511
QList<QPair<QContact, QStringList> > remoteAddModContacts = atom->entryContacts();
512
LOG_DEBUG("Number of changed contacts:" << remoteAddModContacts.size());
513
for (int i = 0; i < remoteAddModContacts.size(); i++) {
514
QContact &c = remoteAddModContacts[i].first;
515
QContactGuid guid = c.detail<QContactGuid>();
516
QString cRemoteId = guid.guid();
517
if (cRemoteId.isEmpty()) {
518
LOG_WARNING("Remote contact without remote id");
521
UContactsBackend::setRemoteId(c, cRemoteId);
522
UContactsBackend::setLocalId(c, batchOperationRemoteToLocalId.value(cRemoteId, ""));
524
QString opType = batchOperationRemoteIdToType.value(cRemoteId, "");
525
c.removeDetail(&guid);
526
if (opType == QStringLiteral("insert")) {
532
delContacts += atom->deletedEntryContacts();
533
LOG_DEBUG("Number of deleted contacts:" << delContacts.size());
535
if (!atom->nextEntriesUrl().isEmpty()) {
536
syncStatus = Sync::SYNC_PROGRESS;
539
syncStatus = Sync::SYNC_DONE;
540
mState = GRemoteSource::STATE_IDLE;
543
uploadAvatars(&addedContacts);
544
uploadAvatars(&modContacts);
546
emitTransactionCommited(addedContacts, modContacts, delContacts, errorMap, syncStatus);
548
if (syncStatus == Sync::SYNC_PROGRESS) {
549
batchOperationContinue();
551
mLocalIdToAvatar.clear();
553
} else if (requestType == GTransport::GET) {
554
LOG_DEBUG ("@@@PREVIOUS REQUEST TYPE=GET");
555
if (mState != GRemoteSource::STATE_FETCHING_CONTACTS) {
556
LOG_WARNING("Received a network request finish but the state is not fetching contacts" << mState);
560
LOG_INFO("received information about" <<
561
atom->entryContacts().size() << "add/mod contacts and " <<
562
atom->deletedEntryContacts().size() << "del contacts");
564
QList<QContact> remoteContacts;
566
// for each remote contact, there are some associated XML elements which
567
// could not be stored in QContactDetail form (eg, link URIs etc).
568
// build up some datastructures to help us retrieve that information
570
// we also store the etag data out-of-band to avoid spurious contact saves
571
// when the etag changes are reported by the remote server.
572
// finally, we can set the id of the contact.
573
QList<QPair<QContact, QStringList> > remoteAddModContacts = atom->entryContacts();
574
for (int i = 0; i < remoteAddModContacts.size(); ++i) {
575
QContact c = remoteAddModContacts[i].first;
576
QContactGuid guid = c.detail<QContactGuid>();
577
UContactsBackend::setRemoteId(c, guid.guid());
578
c.removeDetail(&guid);
579
// FIXME: This code came from the meego implementation, until now we did not face
580
// any unsupported xml element. Keep the code here in case some unsupported element
581
// apper. Then we should store it some how on our backend.
582
// m_unsupportedXmlElements[accountId].insert(
583
// c.detail<QContactGuid>().guid(),
584
// remoteAddModContacts[i].second);
585
// m_contactEtags[accountId].insert(c.detail<QContactGuid>().guid(), c.detail<QContactOriginMetadata>().id());
586
// c.setId(QContactId::fromString(m_contactIds[accountId].value(c.detail<QContactGuid>().guid())));
587
// m_remoteAddMods[accountId].append(c);
592
fetchAvatars(&remoteContacts);
595
QList<QContact> remoteDelContacts = atom->deletedEntryContacts();
596
for (int i = 0; i < remoteDelContacts.size(); ++i) {
597
QContact c = remoteDelContacts[i];
598
QContactGuid guid = c.detail<QContactGuid>();
599
UContactsBackend::setRemoteId(c, guid.guid());
600
c.removeDetail(&guid);
602
// c.setId(QContactId::fromString(m_contactIds[accountId].value(c.detail<QContactGuid>().guid())));
603
// m_contactAvatars[accountId].remove(c.detail<QContactGuid>().guid()); // just in case the avatar was outstanding.
604
// m_remoteDels[accountId].append(c);
608
bool hasMore = (!atom->nextEntriesUrl().isNull() ||
609
!atom->nextEntriesUrl().isEmpty());
611
// Request for the next batch
612
// This condition will make this slot to be
613
// called again and again until there are no more
614
// entries left to be fetched from the server
615
mStartIndex += GConfig::MAX_RESULTS;
616
mTransport->setUrl(atom->nextEntriesUrl());
617
syncStatus = Sync::SYNC_PROGRESS;
618
LOG_DEBUG("Has more contacts to retrieve");
620
LOG_DEBUG("NO contacts to retrieve");
621
syncStatus = Sync::SYNC_DONE;
622
mState = GRemoteSource::STATE_IDLE;
626
qreal progress = -1.0;
627
if (hasMore && (atom->totalResults() > 0)) {
628
progress = (mStartIndex / (qreal) atom->totalResults());
629
} else if (!hasMore) {
633
LOG_TRACE("NOTIFY CONTACTS FETCHED:" << remoteContacts.size() << "Progress" << progress);
634
emit contactsFetched(remoteContacts, syncStatus, progress);
637
LOG_TRACE("FETCH MORE CONTACTS FROM INDEX:" << mStartIndex);
638
fetchRemoteContacts(QDateTime(),
639
mTransport->showDeleted(),
649
case GRemoteSource::STATE_FETCHING_CONTACTS:
650
contactsFetched(QList<QContact>(), syncStatus, -1.0);
652
case GRemoteSource::STATE_BATCH_RUNNING:
653
emitTransactionCommited(QList<QContact>(),
656
QMap<QString, int>(),
662
mState = GRemoteSource::STATE_IDLE;
666
GRemoteSource::networkError(int errorCode)
670
Sync::SyncStatus syncStatus = Sync::SYNC_ERROR;
674
// Bad request. Better to bail out, since it could be a problem with the
675
// data format of the request/response
676
syncStatus = Sync::SYNC_BAD_REQUEST;
679
syncStatus = Sync::SYNC_AUTHENTICATION_FAILURE;
683
syncStatus = Sync::SYNC_ERROR;
689
syncStatus = Sync::SYNC_SERVER_FAILURE;
696
case GRemoteSource::STATE_FETCHING_CONTACTS:
697
contactsFetched(QList<QContact>(), syncStatus, -1.0);
699
case GRemoteSource::STATE_BATCH_RUNNING:
700
emitTransactionCommited(QList<QContact>(),
703
QMap<QString, int>(),
709
mState = GRemoteSource::STATE_IDLE;
712
const GTransport *GRemoteSource::transport() const
714
return mTransport.data();
717
int GRemoteSource::state() const