2015-10-14 11:38:18 +00:00
|
|
|
|
/*
|
|
|
|
|
* Copyright © 2013 – 2015 Ricki Hirner (bitfire web engineering).
|
|
|
|
|
* All rights reserved. This program and the accompanying materials
|
|
|
|
|
* are made available under the terms of the GNU Public License v3.0
|
|
|
|
|
* which accompanies this distribution, and is available at
|
|
|
|
|
* http://www.gnu.org/licenses/gpl.html
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
package at.bitfire.davdroid.syncadapter;
|
|
|
|
|
|
|
|
|
|
import android.accounts.Account;
|
|
|
|
|
import android.content.ContentProviderClient;
|
|
|
|
|
import android.content.Context;
|
|
|
|
|
import android.content.SyncResult;
|
|
|
|
|
import android.os.Bundle;
|
|
|
|
|
|
|
|
|
|
import com.squareup.okhttp.HttpUrl;
|
|
|
|
|
import com.squareup.okhttp.MediaType;
|
|
|
|
|
import com.squareup.okhttp.Request;
|
|
|
|
|
import com.squareup.okhttp.RequestBody;
|
|
|
|
|
import com.squareup.okhttp.Response;
|
|
|
|
|
import com.squareup.okhttp.ResponseBody;
|
|
|
|
|
|
2015-10-15 13:36:55 +00:00
|
|
|
|
import org.apache.commons.codec.Charsets;
|
|
|
|
|
import org.apache.commons.lang3.StringUtils;
|
|
|
|
|
|
2015-10-14 11:38:18 +00:00
|
|
|
|
import java.io.ByteArrayInputStream;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.io.InputStream;
|
|
|
|
|
import java.nio.charset.Charset;
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.LinkedList;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
|
|
|
|
|
import at.bitfire.dav4android.DavAddressBook;
|
|
|
|
|
import at.bitfire.dav4android.DavResource;
|
|
|
|
|
import at.bitfire.dav4android.exception.DavException;
|
|
|
|
|
import at.bitfire.dav4android.exception.HttpException;
|
|
|
|
|
import at.bitfire.dav4android.property.AddressData;
|
|
|
|
|
import at.bitfire.dav4android.property.GetCTag;
|
|
|
|
|
import at.bitfire.dav4android.property.GetContentType;
|
|
|
|
|
import at.bitfire.dav4android.property.GetETag;
|
|
|
|
|
import at.bitfire.dav4android.property.SupportedAddressData;
|
|
|
|
|
import at.bitfire.davdroid.ArrayUtils;
|
|
|
|
|
import at.bitfire.davdroid.Constants;
|
|
|
|
|
import at.bitfire.davdroid.HttpClient;
|
2015-10-16 21:06:35 +00:00
|
|
|
|
import at.bitfire.davdroid.R;
|
2015-10-14 11:38:18 +00:00
|
|
|
|
import at.bitfire.davdroid.resource.LocalAddressBook;
|
|
|
|
|
import at.bitfire.davdroid.resource.LocalContact;
|
2015-10-16 21:06:35 +00:00
|
|
|
|
import at.bitfire.davdroid.resource.LocalGroup;
|
2015-10-14 16:19:59 +00:00
|
|
|
|
import at.bitfire.davdroid.resource.LocalResource;
|
2015-10-14 11:38:18 +00:00
|
|
|
|
import at.bitfire.vcard4android.Contact;
|
|
|
|
|
import at.bitfire.vcard4android.ContactsStorageException;
|
|
|
|
|
import ezvcard.VCardVersion;
|
|
|
|
|
import ezvcard.util.IOUtils;
|
|
|
|
|
import lombok.Cleanup;
|
|
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
|
|
|
|
|
|
|
public class ContactsSyncManager extends SyncManager {
|
2015-10-15 13:36:55 +00:00
|
|
|
|
protected static final int MAX_MULTIGET = 10;
|
2015-10-14 11:38:18 +00:00
|
|
|
|
|
2015-10-14 22:49:15 +00:00
|
|
|
|
final protected ContentProviderClient provider;
|
2015-10-14 11:38:18 +00:00
|
|
|
|
protected boolean hasVCard4;
|
|
|
|
|
|
|
|
|
|
|
2015-10-17 09:33:35 +00:00
|
|
|
|
public ContactsSyncManager(Context context, Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult result) {
|
|
|
|
|
super(Constants.NOTIFICATION_CONTACTS_SYNC, context, account, extras, authority, result);
|
2015-10-14 22:49:15 +00:00
|
|
|
|
this.provider = provider;
|
2015-10-14 11:38:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
2015-10-16 21:06:35 +00:00
|
|
|
|
@Override
|
|
|
|
|
protected String getSyncErrorTitle() {
|
|
|
|
|
return context.getString(R.string.sync_error_contacts, account.name);
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-14 11:38:18 +00:00
|
|
|
|
|
|
|
|
|
@Override
|
2015-10-15 13:36:55 +00:00
|
|
|
|
protected void prepare() throws ContactsStorageException {
|
2015-10-14 11:38:18 +00:00
|
|
|
|
// prepare local address book
|
2015-10-14 16:19:59 +00:00
|
|
|
|
localCollection = new LocalAddressBook(account, provider);
|
2015-10-15 13:36:55 +00:00
|
|
|
|
|
2015-10-16 01:27:56 +00:00
|
|
|
|
String url = localAddressBook().getURL();
|
|
|
|
|
if (url == null)
|
|
|
|
|
throw new ContactsStorageException("Couldn't get address book URL");
|
|
|
|
|
collectionURL = HttpUrl.parse(url);
|
2015-10-18 14:20:26 +00:00
|
|
|
|
davCollection = new DavAddressBook(log, httpClient, collectionURL);
|
2015-10-16 21:06:35 +00:00
|
|
|
|
|
|
|
|
|
processChangedGroups();
|
2015-10-14 11:38:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void queryCapabilities() throws DavException, IOException, HttpException {
|
|
|
|
|
// prepare remote address book
|
|
|
|
|
hasVCard4 = false;
|
|
|
|
|
davCollection.propfind(0, SupportedAddressData.NAME, GetCTag.NAME);
|
|
|
|
|
SupportedAddressData supportedAddressData = (SupportedAddressData) davCollection.properties.get(SupportedAddressData.NAME);
|
|
|
|
|
if (supportedAddressData != null)
|
|
|
|
|
for (MediaType type : supportedAddressData.types)
|
|
|
|
|
if ("text/vcard; version=4.0".equalsIgnoreCase(type.toString()))
|
|
|
|
|
hasVCard4 = true;
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.info("Server advertises VCard/4 support: " + hasVCard4);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
2015-10-14 16:19:59 +00:00
|
|
|
|
protected RequestBody prepareUpload(LocalResource resource) throws IOException, ContactsStorageException {
|
|
|
|
|
LocalContact local = (LocalContact)resource;
|
|
|
|
|
return RequestBody.create(
|
|
|
|
|
hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8,
|
|
|
|
|
local.getContact().toStream(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0).toByteArray()
|
|
|
|
|
);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
2015-10-14 16:19:59 +00:00
|
|
|
|
protected void listRemote() throws IOException, HttpException, DavException {
|
2015-10-14 11:38:18 +00:00
|
|
|
|
// fetch list of remote VCards and build hash table to index file name
|
2015-10-16 01:27:56 +00:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
davAddressBook().addressbookQuery();
|
|
|
|
|
} catch(HttpException e) {
|
|
|
|
|
if (e.status/100 == 4) {
|
2015-10-21 00:06:29 +00:00
|
|
|
|
log.warn("Server error on REPORT addressbook-query, falling back to PROPFIND", e);
|
2015-10-16 01:27:56 +00:00
|
|
|
|
davAddressBook().propfind(1, GetETag.NAME);
|
2015-10-21 00:06:29 +00:00
|
|
|
|
} else
|
|
|
|
|
// no defined fallback, pass through exception
|
|
|
|
|
throw e;
|
2015-10-16 01:27:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
2015-10-14 16:19:59 +00:00
|
|
|
|
remoteResources = new HashMap<>(davCollection.members.size());
|
2015-10-14 11:38:18 +00:00
|
|
|
|
for (DavResource vCard : davCollection.members) {
|
|
|
|
|
String fileName = vCard.fileName();
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.debug("Found remote VCard: " + fileName);
|
2015-10-14 16:19:59 +00:00
|
|
|
|
remoteResources.put(fileName, vCard);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected void downloadRemote() throws IOException, HttpException, DavException, ContactsStorageException {
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.info("Downloading " + toDownload.size() + " contacts (" + MAX_MULTIGET + " at once)");
|
2015-10-14 11:38:18 +00:00
|
|
|
|
|
|
|
|
|
// prepare downloader which may be used to download external resource like contact photos
|
2015-10-14 16:19:59 +00:00
|
|
|
|
Contact.Downloader downloader = new ResourceDownloader(httpClient, collectionURL);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
|
|
|
|
|
// download new/updated VCards from server
|
|
|
|
|
for (DavResource[] bunch : ArrayUtils.partition(toDownload.toArray(new DavResource[toDownload.size()]), MAX_MULTIGET)) {
|
2015-10-17 09:33:35 +00:00
|
|
|
|
if (Thread.interrupted())
|
|
|
|
|
return;
|
|
|
|
|
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.info("Downloading " + StringUtils.join(bunch, ", "));
|
2015-10-14 11:38:18 +00:00
|
|
|
|
|
|
|
|
|
if (bunch.length == 1) {
|
|
|
|
|
// only one contact, use GET
|
|
|
|
|
DavResource remote = bunch[0];
|
|
|
|
|
|
|
|
|
|
ResponseBody body = remote.get("text/vcard;q=0.5, text/vcard;charset=utf-8;q=0.8, text/vcard;version=4.0");
|
|
|
|
|
String eTag = ((GetETag) remote.properties.get(GetETag.NAME)).eTag;
|
|
|
|
|
|
|
|
|
|
@Cleanup InputStream stream = body.byteStream();
|
2015-10-14 16:19:59 +00:00
|
|
|
|
processVCard(remote.fileName(), eTag, stream, body.contentType().charset(Charsets.UTF_8), downloader);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
// multiple contacts, use multi-get
|
|
|
|
|
List<HttpUrl> urls = new LinkedList<>();
|
|
|
|
|
for (DavResource remote : bunch)
|
|
|
|
|
urls.add(remote.location);
|
2015-10-14 16:19:59 +00:00
|
|
|
|
davAddressBook().multiget(urls.toArray(new HttpUrl[urls.size()]), hasVCard4);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
|
|
|
|
|
// process multiget results
|
|
|
|
|
for (DavResource remote : davCollection.members) {
|
|
|
|
|
String eTag;
|
|
|
|
|
GetETag getETag = (GetETag) remote.properties.get(GetETag.NAME);
|
|
|
|
|
if (getETag != null)
|
|
|
|
|
eTag = getETag.eTag;
|
|
|
|
|
else
|
|
|
|
|
throw new DavException("Received multi-get response without ETag");
|
|
|
|
|
|
|
|
|
|
Charset charset = Charsets.UTF_8;
|
|
|
|
|
GetContentType getContentType = (GetContentType) remote.properties.get(GetContentType.NAME);
|
|
|
|
|
if (getContentType != null && getContentType.type != null) {
|
|
|
|
|
MediaType type = MediaType.parse(getContentType.type);
|
|
|
|
|
if (type != null)
|
|
|
|
|
charset = type.charset(Charsets.UTF_8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AddressData addressData = (AddressData) remote.properties.get(AddressData.NAME);
|
|
|
|
|
if (addressData == null || addressData.vCard == null)
|
|
|
|
|
throw new DavException("Received multi-get response without address data");
|
|
|
|
|
|
|
|
|
|
@Cleanup InputStream stream = new ByteArrayInputStream(addressData.vCard.getBytes());
|
2015-10-14 16:19:59 +00:00
|
|
|
|
processVCard(remote.fileName(), eTag, stream, charset, downloader);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2015-10-14 16:19:59 +00:00
|
|
|
|
// helpers
|
|
|
|
|
|
|
|
|
|
private DavAddressBook davAddressBook() { return (DavAddressBook)davCollection; }
|
|
|
|
|
private LocalAddressBook localAddressBook() { return (LocalAddressBook)localCollection; }
|
|
|
|
|
|
2015-10-16 21:06:35 +00:00
|
|
|
|
private void processChangedGroups() throws ContactsStorageException {
|
|
|
|
|
LocalAddressBook addressBook = localAddressBook();
|
|
|
|
|
|
|
|
|
|
// groups with DELETED=1: remove group finally
|
|
|
|
|
for (LocalGroup group : addressBook.getDeletedGroups()) {
|
|
|
|
|
long groupId = group.getId();
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.debug("Finally removing group #" + groupId);
|
2015-10-16 21:06:35 +00:00
|
|
|
|
// remove group memberships, but not as sync adapter (should marks contacts as DIRTY)
|
|
|
|
|
// NOTE: doesn't work that way because Contact Provider removes the group memberships even for DELETED groups
|
|
|
|
|
// addressBook.removeGroupMemberships(groupId, false);
|
|
|
|
|
group.delete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// groups with DIRTY=1: mark all memberships as dirty, then clean DIRTY flag of group
|
|
|
|
|
for (LocalGroup group : addressBook.getDirtyGroups()) {
|
|
|
|
|
long groupId = group.getId();
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.debug("Marking members of modified group #" + groupId + " as dirty");
|
2015-10-16 21:06:35 +00:00
|
|
|
|
addressBook.markMembersDirty(groupId);
|
|
|
|
|
group.clearDirty();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-10-14 16:19:59 +00:00
|
|
|
|
private void processVCard(String fileName, String eTag, InputStream stream, Charset charset, Contact.Downloader downloader) throws IOException, ContactsStorageException {
|
2015-10-14 11:38:18 +00:00
|
|
|
|
Contact contacts[] = Contact.fromStream(stream, charset, downloader);
|
2015-10-14 22:49:15 +00:00
|
|
|
|
if (contacts != null && contacts.length == 1) {
|
2015-10-14 11:38:18 +00:00
|
|
|
|
Contact newData = contacts[0];
|
|
|
|
|
|
2015-10-14 22:49:15 +00:00
|
|
|
|
// update local contact, if it exists
|
2015-10-14 16:19:59 +00:00
|
|
|
|
LocalContact localContact = (LocalContact)localResources.get(fileName);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
if (localContact != null) {
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.info("Updating " + fileName + " in local address book");
|
2015-10-14 11:38:18 +00:00
|
|
|
|
localContact.eTag = eTag;
|
|
|
|
|
localContact.update(newData);
|
|
|
|
|
syncResult.stats.numUpdates++;
|
|
|
|
|
} else {
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.info("Adding " + fileName + " to local address book");
|
2015-10-14 16:19:59 +00:00
|
|
|
|
localContact = new LocalContact(localAddressBook(), newData, fileName, eTag);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
localContact.add();
|
|
|
|
|
syncResult.stats.numInserts++;
|
|
|
|
|
}
|
|
|
|
|
} else
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.error("Received VCard with not exactly one VCARD, ignoring " + fileName);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2015-10-14 16:19:59 +00:00
|
|
|
|
// downloader helper class
|
|
|
|
|
|
2015-10-14 11:38:18 +00:00
|
|
|
|
@RequiredArgsConstructor
|
2015-10-18 14:20:26 +00:00
|
|
|
|
private class ResourceDownloader implements Contact.Downloader {
|
2015-10-14 11:38:18 +00:00
|
|
|
|
final HttpClient httpClient;
|
|
|
|
|
final HttpUrl baseUrl;
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public byte[] download(String url, String accepts) {
|
|
|
|
|
HttpUrl httpUrl = HttpUrl.parse(url);
|
2015-10-18 14:20:26 +00:00
|
|
|
|
HttpClient resourceClient = new HttpClient(log, httpClient, httpUrl.host());
|
2015-10-14 11:38:18 +00:00
|
|
|
|
try {
|
|
|
|
|
Response response = resourceClient.newCall(new Request.Builder()
|
|
|
|
|
.get()
|
|
|
|
|
.url(httpUrl)
|
|
|
|
|
.build()).execute();
|
|
|
|
|
|
|
|
|
|
ResponseBody body = response.body();
|
|
|
|
|
if (body != null) {
|
|
|
|
|
@Cleanup InputStream stream = body.byteStream();
|
|
|
|
|
if (response.isSuccessful() && stream != null) {
|
|
|
|
|
return IOUtils.toByteArray(stream);
|
|
|
|
|
} else
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.error("Couldn't download external resource");
|
2015-10-14 11:38:18 +00:00
|
|
|
|
}
|
|
|
|
|
} catch(IOException e) {
|
2015-10-18 14:20:26 +00:00
|
|
|
|
log.error("Couldn't download external resource", e);
|
2015-10-14 11:38:18 +00:00
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|