You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
etesync-android/app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncManager.java

260 lines
9.4 KiB

/*
* 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.ContentValues;
import android.content.Context;
import android.content.SyncResult;
import android.os.Bundle;
import android.provider.ContactsContract;
import org.apache.commons.codec.Charsets;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.logging.Level;
import at.bitfire.davdroid.AccountSettings;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.InvalidAccountException;
import at.bitfire.davdroid.R;
import at.bitfire.davdroid.journalmanager.JournalEntryManager;
import at.bitfire.davdroid.model.CollectionInfo;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.resource.LocalContact;
import at.bitfire.davdroid.resource.LocalGroup;
import at.bitfire.davdroid.resource.LocalResource;
import at.bitfire.ical4android.CalendarStorageException;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* <p>Synchronization manager for CardDAV collections; handles contacts and groups.</p>
*/
public class ContactsSyncManager extends SyncManager {
protected static final int MAX_MULTIGET = 10;
final private ContentProviderClient provider;
final private HttpUrl remote;
final private CollectionInfo info;
public ContactsSyncManager(Context context, Account account, AccountSettings settings, Bundle extras, String authority, ContentProviderClient provider, SyncResult result, HttpUrl principal, CollectionInfo info) throws InvalidAccountException {
super(context, account, settings, extras, authority, result, "addressBook");
this.provider = provider;
this.remote = principal;
this.info = info;
}
@Override
protected int notificationId() {
return Constants.NOTIFICATION_CONTACTS_SYNC;
}
@Override
protected String getSyncErrorTitle() {
return context.getString(R.string.sync_error_contacts, account.name);
}
@Override
protected void prepare() throws ContactsStorageException, CalendarStorageException {
// prepare local address book
localCollection = new LocalAddressBook(account, provider);
LocalAddressBook localAddressBook = localAddressBook();
localAddressBook.setURL(info.url);
// set up Contacts Provider Settings
ContentValues values = new ContentValues(2);
values.put(ContactsContract.Settings.SHOULD_SYNC, 1);
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1);
localAddressBook.updateSettings(values);
journal = new JournalEntryManager(httpClient, remote, info.url);
}
@Override
protected void applyLocalEntries() throws IOException, ContactsStorageException, CalendarStorageException {
}
@Override
protected void prepareDirty() throws CalendarStorageException, ContactsStorageException {
super.prepareDirty();
LocalAddressBook addressBook = localAddressBook();
/* groups as separate VCards: thtere are group contacts and individual contacts */
// mark groups with changed members as dirty
// FIXME: add back
}
@Override
protected void postProcess() throws CalendarStorageException, ContactsStorageException {
/* VCard4 group handling: there are group contacts and individual contacts */
App.log.info("Assigning memberships of downloaded contact groups");
LocalGroup.applyPendingMemberships(localAddressBook());
}
// helpers
private LocalAddressBook localAddressBook() {
return (LocalAddressBook) localCollection;
}
protected void processSyncEntry(SyncEntry cEntry) throws IOException, ContactsStorageException, CalendarStorageException {
InputStream is = new ByteArrayInputStream(cEntry.getContent().getBytes(Charsets.UTF_8));
// FIXME: Probably cache this and enable it. prepare downloader which may be used to download external resource like contact photos
// Contact.Downloader downloader = new ResourceDownloader(collectionURL);
Contact[] contacts = Contact.fromStream(is, Charsets.UTF_8, null);
if (contacts.length == 0) {
App.log.warning("Received VCard without data, ignoring");
return;
} else if (contacts.length > 1)
App.log.warning("Received multiple VCards, using first one");
Contact contact = contacts[0];
if (cEntry.isAction(SyncEntry.Actions.ADD) || cEntry.isAction(SyncEntry.Actions.CHANGE)) {
LocalResource local = processContact(contact);
if (local != null) {
localResources.put(local.getUuid(), local);
}
} else {
LocalResource local = localResources.get(contact.uid);
App.log.info("Removing local record #" + local.getId() + " which has been deleted on the server");
localResources.remove(local.getUuid());
local.delete();
}
}
private LocalResource processContact(final Contact newData) throws IOException, ContactsStorageException {
String uuid = newData.uid;
// update local contact, if it exists
LocalResource local = localResources.get(uuid);
if (local != null) {
App.log.log(Level.INFO, "Updating " + uuid + " in local address book", newData);
if (local instanceof LocalGroup && newData.group) {
// update group
LocalGroup group = (LocalGroup) local;
group.eTag = uuid;
group.updateFromServer(newData);
syncResult.stats.numUpdates++;
} else if (local instanceof LocalContact && !newData.group) {
// update contact
LocalContact contact = (LocalContact) local;
contact.eTag = uuid;
contact.update(newData);
syncResult.stats.numUpdates++;
} else {
// group has become an individual contact or vice versa
try {
local.delete();
local = null;
} catch (CalendarStorageException e) {
// CalendarStorageException is not used by LocalGroup and LocalContact
}
}
}
if (local == null) {
if (newData.group) {
App.log.log(Level.INFO, "Creating local group", newData);
LocalGroup group = new LocalGroup(localAddressBook(), newData, uuid, null);
group.create();
local = group;
} else {
App.log.log(Level.INFO, "Creating local contact", newData);
LocalContact contact = new LocalContact(localAddressBook(), newData, uuid, null);
contact.create();
local = contact;
}
syncResult.stats.numInserts++;
}
return local;
}
// downloader helper class
@RequiredArgsConstructor
private class ResourceDownloader implements Contact.Downloader {
final HttpUrl baseUrl;
@Override
public byte[] download(String url, String accepts) {
HttpUrl httpUrl = HttpUrl.parse(url);
if (httpUrl == null) {
App.log.log(Level.SEVERE, "Invalid external resource URL", url);
return null;
}
String host = httpUrl.host();
if (host == null) {
App.log.log(Level.SEVERE, "External resource URL doesn't specify a host name", url);
return null;
}
OkHttpClient resourceClient = HttpClient.create(context);
// authenticate only against a certain host, and only upon request
// resourceClient = HttpClient.addAuthentication(resourceClient, baseUrl.host(), settings.username(), settings.password());
// allow redirects
resourceClient = resourceClient.newBuilder()
.followRedirects(true)
.build();
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
App.log.severe("Couldn't download external resource");
}
} catch (IOException e) {
App.log.log(Level.SEVERE, "Couldn't download external resource", e);
}
return null;
}
}
}