/* * Funambol is a mobile platform developed by Funambol, Inc. * Copyright (C) 2010 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.calendar; import java.io.IOException; import java.util.Enumeration; import java.util.Hashtable; import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.provider.CalendarContract; import com.funambol.storage.StringKeyValuePair; import com.funambol.storage.StringKeyValueStore; 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.source.AndroidChangesTracker; /** * This interface can be used by TrackableSyncSource to detect changes occourred * since the last synchronization. The API provides a basic implementation * in CacheTracker which detects changes comparing fingerprints. * Client can implement this interface and use it in the TrackableSyncSource if more * efficient methods are available. */ public class CalendarChangesTracker extends CacheTracker implements AndroidChangesTracker { private static final String TAG_LOG = "CalendarChangesTracker"; protected ContentResolver resolver; protected CalendarAppSyncSourceConfig calendarAppSyncSourceConfig; private String accountName; private String accountType; public CalendarChangesTracker(Context context, StringKeyValueStore status, CalendarAppSyncSourceConfig calendarAppSyncSourceConfig) { super(status); this.resolver = context.getContentResolver(); this.calendarAppSyncSourceConfig = calendarAppSyncSourceConfig; } @Override public void begin(int syncMode, boolean resume) throws TrackerException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "beginning changes computation"); } long calendarId = calendarAppSyncSourceConfig.getCalendarId(); if (calendarId == -1) { throw new TrackerException("Cannot track undefined calendar"); } 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(TAG_LOG, "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) { // Initialize the items snapshot String cols[] = {CalendarContract.Events._ID, CalendarContract.Events.DIRTY}; // Grab only the rows which are sync dirty and belong to the // calendar being synchronized StringBuffer whereClause = new StringBuffer(); whereClause.append(CalendarContract.Events.CALENDAR_ID).append("='").append(calendarId).append("'"); // Get all the items belonging to the calendar being // synchronized in ascending order Cursor snapshot = resolver.query(CalendarContract.Events.CONTENT_URI, cols, whereClause.toString(), null, CalendarContract.Events._ID + " ASC"); // Get the snapshot column indexes int keyColumnIndex = snapshot.getColumnIndexOrThrow(CalendarContract.Events._ID); Enumeration statusPairs = status.keyValuePairs(); // We have two ordered sets to compare try { boolean snapshotDone = !snapshot.moveToFirst(); boolean statusDone = !statusPairs.hasMoreElements(); String statusIdStr = null; long statusId = -1; long snapshotId = -1; StringKeyValuePair kvp = null; do { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "snapshotDone = " + snapshotDone); Log.trace(TAG_LOG, "statusDone = " + statusDone); } String snapshotIdStr = null; if (!snapshotDone) { // Get the item id in the snapshot snapshotIdStr = snapshot.getString(keyColumnIndex); snapshotId = Long.parseLong(snapshotIdStr); } else { snapshotId = -1; } statusDone = !statusPairs.hasMoreElements(); if (statusIdStr == null && !statusDone) { kvp = (StringKeyValuePair)statusPairs.nextElement(); statusIdStr = kvp.getKey(); statusId = Long.parseLong(statusIdStr); } if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "snapshotId = " + snapshotId); Log.trace(TAG_LOG, "statusId = " + statusId); } if (!statusDone || !snapshotDone) { if (snapshotId == statusId) { // Check if the item is updated. Note that on // Android LUIDs can be reused, therefore it is // possible that if the user removes the last item // and add a new one, we detect a replace instead of // a pair delete/add if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Same id: " + statusId); } if (isDirty(snapshot, kvp)) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Found updated item: " + snapshotId); } updatedItems.put(snapshotIdStr, computeFingerprint(snapshotIdStr, snapshot)); } // Advance both pointers snapshotDone = !snapshot.moveToNext(); statusIdStr = null; } else if ((snapshotId < statusId && snapshotId != -1) || statusDone) { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Found new item: " + snapshotId); } // This item was added newItems.put(snapshotIdStr, computeFingerprint(snapshotIdStr, snapshot)); // Move only the snapshot pointer snapshotDone = !snapshot.moveToNext(); } else { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Found deleted item: " + statusId); } // The item was deleted deletedItems.put(statusIdStr, "1"); // Move only the status pointer statusIdStr = null; } } } while(!statusDone || !snapshotDone); } catch (Exception e) { Log.error(TAG_LOG, "Cannot compute changes", e); throw new TrackerException(e.toString()); } finally { snapshot.close(); } } else if(syncMode == SyncSource.FULL_SYNC || syncMode == SyncSource.FULL_UPLOAD || syncMode == SyncSource.FULL_DOWNLOAD) { // Reset the status when performing a slow sync try { status.reset(); } catch(IOException ex) { Log.error(TAG_LOG, "Cannot reset status", ex); throw new TrackerException("Cannot reset status"); } } } @Override public void setItemStatus(String key, int itemStatus) throws TrackerException { if(syncMode == SyncSource.FULL_SYNC || syncMode == SyncSource.FULL_UPLOAD) { if(status.get(key) == null) { status.add(key, "1"); } } 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) { // Update the fingerprint status.add(key, "1"); } else if (deletedItems.get(key) != null) { // Update the fingerprint status.remove(key); } } // If the item was succesfully synchronized, then we clear the dirty // flag if (isSuccess(itemStatus) && itemStatus != SyncSource.CHUNK_SUCCESS_STATUS) { clearSyncDirty(key); } } @Override public boolean removeItem(SyncItem item) throws TrackerException { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Removing item " + item.getKey()); } if (item.getState() == SyncItem.STATE_DELETED) { status.remove(item.getKey()); } else { if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Updating status"); } if (item.getState() == SyncItem.STATE_NEW) { status.add(item.getKey(), "1"); } if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Updating events table"); } // Removing item from the list of changes String key = item.getKey(); clearSyncDirty(key); } return true; } protected boolean isDirty(Cursor snapshot, StringKeyValuePair kvp) throws IOException { int syncDirtyColIdx = snapshot.getColumnIndexOrThrow(CalendarContract.Events.DIRTY); return 1 == snapshot.getInt(syncDirtyColIdx); } protected String computeFingerprint(String key, Cursor cursor) throws IOException { return "1"; } private void clearSyncDirty(String key) { // This item was succesfully synced, mark it as such long id = Long.parseLong(key); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Updating sync dirty flag for " + id); } ContentValues values = new ContentValues(); values.put(CalendarContract.Events.DIRTY, 0); if (this.accountName==null) resolveAccount(id); Uri uri = ContentUris.withAppendedId(CalendarManager.asSyncAdapter(CalendarContract.Events.CONTENT_URI, this.accountName, this.accountType), id); int numUpdates = resolver.update(uri, values, null, null); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "Number of updated rows = " + numUpdates); } } /** * Determines accountName and type via given event * @param eventId */ private void resolveAccount(long eventId) { String projection[] = {CalendarContract.Events.ACCOUNT_NAME, CalendarContract.Events.ACCOUNT_TYPE}; Cursor c = this.resolver.query(CalendarContract.Events.CONTENT_URI, projection, CalendarContract.Events._ID + "=?", new String[]{Long.toString(eventId)}, null); if (c.moveToFirst()) { this.accountName = c.getString(c.getColumnIndexOrThrow(CalendarContract.Events.ACCOUNT_NAME)); this.accountType = c.getString(c.getColumnIndexOrThrow(CalendarContract.Events.ACCOUNT_TYPE)); } } public boolean hasChanges() { boolean result = false; begin(SyncSource.INCREMENTAL_SYNC, false); result |= getNewItemsCount() > 0; result |= getUpdatedItemsCount() > 0; result |= getDeletedItemsCount() > 0; end(); return result; } }