/*
* Funambol is a mobile platform developed by Funambol, Inc.
* Copyright (C) 2008 Funambol, Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses or write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA.
*
* You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite
* 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by Funambol" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by Funambol".
*/
package de.chbosync.android.syncmlclient.source.pim.contact;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import android.accounts.Account;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.RawContacts;
import com.funambol.storage.StringKeyValuePair;
import com.funambol.storage.StringKeyValueStore;
import com.funambol.sync.ItemStatus;
import com.funambol.sync.SyncItem;
import com.funambol.sync.SyncSource;
import com.funambol.sync.client.CacheTracker;
import com.funambol.sync.client.TrackerException;
import com.funambol.util.Log;
import de.chbosync.android.syncmlclient.controller.AndroidController;
import de.chbosync.android.syncmlclient.source.AndroidChangesTracker;
/**
* <code>VersionCacheTracker</code> extends the <code>CacheTracker</code>
* implementation and overloads the changes retrieving and the fingerprint
* computing algorithms.
*
* The fingerprint used to retrieve changes is the contact version.
*
*/
public class VersionCacheTracker extends CacheTracker
implements AndroidChangesTracker {
private final String LOG_TAG = "VersionCacheTracker";
private ContentResolver resolver;
private Uri uri;
private ContactManager cm;
/**
* Creates a VersionCacheTracker. The constructor detects changes so that
* the method to get the changes can be used right away
*
* @param status is the key value store with stored data
* @param context the application Context
* @param uri is the uri of the table that this tracker tracks
*/
public VersionCacheTracker(StringKeyValueStore status, Context context,
ContactManager cm) {
this(status, context, RawContacts.CONTENT_URI, cm);
}
/**
* Creates a VersionCacheTracker. The constructor detects changes so that
* the method to get the changes can be used right away
*
* @param status is the key value store with stored data
* @param context the application Context
* @param uri the tracked table uri
*/
public VersionCacheTracker(StringKeyValueStore status, Context context,
Uri uri, ContactManager cm) {
super(status);
this.uri = uri;
this.cm = cm;
this.resolver = context.getContentResolver();
}
/**
* Implements the changes tracking logic. It retrieves changes based to the
* cache of the items version (the status).
*
* @param syncMode is the logic sync mode
* @param resume true if the sync is being resumed
*
* @throws TrackerException
*/
@Override
public void begin(int syncMode, boolean resume) throws TrackerException {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(LOG_TAG, "begin");
}
// Init account info
Account account = AndroidController.getNativeAccount();
String accountType = null;
String accountName = null;
if(account != null) {
accountType = account.type;
accountName = account.name;
}
this.syncMode = syncMode;
newItems = new Hashtable();
updatedItems = new Hashtable();
deletedItems = new Hashtable();
// Initialize the status
try {
this.status.load();
} catch (Exception ex) {
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(LOG_TAG, "Cannot load tracker status: " + ex.toString());
}
throw new TrackerException("Cannot load tracker status");
}
if(syncMode == SyncSource.INCREMENTAL_SYNC ||
syncMode == SyncSource.INCREMENTAL_UPLOAD ||
syncMode == SyncSource.INCREMENTAL_DOWNLOAD) {
computeIncrementalChanges(accountType, accountName);
} else if (syncMode == SyncSource.FULL_SYNC ||
syncMode == SyncSource.FULL_UPLOAD ||
syncMode == SyncSource.FULL_DOWNLOAD) {
// Reset the status when performing a slow sync
if (resume && syncMode != SyncSource.FULL_DOWNLOAD) {
// In this case we need to know if items that were sent in the
// previous sync attempt have changed
computeIncrementalChanges(accountType, accountName);
// We only need to keep the list of updated items
newItems = null;
deletedItems = null;
} else {
try {
status.reset();
} catch(IOException ex) {
Log.error(LOG_TAG, "Cannot reset status", ex);
throw new TrackerException("Cannot reset status");
}
}
}
}
@Override
public boolean hasChangedSinceLastSync(String key, long ts) {
if (updatedItems != null) {
return updatedItems.get(key) != null;
} else {
return false;
}
}
@Override
public boolean supportsResume() {
return true;
}
private void computeIncrementalChanges(String accountType, String accountName) {
// Initialize the items snapshot
String cols[] = {RawContacts._ID, RawContacts.VERSION, RawContacts.DELETED};
StringBuffer whereClause = new StringBuffer();
if(accountName != null && accountType != null) {
whereClause.append(RawContacts.ACCOUNT_NAME).append("='").append(accountName).append("'");
whereClause.append(" AND ");
whereClause.append(RawContacts.ACCOUNT_TYPE).append("='").append(accountType).append("'");
}
Cursor snapshot = resolver.query(uri, cols, whereClause.toString(), null, RawContacts._ID + " ASC");
try {
// Get the snapshot column indexes
int keyColumnIndex = snapshot.getColumnIndexOrThrow(RawContacts._ID);
int valueColumnIndex = snapshot.getColumnIndexOrThrow(RawContacts.VERSION);
int deletedColumnIndex = snapshot.getColumnIndexOrThrow(RawContacts.DELETED);
// Get the status key/value pairs
Enumeration statusKVPs = status.keyValuePairs();
snapshot.moveToFirst();
StringKeyValuePair statusKVP = null;
String statusKey = null;
String statusVersion = null;
String snapshotKey = null;
String snapshotVersion = null;
// Iterate on the status elements
while(statusKVPs.hasMoreElements()) {
// Get the status key/value
statusKVP = (StringKeyValuePair)statusKVPs.nextElement();
statusKey = statusKVP.getKey();
statusVersion = statusKVP.getValue();
boolean found = false;
// Look for the same element in the snapshot
while(!snapshot.isAfterLast() && !found) {
// Get the snapshot key/value
snapshotKey = snapshot.getString(keyColumnIndex);
snapshotVersion = snapshot.getString(valueColumnIndex);
int snapshotDeleted = snapshot.getInt(deletedColumnIndex);
if(snapshotKey.equals(statusKey)) {
found = true;
if(snapshotDeleted == 1) {
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(LOG_TAG, "Found a deleted item with key: " + statusKey);
}
deletedItems.put(statusKey, statusVersion);
} else if(!statusVersion.equals(snapshotVersion)) {
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(LOG_TAG, "Found an updated item with key: " + snapshotKey);
Log.debug(LOG_TAG, "statusVersion: " + statusVersion +
",snapshotVersion=" + snapshotVersion);
}
updatedItems.put(snapshotKey, snapshotVersion);
}
} else if(!(snapshotDeleted == 1)) {
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(LOG_TAG, "Found a new item with key: " + snapshotKey);
}
newItems.put(snapshotKey, snapshotVersion);
}
snapshot.moveToNext();
}
if(!found) {
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(LOG_TAG, "Found a deleted item with key: " + statusKey);
}
deletedItems.put(statusKey, statusVersion);
}
}
while(!snapshot.isAfterLast()) {
snapshotKey = snapshot.getString(keyColumnIndex);
snapshotVersion = snapshot.getString(valueColumnIndex);
int snapshotDeleted = snapshot.getInt(deletedColumnIndex);
if(!(snapshotDeleted == 1)) {
if (Log.isLoggable(Log.DEBUG)) {
Log.debug(LOG_TAG, "Found a new item with key: " + snapshotKey);
}
newItems.put(snapshotKey, snapshotVersion);
}
snapshot.moveToNext();
}
} finally {
if(snapshot != null) {
snapshot.close();
}
}
}
/**
* Computes the item fingerprint using the Andoid Contact Version. The
* Version attribute is incremented everytime a Contact is modified.
*
* @param item The SyncItem object.
* @return The item version.
*/
@Override
protected String computeFingerprint(SyncItem item) {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(LOG_TAG, "computeFingerprint");
}
String fp = "1";
String cols[] = {RawContacts.VERSION};
Cursor versionCursor = resolver.query(uri, cols,
RawContacts._ID + " = \"" + item.getKey() + "\"", null, null);
if(versionCursor.getCount() > 0) {
versionCursor.moveToFirst();
fp = versionCursor.getString(0);
}
versionCursor.close();
return fp;
}
public void setItemsStatus(Vector itemsStatus) throws TrackerException {
for(int i=0;i<itemsStatus.size();++i) {
ItemStatus itemStatus = (ItemStatus)itemsStatus.elementAt(i);
String key = itemStatus.getKey();
int status = itemStatus.getStatus();
setItemStatus(key, status);
}
}
@Override
public void setItemStatus(String key, int itemStatus) throws TrackerException {
if (Log.isLoggable(Log.TRACE)) {
Log.trace(LOG_TAG, "setItemStatus " + key + "," + itemStatus);
}
// First of all update the contacts store
long id = Long.parseLong(key);
if (isSuccess(itemStatus) &&
itemStatus != SyncSource.CHUNK_SUCCESS_STATUS && cm != null) {
if (deletedItems.contains(key)) {
cm.hardDelete(id);
} else {
cm.refreshSourceIdAndDirtyFlag(id);
}
}
// Update the tracker status
if(syncMode == SyncSource.FULL_SYNC ||
syncMode == SyncSource.FULL_UPLOAD) {
SyncItem item = new SyncItem(key);
if(status.get(key) != null) {
status.update(key, computeFingerprint(item));
} else {
status.add(key, computeFingerprint(item));
}
} else if (isSuccess(itemStatus) && itemStatus != SyncSource.CHUNK_SUCCESS_STATUS) {
// We must update the fingerprint store with the value of the
// fingerprint at the last sync
if (newItems.get(key) != null) {
// This is a new item
String itemFP = (String)newItems.get(key);
// Update the fingerprint
status.add(key, itemFP);
} else if (updatedItems.get(key) != null) {
// This is a new item
String itemFP = (String)updatedItems.get(key);
// Update the fingerprint
status.update(key, itemFP);
} else if (deletedItems.get(key) != null) {
// Update the fingerprint
status.remove(key);
}
}
}
protected Uri addCallerIsSyncAdapterFlag(Uri uri) {
Uri.Builder b = uri.buildUpon();
b.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true");
return b.build();
}
public boolean hasChanges() {
boolean result = false;
begin(SyncSource.INCREMENTAL_SYNC, false);
result |= getNewItemsCount() > 0;
result |= getUpdatedItemsCount() > 0;
result |= getDeletedItemsCount() > 0;
end();
return result;
}
}