Fix "can't edit contact" / "account doesn't show up" and other bugs; refactoring

* support for phonetic names (closes #19)
* update contacts.xml, tested with 4.0 (Samsung), 4.2 (Cyanogen), 4.3 (Cyanogen) (fixes #5, fixes #6, fixes #7)
* smarter error handling (1): notify sync manager in case of HTTP auth errors
* smarter error handling (2): just ignore the dubious resources instead of notifying Android sync service
* refactoring: created DavSyncAdapter and move common code to it
* version bump to 0.3.4-alpha
pull/2/head
rfc2822 11 years ago
parent c87fb7bedd
commit 5db8bcb9d8

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="at.bitfire.davdroid"
android:versionCode="6"
android:versionName="0.3.3-alpha" >
android:versionCode="7"
android:versionName="0.3.4-alpha" >
<uses-sdk
android:minSdkVersion="14"
@ -34,7 +34,8 @@
</service>
<service
android:name=".syncadapter.ContactsSyncAdapterService"
android:exported="true" >
android:exported="true"
android:process=":sync" >
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
@ -48,7 +49,8 @@
</service>
<service
android:name=".syncadapter.CalendarsSyncAdapterService"
android:exported="true" >
android:exported="true"
android:process=":sync" >
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>

@ -30,6 +30,6 @@
<string name="calendars">Calendars</string>
<string name="select_address_book">Select up to one address book (tap again to unselect):</string>
<string name="select_calendars">Select your calendars:</string>
<string name="auth_preemptive">Preemptive authentification (recommended, but incompatible with Digest auth)</string>
<string name="auth_preemptive">Preemptive authentication (recommended, but incompatible with Digest auth)</string>
</resources>

@ -4,7 +4,7 @@
<Preference android:title="DAVdroid Web site" >
<intent
android:action="android.intent.action.VIEW"
android:data="http://davdroid.bitfire.at" />
android:data="http://davdroid.bitfire.at/?pk_campaign=in-app" />
</Preference>
</PreferenceScreen>

@ -1,54 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<ContactsSource xmlns:android="http://schemas.android.com/apk/res/android">
<ContactsAccountType
xmlns:android="http://schemas.android.com/apk/res/android">
<EditSchema>
<DataKind
kind="name"
maxOccurs="1"
supportsDisplayName="true"
supportsMiddleName="true"
supportsFamilyName="true"
supportsPhoneticFamilyName="false"
supportsPhoneticGivenName="false"
supportsPhoneticMiddleName="false"
supportsPrefix="true"
supportsSuffix="true" />
<DataKind
kind="photo"
maxOccurs="1" />
<DataKind kind="phone">
<Type type="fax_home" />
<Type type="fax_work" />
<Type type="home" />
<Type type="mobile" />
<Type type="other_fax" />
<Type type="pager" />
<Type type="work" />
<Type type="work_mobile" />
<Type type="work_pager" />
</DataKind>
<DataKind kind="email">
<Type type="home" />
<Type type="work" />
<Type type="other" />
</DataKind>
<DataKind
kind="nickname"
maxOccurs="1" />
<DataKind kind="website" />
<DataKind kind="note" />
<DataKind
dateWithTime="false"
kind="event">
<Type
maxOccurs="1"
type="birthday"
yearOptional="false" />
</DataKind>
</EditSchema>
</ContactsSource>
<DataKind
kind="name"
maxOccurs="1"
supportsDisplayName="true"
supportsMiddleName="true"
supportsFamilyName="true"
supportsPhoneticFamilyName="true"
supportsPhoneticGivenName="true"
supportsPhoneticMiddleName="true"
supportsPrefix="true"
supportsSuffix="true" />
<DataKind
kind="photo"
maxOccurs="1" />
<DataKind kind="phone">
<Type type="fax_home" />
<Type type="fax_work" />
<Type type="home" />
<Type type="mobile" />
<Type type="other_fax" />
<Type type="pager" />
<Type type="work" />
<Type type="work_mobile" />
<Type type="work_pager" />
</DataKind>
<DataKind kind="email">
<Type type="home" />
<Type type="work" />
<Type type="other" />
</DataKind>
<DataKind
kind="nickname"
maxOccurs="1" />
<DataKind kind="website" />
<DataKind
kind="note"
maxOccurs="1" />
<DataKind
dateWithTime="false"
kind="event">
<Type
maxOccurs="1"
type="birthday"
yearOptional="false" />
</DataKind>
</EditSchema>
</ContactsAccountType>

@ -9,7 +9,7 @@ package at.bitfire.davdroid;
public class Constants {
public static final String
APP_VERSION = "0.3.3-alpha",
APP_VERSION = "0.3.4-alpha",
ACCOUNT_TYPE = "bitfire.at.davdroid",

@ -0,0 +1,46 @@
package at.bitfire.davdroid.ical4j;
import java.util.List;
import net.fortuna.ical4j.model.ValidationException;
import net.fortuna.ical4j.vcard.Group;
import net.fortuna.ical4j.vcard.Parameter;
import net.fortuna.ical4j.vcard.Property;
import net.fortuna.ical4j.vcard.PropertyFactory;
public class PhoneticFirstName extends Property {
private static final long serialVersionUID = 8096989375023262021L;
public static final String PROPERTY_NAME = "PHONETIC-FIRST-NAME";
protected String phoneticFirstName;
public PhoneticFirstName(String value) {
super(PROPERTY_NAME);
phoneticFirstName = value;
}
@Override
public String getValue() {
return phoneticFirstName;
}
@Override
public void validate() throws ValidationException {
}
public static class Factory implements PropertyFactory<Property> {
@Override
public PhoneticFirstName createProperty(List<Parameter> params, String value) {
return new PhoneticFirstName(value);
}
@Override
public PhoneticFirstName createProperty(Group group, List<Parameter> params, String value) {
return new PhoneticFirstName(value);
}
}
}

@ -0,0 +1,46 @@
package at.bitfire.davdroid.ical4j;
import java.util.List;
import net.fortuna.ical4j.model.ValidationException;
import net.fortuna.ical4j.vcard.Group;
import net.fortuna.ical4j.vcard.Parameter;
import net.fortuna.ical4j.vcard.Property;
import net.fortuna.ical4j.vcard.PropertyFactory;
public class PhoneticLastName extends Property {
private static final long serialVersionUID = 8637699713562385556L;
public static final String PROPERTY_NAME = "PHONETIC-LAST-NAME";
protected String phoneticFirstName;
public PhoneticLastName(String value) {
super(PROPERTY_NAME);
phoneticFirstName = value;
}
@Override
public String getValue() {
return phoneticFirstName;
}
@Override
public void validate() throws ValidationException {
}
public static class Factory implements PropertyFactory<Property> {
@Override
public PhoneticLastName createProperty(List<Parameter> params, String value) {
return new PhoneticLastName(value);
}
@Override
public PhoneticLastName createProperty(Group group, List<Parameter> params, String value) {
return new PhoneticLastName(value);
}
}
}

@ -0,0 +1,46 @@
package at.bitfire.davdroid.ical4j;
import java.util.List;
import net.fortuna.ical4j.model.ValidationException;
import net.fortuna.ical4j.vcard.Group;
import net.fortuna.ical4j.vcard.Parameter;
import net.fortuna.ical4j.vcard.Property;
import net.fortuna.ical4j.vcard.PropertyFactory;
public class PhoneticMiddleName extends Property {
private static final long serialVersionUID = 1310410178765057503L;
public static final String PROPERTY_NAME = "PHONETIC-MIDDLE-NAME";
protected String phoneticFirstName;
public PhoneticMiddleName(String value) {
super(PROPERTY_NAME);
phoneticFirstName = value;
}
@Override
public String getValue() {
return phoneticFirstName;
}
@Override
public void validate() throws ValidationException {
}
public static class Factory implements PropertyFactory<Property> {
@Override
public PhoneticMiddleName createProperty(List<Parameter> params, String value) {
return new PhoneticMiddleName(value);
}
@Override
public PhoneticMiddleName createProperty(Group group, List<Parameter> params, String value) {
return new PhoneticMiddleName(value);
}
}
}

@ -31,7 +31,7 @@ public class CalDavCalendar extends RemoteCollection<Event> {
}
public CalDavCalendar(String baseURL, String user, String password, boolean preemptiveAuth) throws IOException, URISyntaxException {
public CalDavCalendar(String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
super(baseURL, user, password, preemptiveAuth);
}
}

@ -31,7 +31,7 @@ public class CardDavAddressBook extends RemoteCollection<Contact> {
}
public CardDavAddressBook(String baseURL, String user, String password, boolean preemptiveAuth) throws IOException, URISyntaxException {
public CardDavAddressBook(String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
super(baseURL, user, password, preemptiveAuth);
}
}

@ -51,6 +51,9 @@ import org.apache.commons.lang.StringUtils;
import android.util.Base64;
import android.util.Log;
import at.bitfire.davdroid.ical4j.PhoneticFirstName;
import at.bitfire.davdroid.ical4j.PhoneticLastName;
import at.bitfire.davdroid.ical4j.PhoneticMiddleName;
import at.bitfire.davdroid.ical4j.Starred;
@ToString(callSuper = true)
@ -61,6 +64,7 @@ public class Contact extends Resource {
@Getter @Setter private String displayName;
@Getter @Setter private String prefix, givenName, middleName, familyName, suffix;
@Getter @Setter private String phoneticGivenName, phoneticMiddleName, phoneticFamilyName;
@Getter @Setter private String[] nickNames;
@Getter @Setter private byte[] photo;
@ -107,7 +111,14 @@ public class Contact extends Resource {
@Override
public void parseEntity(InputStream is) throws IOException, ParserException {
PropertyFactoryRegistry propertyFactoryRegistry = new PropertyFactoryRegistry();
// add support for X-DAVDROID-STARRED
propertyFactoryRegistry.register("X-" + Starred.PROPERTY_NAME, new Starred.Factory());
// add support for phonetic names
propertyFactoryRegistry.register("X-" + PhoneticFirstName.PROPERTY_NAME, new PhoneticFirstName.Factory());
propertyFactoryRegistry.register("X-" + PhoneticMiddleName.PROPERTY_NAME, new PhoneticMiddleName.Factory());
propertyFactoryRegistry.register("X-" + PhoneticLastName.PROPERTY_NAME, new PhoneticLastName.Factory());
VCardBuilder builder = new VCardBuilder(
new InputStreamReader(is),
@ -115,9 +126,15 @@ public class Contact extends Resource {
propertyFactoryRegistry,
new ParameterFactoryRegistry()
);
VCard vcard = builder.build();
if (vcard == null)
return;
VCard vcard;
try {
vcard = builder.build();
if (vcard == null)
return;
} catch(Exception ex) {
throw new ParserException("VCard parser crashed", -1);
}
Uid uid = (Uid)vcard.getProperty(Id.UID);
if (uid != null)
@ -137,6 +154,7 @@ public class Contact extends Resource {
if (nickname != null)
nickNames = nickname.getNames();
// structured name
N n = (N)vcard.getProperty(Id.N);
if (n != null) {
prefix = StringUtils.join(n.getPrefixes(), " ");
@ -146,6 +164,17 @@ public class Contact extends Resource {
suffix = StringUtils.join(n.getSuffixes(), " ");
}
// phonetic name
PhoneticFirstName phoneticFirstName = (PhoneticFirstName)vcard.getExtendedProperty(PhoneticFirstName.PROPERTY_NAME);
if (phoneticFirstName != null)
phoneticGivenName = phoneticFirstName.getValue();
PhoneticMiddleName phoneticMiddleName = (PhoneticMiddleName)vcard.getExtendedProperty(PhoneticMiddleName.PROPERTY_NAME);
if (phoneticMiddleName != null)
this.phoneticMiddleName = phoneticMiddleName.getValue();
PhoneticLastName phoneticLastName = (PhoneticLastName)vcard.getExtendedProperty(PhoneticLastName.PROPERTY_NAME);
if (phoneticLastName != null)
phoneticFamilyName = phoneticLastName.getValue();
for (Property p : vcard.getProperties(Id.EMAIL))
emails.add((Email)p);
@ -228,6 +257,13 @@ public class Contact extends Resource {
properties.add(new N(familyName, givenName, StringUtils.split(middleName),
StringUtils.split(prefix), StringUtils.split(suffix)));
if (phoneticGivenName != null)
properties.add(new PhoneticFirstName(phoneticGivenName));
if (phoneticMiddleName != null)
properties.add(new PhoneticMiddleName(phoneticMiddleName));
if (phoneticFamilyName != null)
properties.add(new PhoneticLastName(phoneticFamilyName));
for (Email email : emails)
properties.add(email);

@ -125,8 +125,9 @@ public class LocalAddressBook extends LocalCollection<Contact> {
// structured name
cursor = providerClient.query(dataURI(), new String[] {
StructuredName.DISPLAY_NAME, StructuredName.PREFIX, StructuredName.GIVEN_NAME,
StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME, StructuredName.SUFFIX
/* 0 */ StructuredName.DISPLAY_NAME, StructuredName.PREFIX, StructuredName.GIVEN_NAME,
/* 3 */ StructuredName.MIDDLE_NAME, StructuredName.FAMILY_NAME, StructuredName.SUFFIX,
/* 6 */ StructuredName.PHONETIC_GIVEN_NAME, StructuredName.PHONETIC_MIDDLE_NAME, StructuredName.PHONETIC_FAMILY_NAME
}, StructuredName.RAW_CONTACT_ID + "=? AND " + Data.MIMETYPE + "=?",
new String[] { String.valueOf(res.getLocalID()), StructuredName.CONTENT_ITEM_TYPE }, null);
if (cursor.moveToNext()) {
@ -137,6 +138,10 @@ public class LocalAddressBook extends LocalCollection<Contact> {
c.setMiddleName(cursor.getString(3));
c.setFamilyName(cursor.getString(4));
c.setSuffix(cursor.getString(5));
c.setPhoneticGivenName(cursor.getString(6));
c.setPhoneticMiddleName(cursor.getString(7));
c.setPhoneticFamilyName(cursor.getString(8));
}
// nick names
@ -347,7 +352,10 @@ public class LocalAddressBook extends LocalCollection<Contact> {
.withValue(StructuredName.GIVEN_NAME, contact.getGivenName())
.withValue(StructuredName.MIDDLE_NAME, contact.getMiddleName())
.withValue(StructuredName.FAMILY_NAME, contact.getFamilyName())
.withValue(StructuredName.SUFFIX, contact.getSuffix());
.withValue(StructuredName.SUFFIX, contact.getSuffix())
.withValue(StructuredName.PHONETIC_GIVEN_NAME, contact.getPhoneticGivenName())
.withValue(StructuredName.PHONETIC_MIDDLE_NAME, contact.getPhoneticMiddleName())
.withValue(StructuredName.PHONETIC_FAMILY_NAME, contact.getPhoneticFamilyName());
}
protected Builder buildNickName(Builder builder, String nickName) {

@ -11,7 +11,6 @@ import java.net.URISyntaxException;
import java.text.ParseException;
import java.util.LinkedList;
import java.util.List;
import lombok.Getter;
import net.fortuna.ical4j.model.Parameter;
import net.fortuna.ical4j.model.ParameterList;
@ -26,11 +25,10 @@ import net.fortuna.ical4j.model.property.Organizer;
import net.fortuna.ical4j.model.property.RDate;
import net.fortuna.ical4j.model.property.RRule;
import net.fortuna.ical4j.model.property.Status;
import org.apache.commons.lang.StringUtils;
import android.accounts.Account;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ContentProviderClient;
import android.content.ContentProviderOperation;
import android.content.ContentProviderOperation.Builder;
@ -40,6 +38,7 @@ import android.content.ContentValues;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.net.Uri;
import android.os.Build;
import android.os.RemoteException;
import android.provider.CalendarContract;
import android.provider.CalendarContract.Attendees;
@ -75,9 +74,9 @@ public class LocalCalendar extends LocalCollection<Event> {
protected String entryColumnDirty() { return Events.DIRTY; }
protected String entryColumnDeleted() { return Events.DELETED; }
@SuppressLint("InlinedApi")
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
protected String entryColumnUID() {
return (android.os.Build.VERSION.SDK_INT >= 17) ?
return (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) ?
Events.UID_2445 : Events.SYNC_DATA2;
}

@ -19,20 +19,24 @@ import net.fortuna.ical4j.model.ValidationException;
import org.apache.http.HttpException;
import android.util.Log;
import at.bitfire.davdroid.webdav.HttpPropfind;
import at.bitfire.davdroid.webdav.InvalidDavResponseException;
import at.bitfire.davdroid.webdav.WebDavCollection;
import at.bitfire.davdroid.webdav.WebDavCollection.MultigetType;
import at.bitfire.davdroid.webdav.WebDavResource;
import at.bitfire.davdroid.webdav.WebDavResource.PutMode;
public abstract class RemoteCollection<ResourceType extends Resource> {
private static final String TAG = "davdroid.RemoteCollection";
@Getter WebDavCollection collection;
abstract protected String memberContentType();
abstract protected MultigetType multiGetType();
abstract protected ResourceType newResourceSkeleton(String name, String ETag);
public RemoteCollection(String baseURL, String user, String password, boolean preemptiveAuth) throws IOException, URISyntaxException {
public RemoteCollection(String baseURL, String user, String password, boolean preemptiveAuth) throws URISyntaxException {
collection = new WebDavCollection(new URI(baseURL), user, password, preemptiveAuth);
}
@ -43,13 +47,13 @@ public abstract class RemoteCollection<ResourceType extends Resource> {
try {
if (collection.getCTag() == null && collection.getMembers() == null) // not already fetched
collection.propfind(HttpPropfind.Mode.COLLECTION_CTAG);
} catch (IncapableResourceException e) {
} catch (InvalidDavResponseException e) {
return null;
}
return collection.getCTag();
}
public Resource[] getMemberETags() throws IOException, IncapableResourceException, HttpException {
public Resource[] getMemberETags() throws IOException, InvalidDavResponseException, HttpException {
collection.propfind(HttpPropfind.Mode.MEMBERS_ETAG);
List<ResourceType> resources = new LinkedList<ResourceType>();
@ -59,7 +63,7 @@ public abstract class RemoteCollection<ResourceType extends Resource> {
}
@SuppressWarnings("unchecked")
public Resource[] multiGet(ResourceType[] resources) throws IOException, IncapableResourceException, HttpException, ParserException {
public Resource[] multiGet(ResourceType[] resources) throws IOException, InvalidDavResponseException, HttpException {
try {
if (resources.length == 1) {
Resource resource = get(resources[0]);
@ -75,19 +79,25 @@ public abstract class RemoteCollection<ResourceType extends Resource> {
LinkedList<ResourceType> foundResources = new LinkedList<ResourceType>();
for (WebDavResource member : collection.getMembers()) {
ResourceType resource = newResourceSkeleton(member.getName(), member.getETag());
resource.parseEntity(member.getContent());
foundResources.add(resource);
try {
resource.parseEntity(member.getContent());
foundResources.add(resource);
} catch (ParserException ex) {
Log.e(TAG, "Ignoring unparseable entity in multi-response: " + ex.toString(), ex);
}
}
return foundResources.toArray(new Resource[0]);
} catch(ValidationException ex) {
return null;
} catch (ParserException ex) {
Log.w(TAG, "Couldn't parse single multi-get entity", ex);
}
return new Resource[0];
}
/* internal member operations */
public ResourceType get(ResourceType resource) throws IOException, HttpException, ParserException, ValidationException {
public ResourceType get(ResourceType resource) throws IOException, HttpException, ParserException {
WebDavResource member = new WebDavResource(collection, resource.getName());
member.get();
resource.parseEntity(member.getContent());

@ -10,11 +10,14 @@ package at.bitfire.davdroid.syncadapter;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import lombok.Synchronized;
import net.fortuna.ical4j.data.ParserException;
import org.apache.http.HttpException;
import org.apache.http.auth.AuthenticationException;
import android.accounts.Account;
import android.accounts.AccountManager;
@ -34,6 +37,7 @@ import at.bitfire.davdroid.Constants;
import at.bitfire.davdroid.resource.CalDavCalendar;
import at.bitfire.davdroid.resource.IncapableResourceException;
import at.bitfire.davdroid.resource.LocalCalendar;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.RemoteCollection;
import at.bitfire.davdroid.webdav.WebDavResource;
@ -51,58 +55,35 @@ public class CalendarsSyncAdapterService extends Service {
return syncAdapter.getSyncAdapterBinder();
}
private static class SyncAdapter extends AbstractThreadedSyncAdapter {
private static class SyncAdapter extends DavSyncAdapter {
private final static String TAG = "davdroid.CalendarsSyncAdapter";
private AccountManager accountManager;
public SyncAdapter(Context context) {
super(context, true);
accountManager = AccountManager.get(context);
super(context);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider,
SyncResult syncResult) {
Log.i(TAG, "Performing sync for authority " + authority);
// set class loader for iCal4j ResourceLoader
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
protected Map<LocalCollection, RemoteCollection> getSyncPairs(Account account, ContentProviderClient provider) {
try {
SyncManager syncManager = new SyncManager(account, accountManager);
LocalCalendar[] calendars = LocalCalendar.findAll(account, provider);
for (LocalCalendar calendar : calendars) {
Map<LocalCollection, RemoteCollection> map = new HashMap<LocalCollection, RemoteCollection>();
for (LocalCalendar calendar : LocalCalendar.findAll(account, provider)) {
URI uri = new URI(accountManager.getUserData(account, Constants.ACCOUNT_KEY_BASE_URL)).resolve(calendar.getPath());
RemoteCollection dav = new CalDavCalendar(uri.toString(),
accountManager.getUserData(account, Constants.ACCOUNT_KEY_USERNAME),
accountManager.getPassword(account),
Boolean.parseBoolean(accountManager.getUserData(account, Constants.ACCOUNT_KEY_AUTH_PREEMPTIVE)));
syncManager.synchronize(calendar, dav, extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL), syncResult);
map.put(calendar, dav);
}
} catch (HttpException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, e.toString());
} catch (ParserException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, e.toString());
} catch (RemoteException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, e.getLocalizedMessage());
} catch (OperationApplicationException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, e.getLocalizedMessage());
} catch (IOException e) {
syncResult.stats.numIoExceptions++;
Log.e(TAG, e.toString());
} catch (IncapableResourceException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, e.toString());
} catch (URISyntaxException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, e.toString());
return map;
} catch (RemoteException ex) {
Log.e(TAG, "Couldn't find local calendars", ex);
} catch (URISyntaxException ex) {
Log.e(TAG, "Couldn't build calendar URI", ex);
}
return null;
}
}
}

@ -10,7 +10,11 @@ package at.bitfire.davdroid.syncadapter;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.NonNull;
import lombok.Synchronized;
import net.fortuna.ical4j.data.ParserException;
@ -38,12 +42,12 @@ import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.RemoteCollection;
public class ContactsSyncAdapterService extends Service {
private static SyncAdapter syncAdapter;
private static ContactsSyncAdapter syncAdapter;
@Override @Synchronized
public void onCreate() {
if (syncAdapter == null)
syncAdapter = new SyncAdapter(getApplicationContext());
syncAdapter = new ContactsSyncAdapter(getApplicationContext());
}
@Override
@ -51,59 +55,38 @@ public class ContactsSyncAdapterService extends Service {
return syncAdapter.getSyncAdapterBinder();
}
private static class SyncAdapter extends AbstractThreadedSyncAdapter {
private static class ContactsSyncAdapter extends DavSyncAdapter {
private final static String TAG = "davdroid.ContactsSyncAdapter";
private AccountManager accountManager;
public SyncAdapter(Context context) {
super(context, true);
accountManager = AccountManager.get(context);
public ContactsSyncAdapter(Context context) {
super(context);
}
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Log.i(TAG, "Performing sync for authority " + authority);
protected Map<LocalCollection, RemoteCollection> getSyncPairs(Account account, ContentProviderClient provider) {
String addressBookPath = accountManager.getUserData(account, Constants.ACCOUNT_KEY_ADDRESSBOOK_PATH);
if (addressBookPath == null)
return;
return null;
try {
URI uri = new URI(accountManager.getUserData(account, Constants.ACCOUNT_KEY_BASE_URL)).resolve(addressBookPath);
LocalCollection database = new LocalAddressBook(account, provider, accountManager);
URI uri = new URI(accountManager.getUserData(account, Constants.ACCOUNT_KEY_BASE_URL)).resolve(addressBookPath);
RemoteCollection dav = new CardDavAddressBook(
uri.toString(),
accountManager.getUserData(account, Constants.ACCOUNT_KEY_USERNAME),
accountManager.getPassword(account),
Boolean.parseBoolean(accountManager.getUserData(account, Constants.ACCOUNT_KEY_AUTH_PREEMPTIVE)));
LocalCollection database = new LocalAddressBook(account, provider, accountManager);
SyncManager syncManager = new SyncManager(account, accountManager);
syncManager.synchronize(database, dav, extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL), syncResult);
Map<LocalCollection, RemoteCollection> map = new HashMap<LocalCollection, RemoteCollection>();
map.put(database, dav);
} catch (IOException e) {
syncResult.stats.numIoExceptions++;
Log.e(TAG, e.toString());
} catch (ParserException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, e.toString());
} catch (HttpException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, e.toString());
} catch (IncapableResourceException e) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, e.toString());
} catch(RemoteException e) {
syncResult.databaseError = true;
Log.e(TAG, e.toString());
} catch(OperationApplicationException e) {
syncResult.databaseError = true;
Log.e(TAG, e.toString());
} catch (URISyntaxException e) {
syncResult.stats.numIoExceptions++;
Log.e(TAG, e.toString());
return map;
} catch (URISyntaxException ex) {
Log.e(TAG, "Couldn't build address book URI", ex);
}
return null;
}
}
}

@ -0,0 +1,78 @@
package at.bitfire.davdroid.syncadapter;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Map;
import org.apache.http.HttpException;
import org.apache.http.auth.AuthenticationException;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.OperationApplicationException;
import android.content.SyncResult;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
import at.bitfire.davdroid.resource.IncapableResourceException;
import at.bitfire.davdroid.resource.LocalCollection;
import at.bitfire.davdroid.resource.RemoteCollection;
import at.bitfire.davdroid.webdav.InvalidDavResponseException;
public abstract class DavSyncAdapter extends AbstractThreadedSyncAdapter {
private final static String TAG = "davdroid.DavSyncAdapter";
protected AccountManager accountManager;
public DavSyncAdapter(Context context) {
super(context, true);
accountManager = AccountManager.get(context);
}
protected abstract Map<LocalCollection, RemoteCollection> getSyncPairs(Account account, ContentProviderClient provider);
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
Log.i(TAG, "Performing sync for authority " + authority);
// set class loader for iCal4j ResourceLoader
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
SyncManager syncManager = new SyncManager(account, accountManager);
Map<LocalCollection, RemoteCollection> syncCollections = getSyncPairs(account, provider);
if (syncCollections == null)
Log.i(TAG, "Nothing to synchronize");
else
try {
for (Map.Entry<LocalCollection, RemoteCollection> entry : syncCollections.entrySet())
syncManager.synchronize(entry.getKey(), entry.getValue(), extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL), syncResult);
} catch (AuthenticationException ex) {
syncResult.stats.numAuthExceptions++;
Log.e(TAG, "HTTP authorization error", ex);
} catch (InvalidDavResponseException ex) {
syncResult.stats.numParseExceptions++;
Log.e(TAG, "Invalid DAV response", ex);
} catch (HttpException ex) {
syncResult.stats.numIoExceptions++;
Log.e(TAG, "HTTP error", ex);
} catch (OperationApplicationException ex) {
syncResult.databaseError = true;
Log.e(TAG, "Content provider operation error", ex);
} catch (RemoteException ex) {
syncResult.databaseError = true;
Log.e(TAG, "Remote process (content provider?) died", ex);
} catch (IOException ex) {
syncResult.stats.numIoExceptions++;
Log.e(TAG, "I/O error", ex);
}
}
}

@ -40,7 +40,7 @@ public class SyncManager {
this.accountManager = accountManager;
}
public void synchronize(LocalCollection local, RemoteCollection dav, boolean manualSync, SyncResult syncResult) throws RemoteException, OperationApplicationException, IOException, IncapableResourceException, HttpException, ParserException {
public void synchronize(LocalCollection local, RemoteCollection dav, boolean manualSync, SyncResult syncResult) throws RemoteException, OperationApplicationException, IOException, HttpException {
boolean fetchCollection = false;
// PHASE 1: UPLOAD LOCALLY-CHANGED RESOURCES
@ -139,8 +139,8 @@ public class SyncManager {
Log.i(TAG, "Adding " + res.getName());
try {
local.add(res);
} catch (ValidationException e) {
Log.e(TAG, "Invalid resource: " + res.getName());
} catch (ValidationException ex) {
Log.w(TAG, "Ignoring invalid remote resource: " + res.getName(), ex);
}
syncResult.stats.numInserts++;
}
@ -151,8 +151,8 @@ public class SyncManager {
for (Resource res : dav.multiGet(resourcesToUpdate.toArray(new Resource[0]))) {
try {
local.updateByRemoteName(res);
} catch (ValidationException e) {
Log.e(TAG, "Invalid resource: " + res.getName());
} catch (ValidationException ex) {
Log.e(TAG, "Ignoring invalid remote resource: " + res.getName(), ex);
}
Log.i(TAG, "Updating " + res.getName());
syncResult.stats.numInserts++;

@ -0,0 +1,11 @@
package at.bitfire.davdroid.webdav;
import org.apache.http.HttpException;
public class InvalidDavResponseException extends HttpException {
private static final long serialVersionUID = -2118919144443165706L;
public InvalidDavResponseException() {
super("Invalid DAV response");
}
}

@ -44,7 +44,7 @@ public class WebDavCollection extends WebDavResource {
@Getter protected List<WebDavResource> members = new LinkedList<WebDavResource>();
public WebDavCollection(URI baseURL, String username, String password, boolean preemptiveAuth) throws IOException {
public WebDavCollection(URI baseURL, String username, String password, boolean preemptiveAuth) {
super(baseURL, username, password, preemptiveAuth);
}
@ -60,7 +60,7 @@ public class WebDavCollection extends WebDavResource {
/* collection operations */
public boolean propfind(HttpPropfind.Mode mode) throws IOException, IncapableResourceException, HttpException {
public boolean propfind(HttpPropfind.Mode mode) throws IOException, InvalidDavResponseException, HttpException {
HttpPropfind propfind = new HttpPropfind(location, mode);
HttpResponse response = client.execute(propfind);
checkResponse(response);
@ -74,9 +74,9 @@ public class WebDavCollection extends WebDavResource {
multistatus = serializer.read(DavMultistatus.class, is, false);
Log.d(TAG, "Received multistatus response: " + baos.toString("UTF-8"));
} catch (Exception e) {
Log.w(TAG, e);
throw new IncapableResourceException();
} catch (Exception ex) {
Log.w(TAG, "Invalid PROPFIND XML response", ex);
throw new InvalidDavResponseException();
}
processMultiStatus(multistatus);
return true;
@ -85,7 +85,7 @@ public class WebDavCollection extends WebDavResource {
return false;
}
public boolean multiGet(String[] names, MultigetType type) throws IOException, IncapableResourceException, HttpException {
public boolean multiGet(String[] names, MultigetType type) throws IOException, InvalidDavResponseException, HttpException {
DavMultiget multiget = (type == MultigetType.ADDRESS_BOOK) ? new DavAddressbookMultiget() : new DavCalendarMultiget();
multiget.prop = new DavProp();
@ -128,7 +128,7 @@ public class WebDavCollection extends WebDavResource {
processMultiStatus(multistatus);
} else
throw new IncapableResourceException();
throw new InvalidDavResponseException();
return true;
}

@ -64,7 +64,7 @@ public class WebDavResource {
protected DefaultHttpClient client;
public WebDavResource(URI baseURL, String username, String password, boolean preemptive) throws IOException {
public WebDavResource(URI baseURL, String username, String password, boolean preemptive) {
location = baseURL.normalize();
client = new DefaultHttpClient();

Loading…
Cancel
Save