Commit a4d1c2ab authored by Ricki Hirner's avatar Ricki Hirner 🐑

First implementation of CardDAV sync with dav4android and vcard4android

* try to get rid of Apache Commons
parent 0d26eac2
[submodule "dav4android"]
path = dav4android
url = [email protected]:bitfireAT/dav4android.git
[submodule "vcard4android"]
path = vcard4android
url = [email protected]:bitfireAT/vcard4android.git
......@@ -56,14 +56,6 @@ dependencies {
exclude group: 'org.codehaus.groovy', module: 'groovy-all'
}
compile('org.slf4j:slf4j-android:1.7.12') // slf4j is used by ical4j
// ez-vcard for parsing/generating VCards
compile('com.googlecode.ez-vcard:ez-vcard:0.9.6') {
// hCard functionality not needed
exclude group: 'org.jsoup', module: 'jsoup'
exclude group: 'org.freemarker', module: 'freemarker'
// jCard functionality not needed
exclude group: 'com.fasterxml.jackson.core', module: 'jackson-core'
}
// dnsjava for querying SRV/TXT records
compile 'dnsjava:dnsjava:2.1.7'
// HttpClient 4.3, Android flavour for WebDAV operations
......@@ -76,4 +68,5 @@ dependencies {
}
compile project(':dav4android')
compile project(':vcard4android')
}
/*
* 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.resource;
import android.content.res.AssetManager;
import android.test.InstrumentationTestCase;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.CharEncoding;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.util.Arrays;
import at.bitfire.davdroid.webdav.DavException;
import at.bitfire.davdroid.webdav.HttpException;
import ezvcard.VCardVersion;
import ezvcard.property.Email;
import ezvcard.property.Telephone;
import lombok.Cleanup;
public class ContactTest extends InstrumentationTestCase {
AssetManager assetMgr;
public void setUp() throws IOException, InvalidResourceException {
assetMgr = getInstrumentation().getContext().getResources().getAssets();
}
public void testGenerateDifferentVersions() throws Exception {
Contact c = new Contact("test.vcf", null);
// should generate VCard 3.0 by default
assertEquals("text/vcard; charset=utf-8", c.getContentType().toString().toLowerCase());
assertTrue(new String(c.toEntity().toByteArray()).contains("VERSION:3.0"));
// now let's generate VCard 4.0
c.vCardVersion = VCardVersion.V4_0;
assertEquals("text/vcard; version=4.0", c.getContentType().toString());
assertTrue(new String(c.toEntity().toByteArray()).contains("VERSION:4.0"));
}
public void testReferenceVCard3() throws IOException, InvalidResourceException {
Contact c = parseVCF("reference-vcard3.vcf", Charset.forName(CharEncoding.UTF_8));
assertEquals("Gümp", c.familyName);
assertEquals("Förrest", c.givenName);
assertEquals("Förrest Gümp", c.displayName);
assertEquals("Bubba Gump Shrimpß Co.", c.organization.getValues().get(0));
assertEquals("Shrimp Man", c.jobTitle);
Telephone phone1 = c.getPhoneNumbers().get(0);
assertEquals("(111) 555-1212", phone1.getText());
assertEquals("WORK", phone1.getParameters("TYPE").get(0));
assertEquals("VOICE", phone1.getParameters("TYPE").get(1));
Telephone phone2 = c.getPhoneNumbers().get(1);
assertEquals("(404) 555-1212", phone2.getText());
assertEquals("HOME", phone2.getParameters("TYPE").get(0));
assertEquals("VOICE", phone2.getParameters("TYPE").get(1));
Email email = c.getEmails().get(0);
assertEquals("[email protected]", email.getValue());
assertEquals("PREF", email.getParameters("TYPE").get(0));
assertEquals("INTERNET", email.getParameters("TYPE").get(1));
@Cleanup InputStream photoStream = assetMgr.open("davdroid-logo-192.png", AssetManager.ACCESS_STREAMING);
byte[] expectedPhoto = IOUtils.toByteArray(photoStream);
assertTrue(Arrays.equals(c.photo, expectedPhoto));
}
public void testParseInvalidUnknownProperties() throws IOException {
Contact c = parseVCF("invalid-unknown-properties.vcf");
assertEquals("VCard with invalid unknown properties", c.displayName);
assertNull(c.unknownProperties);
}
public void testParseLatin1() throws IOException {
Contact c = parseVCF("latin1.vcf", Charset.forName(CharEncoding.ISO_8859_1));
assertEquals("Özkan Äuçek", c.displayName);
assertEquals("Özkan", c.givenName);
assertEquals("Äuçek", c.familyName);
assertNull(c.unknownProperties);
}
protected Contact parseVCF(String fname, Charset charset) throws IOException {
@Cleanup InputStream in = assetMgr.open(fname, AssetManager.ACCESS_STREAMING);
Contact c = new Contact(fname, null);
c.parseEntity(in, charset, new Resource.AssetDownloader() {
@Override
public byte[] download(URI uri) throws URISyntaxException, IOException, HttpException, DavException {
return IOUtils.toByteArray(uri);
}
});
return c;
}
protected Contact parseVCF(String fname) throws IOException {
return parseVCF(fname, null);
}
}
......@@ -30,7 +30,6 @@ public class DavResourceFinderTest extends InstrumentationTestCase {
@Override
protected void tearDown() throws IOException {
finder.close();
}
......@@ -39,7 +38,6 @@ public class DavResourceFinderTest extends InstrumentationTestCase {
finder.findResources(info);
/*** CardDAV ***/
assertTrue(info.isCardDAV());
List<ResourceInfo> collections = info.getAddressBooks();
// two address books
assertEquals(2, collections.size());
......@@ -52,7 +50,6 @@ public class DavResourceFinderTest extends InstrumentationTestCase {
assertEquals("Absolute URI VCard Book", collection.getDescription());
/*** CalDAV ***/
assertTrue(info.isCalDAV());
collections = info.getCalendars();
assertEquals(2, collections.size());
......
/*
* 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.resource;
import org.apache.http.impl.client.CloseableHttpClient;
import java.net.URISyntaxException;
import at.bitfire.davdroid.syncadapter.AccountSettings;
import at.bitfire.davdroid.webdav.DavMultiget;
import ezvcard.VCardVersion;
public class CardDavAddressBook extends WebDavCollection<Contact> {
AccountSettings accountSettings;
@Override
protected String memberAcceptedMimeTypes() {
return "text/vcard;q=0.8, text/vcard;version=4.0";
}
@Override
protected DavMultiget.Type multiGetType() {
return accountSettings.getAddressBookVCardVersion() == VCardVersion.V4_0 ?
DavMultiget.Type.ADDRESS_BOOK_V4 : DavMultiget.Type.ADDRESS_BOOK;
}
@Override
protected Contact newResourceSkeleton(String name, String ETag) {
return new Contact(name, ETag);
}
public CardDavAddressBook(AccountSettings settings, CloseableHttpClient httpClient, String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
super(httpClient, baseURL, user, password, preemptiveAuth);
accountSettings = settings;
}
}
/*
* 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.resource;
import at.bitfire.vcard4android.AndroidAddressBook;
import at.bitfire.vcard4android.AndroidContact;
import at.bitfire.vcard4android.AndroidContactFactory;
import at.bitfire.vcard4android.Contact;
public class LocalContact extends AndroidContact {
protected LocalContact(AndroidAddressBook addressBook, long id) {
super(addressBook, id);
}
public LocalContact(AndroidAddressBook addressBook, Contact contact) {
super(addressBook, contact);
}
static class Factory extends AndroidContactFactory {
static final Factory INSTANCE = new Factory();
@Override
public LocalContact newInstance(AndroidAddressBook addressBook, long id) {
return new LocalContact(addressBook, id);
}
@Override
public LocalContact newInstance(AndroidAddressBook addressBook, Contact contact) {
return new LocalContact(addressBook, contact);
}
public LocalContact[] newArray(int size) {
return new LocalContact[size];
}
}
}
......@@ -9,20 +9,29 @@ package at.bitfire.davdroid.syncadapter;
import android.accounts.Account;
import android.app.Service;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.Context;
import android.content.Intent;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.ResponseBody;
import at.bitfire.davdroid.resource.CardDavAddressBook;
import org.apache.commons.io.Charsets;
import at.bitfire.dav4android.DavAddressBook;
import at.bitfire.dav4android.DavResource;
import at.bitfire.dav4android.property.SupportedAddressData;
import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.HttpClient;
import at.bitfire.davdroid.resource.LocalAddressBook;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.WebDavCollection;
import at.bitfire.davdroid.resource.LocalContact;
import at.bitfire.vcard4android.Contact;
public class ContactsSyncAdapterService extends Service {
private static ContactsSyncAdapter syncAdapter;
......@@ -35,7 +44,6 @@ public class ContactsSyncAdapterService extends Service {
@Override
public void onDestroy() {
syncAdapter.close();
syncAdapter = null;
}
......@@ -45,37 +53,53 @@ public class ContactsSyncAdapterService extends Service {
}
private static class ContactsSyncAdapter extends DavSyncAdapter {
private final static String TAG = "davdroid.ContactsSync";
private ContactsSyncAdapter(Context context) {
super(context);
}
@Override
protected Map<LocalCollection<?>, WebDavCollection<?>> getSyncPairs(Account account, ContentProviderClient provider) {
AccountSettings settings = new AccountSettings(getContext(), account);
String userName = settings.getUserName(),
password = settings.getPassword();
boolean preemptive = settings.getPreemptiveAuth();
String addressBookURL = settings.getAddressBookURL();
if (addressBookURL == null)
return null;
try {
LocalCollection<?> database = new LocalAddressBook(account, provider, settings);
WebDavCollection<?> dav = new CardDavAddressBook(settings, httpClient, addressBookURL, userName, password, preemptive);
Map<LocalCollection<?>, WebDavCollection<?>> map = new HashMap<>();
map.put(database, dav);
return map;
} catch (URISyntaxException ex) {
Log.e(TAG, "Couldn't build address book URI", ex);
}
return null;
}
}
private static class ContactsSyncAdapter extends AbstractThreadedSyncAdapter {
public ContactsSyncAdapter(Context context) {
super(context, false);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Constants.log.info("Starting sync for authority " + authority);
AccountSettings settings = new AccountSettings(getContext(), account);
HttpClient httpClient = new HttpClient(settings.getUserName(), settings.getPassword(), settings.getPreemptiveAuth());
DavAddressBook dav = new DavAddressBook(httpClient, HttpUrl.parse(settings.getAddressBookURL()));
try {
boolean hasVCard4 = false;
dav.propfind(0, SupportedAddressData.NAME);
SupportedAddressData supportedAddressData = (SupportedAddressData)dav.properties.get(SupportedAddressData.NAME);
if (supportedAddressData != null)
for (MediaType type : supportedAddressData.types)
if ("text/vcard; version=4.0".equalsIgnoreCase(type.toString()))
hasVCard4 = true;
Constants.log.info("Server advertises VCard/4 support: " + hasVCard4);
LocalAddressBook addressBook = new LocalAddressBook(account, provider);
dav.queryMemberETags();
for (DavResource vCard : dav.members) {
Constants.log.info("Found remote VCard: " + vCard.location);
ResponseBody body = vCard.get("text/vcard;q=0.8, text/vcard;version=4.0");
Contact contacts[] = Contact.fromStream(body.byteStream(), body.contentType().charset(Charsets.UTF_8));
if (contacts.length == 1) {
Contact contact = contacts[0];
Constants.log.info(contact.toString());
LocalContact localContact = new LocalContact(addressBook, contact);
localContact.add();
} else
Constants.log.error("Received VCard with not exactly one VCARD");
}
} catch (Exception e) {
Log.e("davdroid", "querying member etags", e);
}
Constants.log.info("Sync complete for authority " + authority);
}
}
}
......@@ -8,3 +8,4 @@
include ':app'
include ':dav4android'
include ':vcard4android'
Subproject commit 644ee03c74d35837974db771a7093f6f28623fbc
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment