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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* 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;
}
}
}