Commit faaaa330 authored by Ricki Hirner's avatar Ricki Hirner

Implement checksum to check whether DIRTY contacts have "really" changed

* contact data hash code = hash code of data fields and group memberships
* Before every contact sync, all dirty contacts are checked whether they're
  "really dirty" (= data hash code has changed). If they're not, the DIRTY
  flag is reset. Works around Android 7 behavior of setting contacts to DIRTY
  even if onky meta data has been updated (for instance, lastContacted after
  a call or SMS),
* When an "upload" sync is initiated by notifyChange and there are no
  "really dirty" contacts, the sync is ignored.
* contact upload: clearDirty() saves hash code, too
* contact download: create()/update() saves hash code, too
* debugging: sync flags (extras) are now logged
parent 712467d9
Pipeline #6230951 passed with stage
in 16 minutes and 20 seconds
......@@ -84,6 +84,34 @@ public class LocalAddressBook extends AndroidAddressBook implements LocalCollect
return deleted.toArray(new LocalResource[deleted.size()]);
}
/**
* Queries all contacts with DIRTY flag and checks whether their data checksum has changed, i.e.
* if they're "really dirty" (= data has changed, not only metadata, which is not hashed).
* The DIRTY flag is removed from contacts which are not "really dirty", i.e. from contacts
* whose contact data checksum has not changed.
* @return number of "really dirty" contacts
*/
public int verifyDirty() throws ContactsStorageException {
int reallyDirty = 0;
for (LocalContact contact : getDirtyContacts()) {
try {
if (contact.getLastHashCode() == contact.dataHashCode()) {
// hash is code still the same, contact is not "really dirty" (only metadata been have changed)
App.log.log(Level.FINE, "Contact data hash has not changed, resetting dirty flag", contact);
contact.resetDirty();
} else
reallyDirty++;
} catch(FileNotFoundException e) {
throw new ContactsStorageException("Couldn't calculate hash code", e);
}
}
if (includeGroups)
reallyDirty += getDirtyGroups().length;
return reallyDirty;
}
/**
* Returns an array of local contacts/groups which have been changed locally (DIRTY != 0).
*/
......
......@@ -10,6 +10,8 @@ package at.bitfire.davdroid.resource;
import android.content.ContentProviderOperation;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
......@@ -20,6 +22,7 @@ import java.io.FileNotFoundException;
import java.util.HashSet;
import java.util.Set;
import at.bitfire.davdroid.App;
import at.bitfire.davdroid.BuildConfig;
import at.bitfire.davdroid.model.UnknownProperties;
import at.bitfire.vcard4android.AndroidAddressBook;
......@@ -30,11 +33,13 @@ import at.bitfire.vcard4android.CachedGroupMembership;
import at.bitfire.vcard4android.Contact;
import at.bitfire.vcard4android.ContactsStorageException;
import ezvcard.Ezvcard;
import lombok.Cleanup;
public class LocalContact extends AndroidContact implements LocalResource {
static {
Contact.productID = "+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " vcard4android ez-vcard/" + Ezvcard.VERSION;
}
public static final String COLUMN_HASHCODE = ContactsContract.RawContacts.SYNC3;
protected final Set<Long>
cachedGroupMemberships = new HashSet<>(),
......@@ -49,15 +54,30 @@ public class LocalContact extends AndroidContact implements LocalResource {
super(addressBook, contact, fileName, eTag);
}
public void resetDirty() throws ContactsStorageException {
ContentValues values = new ContentValues(1);
values.put(ContactsContract.RawContacts.DIRTY, 0);
try {
addressBook.provider.update(rawContactSyncURI(), values, null, null);
} catch(RemoteException e) {
throw new ContactsStorageException("Couldn't clear dirty flag", e);
}
}
public void clearDirty(String eTag) throws ContactsStorageException {
try {
ContentValues values = new ContentValues(1);
values.put(ContactsContract.RawContacts.DIRTY, 0);
ContentValues values = new ContentValues(3);
values.put(COLUMN_ETAG, eTag);
values.put(ContactsContract.RawContacts.DIRTY, 0);
int hashCode = dataHashCode();
values.put(COLUMN_HASHCODE, hashCode);
App.log.finer("Clearing dirty flag with eTag = " + eTag + ", contact hash = " + hashCode);
addressBook.provider.update(rawContactSyncURI(), values, null, null);
this.eTag = eTag;
} catch (RemoteException e) {
} catch (FileNotFoundException|RemoteException e) {
throw new ContactsStorageException("Couldn't clear dirty flag", e);
}
}
......@@ -114,6 +134,53 @@ public class LocalContact extends AndroidContact implements LocalResource {
}
@Override
public int update(Contact contact) throws ContactsStorageException {
int result = super.update(contact);
updateHashCode();
return result;
}
@Override
public Uri create() throws ContactsStorageException {
Uri uri = super.create();
updateHashCode();
return uri;
}
/**
* Calculates a hash code from the contact's data (VCard) and group memberships.
* @return hash code of contact data (including group memberships)
*/
public int dataHashCode() throws FileNotFoundException, ContactsStorageException {
// groupMemberships is filled by getContact()
return getContact().hashCode() ^ groupMemberships.hashCode();
}
protected void updateHashCode() throws ContactsStorageException {
ContentValues values = new ContentValues(1);
try {
int hashCode = dataHashCode();
App.log.fine("Storing contact hash = " + hashCode);
values.put(COLUMN_HASHCODE, hashCode);
addressBook.provider.update(rawContactSyncURI(), values, null, null);
} catch(FileNotFoundException|RemoteException e) {
throw new ContactsStorageException("Couldn't store contact checksum", e);
}
}
int getLastHashCode() throws ContactsStorageException {
try {
@Cleanup Cursor c = addressBook.provider.query(rawContactSyncURI(), new String[] { COLUMN_HASHCODE }, null, null, null);
if (c == null || !c.moveToNext() || c.isNull(0))
return 0;
return c.getInt(0);
} catch(RemoteException e) {
throw new ContactsStorageException("Could't read last hash code", e);
}
}
public void addToGroup(BatchOperation batch, long groupID) {
assertID();
batch.enqueue(new BatchOperation.Operation(
......
......@@ -147,6 +147,12 @@ public class ContactsSyncManager extends SyncManager {
localCollection = new LocalAddressBook(account, provider);
LocalAddressBook localAddressBook = localAddressBook();
int reallyDirty = localAddressBook.verifyDirty();
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0) {
App.log.info("This sync was called to upload dirty contacts, but no contact data have been changed");
return false;
}
String url = remote.url;
String lastUrl = localAddressBook.getURL();
if (!url.equals(lastUrl)) {
......
......@@ -57,7 +57,7 @@ public abstract class SyncAdapterService extends Service {
@Override
public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {
App.log.info("Sync for " + authority + " has been initiated");
App.log.log(Level.INFO, "Sync for " + authority + " has been initiated.", extras.keySet().toArray());
// required for dav4android (ServiceLoader)
Thread.currentThread().setContextClassLoader(getContext().getClassLoader());
......
......@@ -132,7 +132,6 @@ public class AccountSettingsActivity extends AppCompatActivity {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setSyncInterval(ContactsContract.AUTHORITY, Long.parseLong((String)newValue));
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
......@@ -153,7 +152,6 @@ public class AccountSettingsActivity extends AppCompatActivity {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setSyncInterval(CalendarContract.AUTHORITY, Long.parseLong((String)newValue));
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
......@@ -174,7 +172,6 @@ public class AccountSettingsActivity extends AppCompatActivity {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
settings.setSyncInterval(TaskProvider.ProviderName.OpenTasks.authority, Long.parseLong((String)newValue));
getLoaderManager().restartLoader(0, getArguments(), AccountSettingsFragment.this);
return false;
}
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment