Loading app/src/main/java/at/bitfire/davdroid/HttpClient.java +2 −2 Original line number Diff line number Diff line Loading @@ -40,7 +40,7 @@ public class HttpClient extends OkHttpClient { super(); initialize(); // authentication // authentication and User-Agent if (preemptive) networkInterceptors().add(new PreemptiveAuthenticationInterceptor(username, password)); else Loading Loading @@ -68,7 +68,7 @@ public class HttpClient extends OkHttpClient { } }); logging.setLevel(HttpLoggingInterceptor.Level.BODY); networkInterceptors().add(logging); interceptors().add(logging); } Loading app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java +25 −3 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ import android.accounts.Account; import android.content.ContentProviderClient; import android.provider.ContactsContract; import at.bitfire.davdroid.Constants; import at.bitfire.vcard4android.AndroidAddressBook; import at.bitfire.vcard4android.AndroidContact; import at.bitfire.vcard4android.AndroidContactFactory; Loading @@ -25,9 +26,30 @@ public class LocalAddressBook extends AndroidAddressBook { super(account, provider, AndroidGroupFactory.INSTANCE, LocalContact.Factory.INSTANCE); } /*LocalContact[] queryAll() throws ContactsStorageException { LocalContact contacts[] = (LocalContact[])queryContacts(ContactsContract.RawContacts.DELETED + "=0", null); public LocalContact[] getAll() throws ContactsStorageException { LocalContact contacts[] = (LocalContact[])queryContacts(null, null); return contacts; }*/ } /** * Returns an array of local contacts which have been deleted locally. (DELETED != 0). */ public LocalContact[] getDeleted() throws ContactsStorageException { return (LocalContact[])queryContacts(ContactsContract.RawContacts.DELETED + " != 0", null); } /** * Returns an array of local contacts which have been changed locally (DIRTY != 0). */ public LocalContact[] getDirty() throws ContactsStorageException { return (LocalContact[])queryContacts(ContactsContract.RawContacts.DIRTY + " != 0", null); } /** * Returns an array of local contacts which don't have a file name yet. */ public LocalContact[] getWithoutFileName() throws ContactsStorageException { return (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null); } } app/src/main/java/at/bitfire/davdroid/resource/LocalContact.java +23 −8 Original line number Diff line number Diff line Loading @@ -8,19 +8,34 @@ package at.bitfire.davdroid.resource; import android.content.ContentValues; import android.os.RemoteException; import android.provider.ContactsContract; import at.bitfire.vcard4android.AndroidAddressBook; import at.bitfire.vcard4android.AndroidContact; import at.bitfire.vcard4android.AndroidContactFactory; import at.bitfire.vcard4android.Contact; import at.bitfire.vcard4android.ContactsStorageException; public class LocalContact extends AndroidContact { protected LocalContact(AndroidAddressBook addressBook, long id) { super(addressBook, id); protected LocalContact(AndroidAddressBook addressBook, long id, String fileName, String eTag) { super(addressBook, id, fileName, eTag); } public LocalContact(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) { super(addressBook, contact, fileName, eTag); } public LocalContact(AndroidAddressBook addressBook, Contact contact) { super(addressBook, contact); public void updateUID(String uid) throws ContactsStorageException { try { ContentValues values = new ContentValues(1); values.put(COLUMN_UID, uid); addressBook.provider.update(rawContactSyncURI(), values, null, null); } catch (RemoteException e) { throw new ContactsStorageException("Couldn't update UID", e); } } Loading @@ -28,13 +43,13 @@ public class LocalContact extends AndroidContact { static final Factory INSTANCE = new Factory(); @Override public LocalContact newInstance(AndroidAddressBook addressBook, long id) { return new LocalContact(addressBook, id); public LocalContact newInstance(AndroidAddressBook addressBook, long id, String fileName, String eTag) { return new LocalContact(addressBook, id, fileName, eTag); } @Override public LocalContact newInstance(AndroidAddressBook addressBook, Contact contact) { return new LocalContact(addressBook, contact); public LocalContact newInstance(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) { return new LocalContact(addressBook, contact, fileName, eTag); } public LocalContact[] newArray(int size) { Loading app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java +148 −14 Original line number Diff line number Diff line Loading @@ -16,22 +16,36 @@ import android.content.Intent; import android.content.SyncResult; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; import com.squareup.okhttp.HttpUrl; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.ResponseBody; import org.apache.commons.io.Charsets; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import at.bitfire.dav4android.DavAddressBook; import at.bitfire.dav4android.DavResource; import at.bitfire.dav4android.exception.DavException; import at.bitfire.dav4android.exception.HttpException; import at.bitfire.dav4android.property.GetETag; 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.LocalContact; import at.bitfire.vcard4android.Contact; import ezvcard.VCardVersion; import ezvcard.property.Uid; public class ContactsSyncAdapterService extends Service { private static ContactsSyncAdapter syncAdapter; Loading Loading @@ -66,8 +80,13 @@ public class ContactsSyncAdapterService extends Service { 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())); HttpUrl addressBookURL = HttpUrl.parse(settings.getAddressBookURL()); DavAddressBook dav = new DavAddressBook(httpClient, addressBookURL); try { // prepare local address book LocalAddressBook addressBook = new LocalAddressBook(account, provider); // prepare remote address book boolean hasVCard4 = false; dav.propfind(0, SupportedAddressData.NAME); SupportedAddressData supportedAddressData = (SupportedAddressData)dav.properties.get(SupportedAddressData.NAME); Loading @@ -77,22 +96,137 @@ public class ContactsSyncAdapterService extends Service { hasVCard4 = true; Constants.log.info("Server advertises VCard/4 support: " + hasVCard4); LocalAddressBook addressBook = new LocalAddressBook(account, provider); // Remove locally deleted contacts from server (if they have a name, i.e. if they were uploaded before), // but only if they don't have changed on the server. Then finally remove them from the local address book. LocalContact[] localList = addressBook.getDeleted(); for (LocalContact local : localList) { final String fileName = local.getFileName(); if (!TextUtils.isEmpty(fileName)) { Constants.log.info(fileName + " has been deleted locally -> deleting from server"); try { new DavResource(httpClient, addressBookURL.newBuilder().addPathSegment(fileName).build()) .delete(local.eTag); } catch(IOException|HttpException e) { Constants.log.warn("Couldn't delete " + fileName + " from server"); } } else Constants.log.info("Removing local contact #" + local.getId() + " which has been deleted locally and was never uploaded"); local.delete(); } // assign file names and UIDs to new contacts so that we can use the file name as an index localList = addressBook.getWithoutFileName(); for (LocalContact local : localList) { String uuid = Uid.random().toString(); Constants.log.info("Found local contact #" + local.getId() + " without file name; assigning name UID/name " + uuid + "[.vcf]"); local.updateUID(uuid); } // upload dirty contacts localList = addressBook.getDirty(); for (LocalContact local : localList) { final String fileName = local.getFileName(); DavResource remote = new DavResource(httpClient, addressBookURL.newBuilder().addPathSegment(fileName).build()); RequestBody vCard = RequestBody.create( hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8, local.getContact().toStream(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0).toByteArray() ); if (local.eTag == null) { Constants.log.info("Uploading new contact " + fileName); remote.put(vCard, null, local.eTag); } else { Constants.log.info("Uploading locally modified contact " + fileName); remote.put(vCard, local.eTag, null); } // reset DIRTY } // check CTag (ignore on forced sync) if (true) { // fetch list of local contacts and build hash table to index file name localList = addressBook.getAll(); Map<String, LocalContact> localContacts = new HashMap<>(localList.length); for (LocalContact contact : localList) { Constants.log.debug("Found local contact: " + contact.getFileName()); localContacts.put(contact.getFileName(), contact); } // fetch list of remote VCards and build hash table to index file name Constants.log.info("Listing remote VCards"); dav.queryMemberETags(); Map<String, DavResource> remoteContacts = new HashMap<>(dav.members.size()); 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"); String fileName = vCard.fileName(); Constants.log.debug("Found remote VCard: " + fileName); remoteContacts.put(fileName, vCard); } /* check which contacts 1. are not present anymore remotely -> delete immediately on local side 2. updated remotely -> add to downloadNames 3. added remotely -> add to downloadNames */ Set<DavResource> toDownload = new HashSet<>(); for (String localName : localContacts.keySet()) { DavResource remote = remoteContacts.get(localName); if (remote == null) { Constants.log.info(localName + " is not on server anymore, deleting"); localContacts.get(localName).delete(); } else { // contact is still on server, check whether it has been updated remotely GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME); if (getETag == null || getETag.eTag == null) throw new DavException("Server didn't provide ETag"); String localETag = localContacts.get(localName).eTag, remoteETag = getETag.eTag; if (!remoteETag.equals(localETag)) { Constants.log.info(localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")"); toDownload.add(remote); } // remote entry has been seen, remove from list remoteContacts.remove(localName); } } // add all unseen (= remotely added) remote contacts if (!remoteContacts.isEmpty()) { Constants.log.info("New VCards have been found on the server: " + TextUtils.join(", ", remoteContacts.keySet())); toDownload.addAll(remoteContacts.values()); } // download new/updated VCards from server for (DavResource remoteContact : toDownload) { Constants.log.info("Downloading " + remoteContact.location); String fileName = remoteContact.fileName(); ResponseBody body = remoteContact.get("text/vcard;q=0.5, text/vcard;charset=utf-8;q=0.8, text/vcard;version=4.0"); String remoteETag = ((GetETag)remoteContact.properties.get(GetETag.NAME)).eTag; 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); Contact newData = contacts[0]; // delete local contact, if it exists LocalContact localContact = localContacts.get(fileName); if (localContact != null) { Constants.log.info("Updating " + fileName + " in local address book"); localContact.eTag = remoteETag; localContact.update(newData); } else { Constants.log.info("Adding " + fileName + " to local address book"); localContact = new LocalContact(addressBook, newData, fileName, remoteETag); localContact.add(); } // add the new contact } else Constants.log.error("Received VCard with not exactly one VCARD"); Constants.log.error("Received VCard with not exactly one VCARD, ignoring " + fileName); } } } catch (Exception e) { Loading dav4android @ 84a2cf0b Compare 57e1f34c to 84a2cf0b Original line number Diff line number Diff line Subproject commit 57e1f34c45f070ca9b269f2bea109f6b1cdcb385 Subproject commit 84a2cf0bbad257274e362851020da3822957449d Loading
app/src/main/java/at/bitfire/davdroid/HttpClient.java +2 −2 Original line number Diff line number Diff line Loading @@ -40,7 +40,7 @@ public class HttpClient extends OkHttpClient { super(); initialize(); // authentication // authentication and User-Agent if (preemptive) networkInterceptors().add(new PreemptiveAuthenticationInterceptor(username, password)); else Loading Loading @@ -68,7 +68,7 @@ public class HttpClient extends OkHttpClient { } }); logging.setLevel(HttpLoggingInterceptor.Level.BODY); networkInterceptors().add(logging); interceptors().add(logging); } Loading
app/src/main/java/at/bitfire/davdroid/resource/LocalAddressBook.java +25 −3 Original line number Diff line number Diff line Loading @@ -11,6 +11,7 @@ import android.accounts.Account; import android.content.ContentProviderClient; import android.provider.ContactsContract; import at.bitfire.davdroid.Constants; import at.bitfire.vcard4android.AndroidAddressBook; import at.bitfire.vcard4android.AndroidContact; import at.bitfire.vcard4android.AndroidContactFactory; Loading @@ -25,9 +26,30 @@ public class LocalAddressBook extends AndroidAddressBook { super(account, provider, AndroidGroupFactory.INSTANCE, LocalContact.Factory.INSTANCE); } /*LocalContact[] queryAll() throws ContactsStorageException { LocalContact contacts[] = (LocalContact[])queryContacts(ContactsContract.RawContacts.DELETED + "=0", null); public LocalContact[] getAll() throws ContactsStorageException { LocalContact contacts[] = (LocalContact[])queryContacts(null, null); return contacts; }*/ } /** * Returns an array of local contacts which have been deleted locally. (DELETED != 0). */ public LocalContact[] getDeleted() throws ContactsStorageException { return (LocalContact[])queryContacts(ContactsContract.RawContacts.DELETED + " != 0", null); } /** * Returns an array of local contacts which have been changed locally (DIRTY != 0). */ public LocalContact[] getDirty() throws ContactsStorageException { return (LocalContact[])queryContacts(ContactsContract.RawContacts.DIRTY + " != 0", null); } /** * Returns an array of local contacts which don't have a file name yet. */ public LocalContact[] getWithoutFileName() throws ContactsStorageException { return (LocalContact[])queryContacts(AndroidContact.COLUMN_FILENAME + " IS NULL", null); } }
app/src/main/java/at/bitfire/davdroid/resource/LocalContact.java +23 −8 Original line number Diff line number Diff line Loading @@ -8,19 +8,34 @@ package at.bitfire.davdroid.resource; import android.content.ContentValues; import android.os.RemoteException; import android.provider.ContactsContract; import at.bitfire.vcard4android.AndroidAddressBook; import at.bitfire.vcard4android.AndroidContact; import at.bitfire.vcard4android.AndroidContactFactory; import at.bitfire.vcard4android.Contact; import at.bitfire.vcard4android.ContactsStorageException; public class LocalContact extends AndroidContact { protected LocalContact(AndroidAddressBook addressBook, long id) { super(addressBook, id); protected LocalContact(AndroidAddressBook addressBook, long id, String fileName, String eTag) { super(addressBook, id, fileName, eTag); } public LocalContact(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) { super(addressBook, contact, fileName, eTag); } public LocalContact(AndroidAddressBook addressBook, Contact contact) { super(addressBook, contact); public void updateUID(String uid) throws ContactsStorageException { try { ContentValues values = new ContentValues(1); values.put(COLUMN_UID, uid); addressBook.provider.update(rawContactSyncURI(), values, null, null); } catch (RemoteException e) { throw new ContactsStorageException("Couldn't update UID", e); } } Loading @@ -28,13 +43,13 @@ public class LocalContact extends AndroidContact { static final Factory INSTANCE = new Factory(); @Override public LocalContact newInstance(AndroidAddressBook addressBook, long id) { return new LocalContact(addressBook, id); public LocalContact newInstance(AndroidAddressBook addressBook, long id, String fileName, String eTag) { return new LocalContact(addressBook, id, fileName, eTag); } @Override public LocalContact newInstance(AndroidAddressBook addressBook, Contact contact) { return new LocalContact(addressBook, contact); public LocalContact newInstance(AndroidAddressBook addressBook, Contact contact, String fileName, String eTag) { return new LocalContact(addressBook, contact, fileName, eTag); } public LocalContact[] newArray(int size) { Loading
app/src/main/java/at/bitfire/davdroid/syncadapter/ContactsSyncAdapterService.java +148 −14 Original line number Diff line number Diff line Loading @@ -16,22 +16,36 @@ import android.content.Intent; import android.content.SyncResult; import android.os.Bundle; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; import com.squareup.okhttp.HttpUrl; import com.squareup.okhttp.MediaType; import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.ResponseBody; import org.apache.commons.io.Charsets; import java.io.IOException; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.UUID; import at.bitfire.dav4android.DavAddressBook; import at.bitfire.dav4android.DavResource; import at.bitfire.dav4android.exception.DavException; import at.bitfire.dav4android.exception.HttpException; import at.bitfire.dav4android.property.GetETag; 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.LocalContact; import at.bitfire.vcard4android.Contact; import ezvcard.VCardVersion; import ezvcard.property.Uid; public class ContactsSyncAdapterService extends Service { private static ContactsSyncAdapter syncAdapter; Loading Loading @@ -66,8 +80,13 @@ public class ContactsSyncAdapterService extends Service { 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())); HttpUrl addressBookURL = HttpUrl.parse(settings.getAddressBookURL()); DavAddressBook dav = new DavAddressBook(httpClient, addressBookURL); try { // prepare local address book LocalAddressBook addressBook = new LocalAddressBook(account, provider); // prepare remote address book boolean hasVCard4 = false; dav.propfind(0, SupportedAddressData.NAME); SupportedAddressData supportedAddressData = (SupportedAddressData)dav.properties.get(SupportedAddressData.NAME); Loading @@ -77,22 +96,137 @@ public class ContactsSyncAdapterService extends Service { hasVCard4 = true; Constants.log.info("Server advertises VCard/4 support: " + hasVCard4); LocalAddressBook addressBook = new LocalAddressBook(account, provider); // Remove locally deleted contacts from server (if they have a name, i.e. if they were uploaded before), // but only if they don't have changed on the server. Then finally remove them from the local address book. LocalContact[] localList = addressBook.getDeleted(); for (LocalContact local : localList) { final String fileName = local.getFileName(); if (!TextUtils.isEmpty(fileName)) { Constants.log.info(fileName + " has been deleted locally -> deleting from server"); try { new DavResource(httpClient, addressBookURL.newBuilder().addPathSegment(fileName).build()) .delete(local.eTag); } catch(IOException|HttpException e) { Constants.log.warn("Couldn't delete " + fileName + " from server"); } } else Constants.log.info("Removing local contact #" + local.getId() + " which has been deleted locally and was never uploaded"); local.delete(); } // assign file names and UIDs to new contacts so that we can use the file name as an index localList = addressBook.getWithoutFileName(); for (LocalContact local : localList) { String uuid = Uid.random().toString(); Constants.log.info("Found local contact #" + local.getId() + " without file name; assigning name UID/name " + uuid + "[.vcf]"); local.updateUID(uuid); } // upload dirty contacts localList = addressBook.getDirty(); for (LocalContact local : localList) { final String fileName = local.getFileName(); DavResource remote = new DavResource(httpClient, addressBookURL.newBuilder().addPathSegment(fileName).build()); RequestBody vCard = RequestBody.create( hasVCard4 ? DavAddressBook.MIME_VCARD4 : DavAddressBook.MIME_VCARD3_UTF8, local.getContact().toStream(hasVCard4 ? VCardVersion.V4_0 : VCardVersion.V3_0).toByteArray() ); if (local.eTag == null) { Constants.log.info("Uploading new contact " + fileName); remote.put(vCard, null, local.eTag); } else { Constants.log.info("Uploading locally modified contact " + fileName); remote.put(vCard, local.eTag, null); } // reset DIRTY } // check CTag (ignore on forced sync) if (true) { // fetch list of local contacts and build hash table to index file name localList = addressBook.getAll(); Map<String, LocalContact> localContacts = new HashMap<>(localList.length); for (LocalContact contact : localList) { Constants.log.debug("Found local contact: " + contact.getFileName()); localContacts.put(contact.getFileName(), contact); } // fetch list of remote VCards and build hash table to index file name Constants.log.info("Listing remote VCards"); dav.queryMemberETags(); Map<String, DavResource> remoteContacts = new HashMap<>(dav.members.size()); 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"); String fileName = vCard.fileName(); Constants.log.debug("Found remote VCard: " + fileName); remoteContacts.put(fileName, vCard); } /* check which contacts 1. are not present anymore remotely -> delete immediately on local side 2. updated remotely -> add to downloadNames 3. added remotely -> add to downloadNames */ Set<DavResource> toDownload = new HashSet<>(); for (String localName : localContacts.keySet()) { DavResource remote = remoteContacts.get(localName); if (remote == null) { Constants.log.info(localName + " is not on server anymore, deleting"); localContacts.get(localName).delete(); } else { // contact is still on server, check whether it has been updated remotely GetETag getETag = (GetETag)remote.properties.get(GetETag.NAME); if (getETag == null || getETag.eTag == null) throw new DavException("Server didn't provide ETag"); String localETag = localContacts.get(localName).eTag, remoteETag = getETag.eTag; if (!remoteETag.equals(localETag)) { Constants.log.info(localName + " has been changed on server (current ETag=" + remoteETag + ", last known ETag=" + localETag + ")"); toDownload.add(remote); } // remote entry has been seen, remove from list remoteContacts.remove(localName); } } // add all unseen (= remotely added) remote contacts if (!remoteContacts.isEmpty()) { Constants.log.info("New VCards have been found on the server: " + TextUtils.join(", ", remoteContacts.keySet())); toDownload.addAll(remoteContacts.values()); } // download new/updated VCards from server for (DavResource remoteContact : toDownload) { Constants.log.info("Downloading " + remoteContact.location); String fileName = remoteContact.fileName(); ResponseBody body = remoteContact.get("text/vcard;q=0.5, text/vcard;charset=utf-8;q=0.8, text/vcard;version=4.0"); String remoteETag = ((GetETag)remoteContact.properties.get(GetETag.NAME)).eTag; 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); Contact newData = contacts[0]; // delete local contact, if it exists LocalContact localContact = localContacts.get(fileName); if (localContact != null) { Constants.log.info("Updating " + fileName + " in local address book"); localContact.eTag = remoteETag; localContact.update(newData); } else { Constants.log.info("Adding " + fileName + " to local address book"); localContact = new LocalContact(addressBook, newData, fileName, remoteETag); localContact.add(); } // add the new contact } else Constants.log.error("Received VCard with not exactly one VCARD"); Constants.log.error("Received VCard with not exactly one VCARD, ignoring " + fileName); } } } catch (Exception e) { Loading
dav4android @ 84a2cf0b Compare 57e1f34c to 84a2cf0b Original line number Diff line number Diff line Subproject commit 57e1f34c45f070ca9b269f2bea109f6b1cdcb385 Subproject commit 84a2cf0bbad257274e362851020da3822957449d