1
0
mirror of https://github.com/etesync/android synced 2024-12-27 08:58:09 +00:00

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
This commit is contained in:
rfc2822 2013-10-20 17:40:03 +02:00
parent c87fb7bedd
commit 5db8bcb9d8
21 changed files with 420 additions and 170 deletions

View File

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

View File

@ -30,6 +30,6 @@
<string name="calendars">Calendars</string> <string name="calendars">Calendars</string>
<string name="select_address_book">Select up to one address book (tap again to unselect):</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="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> </resources>

View File

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

View File

@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?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> <EditSchema>
<DataKind <DataKind
kind="name" kind="name"
@ -7,9 +9,9 @@
supportsDisplayName="true" supportsDisplayName="true"
supportsMiddleName="true" supportsMiddleName="true"
supportsFamilyName="true" supportsFamilyName="true"
supportsPhoneticFamilyName="false" supportsPhoneticFamilyName="true"
supportsPhoneticGivenName="false" supportsPhoneticGivenName="true"
supportsPhoneticMiddleName="false" supportsPhoneticMiddleName="true"
supportsPrefix="true" supportsPrefix="true"
supportsSuffix="true" /> supportsSuffix="true" />
@ -40,7 +42,9 @@
maxOccurs="1" /> maxOccurs="1" />
<DataKind kind="website" /> <DataKind kind="website" />
<DataKind kind="note" /> <DataKind
kind="note"
maxOccurs="1" />
<DataKind <DataKind
dateWithTime="false" dateWithTime="false"
@ -51,4 +55,4 @@
yearOptional="false" /> yearOptional="false" />
</DataKind> </DataKind>
</EditSchema> </EditSchema>
</ContactsSource> </ContactsAccountType>

View File

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

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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); super(baseURL, user, password, preemptiveAuth);
} }
} }

View File

@ -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); super(baseURL, user, password, preemptiveAuth);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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);
}
}
}

View File

@ -40,7 +40,7 @@ public class SyncManager {
this.accountManager = accountManager; 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; boolean fetchCollection = false;
// PHASE 1: UPLOAD LOCALLY-CHANGED RESOURCES // PHASE 1: UPLOAD LOCALLY-CHANGED RESOURCES
@ -139,8 +139,8 @@ public class SyncManager {
Log.i(TAG, "Adding " + res.getName()); Log.i(TAG, "Adding " + res.getName());
try { try {
local.add(res); local.add(res);
} catch (ValidationException e) { } catch (ValidationException ex) {
Log.e(TAG, "Invalid resource: " + res.getName()); Log.w(TAG, "Ignoring invalid remote resource: " + res.getName(), ex);
} }
syncResult.stats.numInserts++; syncResult.stats.numInserts++;
} }
@ -151,8 +151,8 @@ public class SyncManager {
for (Resource res : dav.multiGet(resourcesToUpdate.toArray(new Resource[0]))) { for (Resource res : dav.multiGet(resourcesToUpdate.toArray(new Resource[0]))) {
try { try {
local.updateByRemoteName(res); local.updateByRemoteName(res);
} catch (ValidationException e) { } catch (ValidationException ex) {
Log.e(TAG, "Invalid resource: " + res.getName()); Log.e(TAG, "Ignoring invalid remote resource: " + res.getName(), ex);
} }
Log.i(TAG, "Updating " + res.getName()); Log.i(TAG, "Updating " + res.getName());
syncResult.stats.numInserts++; syncResult.stats.numInserts++;

View File

@ -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");
}
}

View File

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

View File

@ -64,7 +64,7 @@ public class WebDavResource {
protected DefaultHttpClient client; 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(); location = baseURL.normalize();
client = new DefaultHttpClient(); client = new DefaultHttpClient();