/*
* 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.Vector;
import android.accounts.Account;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import android.provider.ContactsContract.RawContacts;
import com.funambol.sync.ItemStatus;
import com.funambol.sync.SyncItem;
import com.funambol.sync.SyncSource;
import com.funambol.sync.client.ChangesTracker;
import com.funambol.sync.client.TrackableSyncSource;
import com.funambol.sync.client.TrackerException;
import com.funambol.util.Log;
import com.funambol.util.StringUtil;
import de.chbosync.android.syncmlclient.controller.AndroidController;
import de.chbosync.android.syncmlclient.source.AndroidChangesTracker;
/**
* DirtyChangesTracker is a ChangesTracker that makes use of the dirty flag in
* order to detect changes in the contact sync source.
*/
public class DirtyChangesTracker implements ChangesTracker, AndroidChangesTracker {
private final String TAG_LOG = "DirtyChangesTracker";
private ContentResolver resolver;
protected Vector<String> newItems;
protected Vector<String> deletedItems;
protected Vector<String> updatedItems;
protected int syncMode;
protected TrackableSyncSource ss;
protected ContactManager cm;
public DirtyChangesTracker(Context context, ContactManager cm) {
this.cm = cm;
this.resolver = context.getContentResolver();
}
public void begin(int syncMode, boolean resume) throws TrackerException {
if(Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "begin");
}
this.syncMode = syncMode;
this.newItems = new Vector<String>();
this.updatedItems = new Vector<String>();
this.deletedItems = new Vector<String>();
if(syncMode == SyncSource.INCREMENTAL_SYNC ||
syncMode == SyncSource.INCREMENTAL_UPLOAD ||
syncMode == SyncSource.INCREMENTAL_DOWNLOAD) {
computeIncrementalChanges();
} 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();
// We only need to keep the list of updated items
newItems = null;
deletedItems = null;
} else {
reset();
}
}
}
public void end() throws TrackerException {
if(Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "end");
}
// Allow the GC to pick this memory
newItems = null;
updatedItems = null;
deletedItems = null;
}
public boolean hasChangedSinceLastSync(String key, long ts) {
if (updatedItems != null) {
return updatedItems.contains(key);
} else {
return false;
}
}
public boolean supportsResume() {
return true;
}
private void computeIncrementalChanges() {
// Initialize the items snapshot
String cols[] = {RawContacts._ID, RawContacts.SOURCE_ID, RawContacts.DELETED, RawContacts.DIRTY};
StringBuffer whereClause = getAccountWhereClause();
// Look for dirty contacts only
whereClause.append(" AND ");
whereClause.append(RawContacts.DIRTY).append("=1");
Cursor snapshot = resolver.query(RawContacts.CONTENT_URI, cols,
whereClause.toString(), null, null);
try {
// Get the snapshot column indexes
int keyColumnIndex = 0;
int sourceIdColumnIndex = 1;
int deletedColumnIndex = 2;
snapshot.moveToFirst();
String snapshotKey = null;
String sourceId = null;
// Look for the same element in the snapshot
while(!snapshot.isAfterLast()) {
// Get the snapshot key/value
snapshotKey = snapshot.getString(keyColumnIndex);
sourceId = snapshot.getString(sourceIdColumnIndex);
int snapshotDeleted = snapshot.getInt(deletedColumnIndex);
if(snapshotDeleted == 1) {
if(Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Found a deleted item with key: " + snapshotKey);
}
deletedItems.addElement(snapshotKey);
} else if(StringUtil.isNullOrEmpty(sourceId)) {
if(Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Found a new item with key: " + snapshotKey);
}
newItems.addElement(snapshotKey);
} else {
if(Log.isLoggable(Log.DEBUG)) {
Log.debug(TAG_LOG, "Found an updated item with key: " + snapshotKey);
}
updatedItems.addElement(snapshotKey);
}
snapshot.moveToNext();
}
} finally {
if(snapshot != null) {
snapshot.close();
}
}
}
public Enumeration getNewItems() throws TrackerException {
if(Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "getNewItems");
}
// Any item in the sync source which is not part of the
// old state is a new item
if (newItems != null) {
return newItems.elements();
} else {
return null;
}
}
public int getNewItemsCount() throws TrackerException {
if (newItems != null) {
return newItems.size();
} else {
return 0;
}
}
public Enumeration getUpdatedItems() throws TrackerException {
if(Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "getUpdatedItems");
}
// Any item whose fingerprint has changed is a new item
if (updatedItems != null) {
return updatedItems.elements();
} else {
return null;
}
}
public int getUpdatedItemsCount() throws TrackerException {
if (updatedItems != null) {
return updatedItems.size();
} else {
return 0;
}
}
public Enumeration getDeletedItems() throws TrackerException {
if(Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "getDeletedItems");
}
// Any item in the sync source which is not part of the
// old state is a new item
if (deletedItems != null) {
return deletedItems.elements();
} else {
return null;
}
}
public int getDeletedItemsCount() throws TrackerException {
if (deletedItems != null) {
return deletedItems.size();
} else {
return 0;
}
}
public void setItemsStatus(Vector itemsStatus) throws TrackerException {
Vector filteredItemsStatus = new Vector();
for(int i=0;i<itemsStatus.size();++i) {
ItemStatus status = (ItemStatus)itemsStatus.elementAt(i);
String key = status.getKey();
long id = Long.parseLong(key);
int itemStatus = status.getStatus();
if (isSuccess(itemStatus) && itemStatus != SyncSource.CHUNK_SUCCESS_STATUS) {
if (deletedItems.contains(key)) {
cm.hardDelete(id);
} else {
filteredItemsStatus.addElement(status);
}
}
}
// Now apply all the changes in one shot
try {
cm.refreshSourceIdAndDirtyFlag(filteredItemsStatus);
} catch (IOException ioe) {
throw new TrackerException("Cannot set dirty flag");
}
}
public boolean filterItem(String key, boolean removed) {
return false;
}
protected Uri addCallerIsSyncAdapterFlag(Uri uri) {
Uri.Builder b = uri.buildUpon();
b.appendQueryParameter(ContactsContract.CALLER_IS_SYNCADAPTER, "true");
return b.build();
}
protected boolean isSuccess(int status) {
if(Log.isLoggable(Log.TRACE)) {
Log.trace(TAG_LOG, "isSuccess " + status);
}
return SyncSource.SUCCESS_STATUS == status;
}
public void empty() throws TrackerException {
// Nothing to do
}
public boolean removeItem(SyncItem item) throws TrackerException {
// Nothing to do
return true;
}
public void reset() throws TrackerException {
StringBuffer whereClause = getAccountWhereClause();
// Here we have to reset all the possible changes by resetting
// the dirty flag for all the items
Uri uri = addCallerIsSyncAdapterFlag(
ContactsContract.RawContacts.CONTENT_URI);
ContentValues cv = new ContentValues();
cv.put(ContactsContract.RawContacts.DIRTY, 0);
resolver.update(uri, cv, whereClause.toString(), null);
whereClause.append(" AND ");
whereClause.append(RawContacts.DELETED).append("=1");
// Then we have to hard delete all of the temp deleted items
resolver.delete(uri, whereClause.toString(), null);
}
public void setSyncSource(TrackableSyncSource ss) {
this.ss = ss;
}
public boolean hasChanges() {
boolean result = false;
StringBuffer whereClause = getAccountWhereClause();
whereClause.append(" AND ");
whereClause.append(RawContacts.DIRTY).append("=1");
Cursor items = resolver.query(RawContacts.CONTENT_URI,
new String[] {ContactsContract.RawContacts.DIRTY},
whereClause.toString(), null, null);
result = items.getCount() > 0;
items.close();
return result;
}
private StringBuffer getAccountWhereClause() {
StringBuffer whereClause = new StringBuffer();
Account account = AndroidController.getNativeAccount();
if(account != null) {
String accountType = account.type;
String accountName = account.name;
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("'");
}
} else {
whereClause.append("(0)");
}
return whereClause;
}
}