Java tutorial
/********************************************************************\ File: SweetContactSync.java Copyright 2011 Vicent Segu Pascual This file is part of Sweet. Sweet is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Sweet 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 General Public License along with Sweet. If not, see http://www.gnu.org/licenses/. \********************************************************************/ package com.github.vseguip.sweet.contacts; import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.http.auth.AuthenticationException; import com.github.vseguip.sweet.R; import com.github.vseguip.sweet.rest.SugarAPI; import com.github.vseguip.sweet.rest.SugarAPIFactory; import android.accounts.Account; import android.accounts.AccountManager; import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.AbstractThreadedSyncAdapter; import android.content.ContentProviderClient; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.SyncResult; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; /** * SweetContactSync * * @author vseguip * * * SugarCRM return date_modified field as a String (in GMT) which can be * lexicographically compared to determine which date is the latest. The * sync strategy is as follows * * 1) If the account "lastSync" user data is null perform a full sync * * 2) If the account "lastSync" field contains a String use it to get * only newer contacts.Everytime we get new contacts we search for the * latest modified time and we get all contacts with modification time * greater than or equal to the last sync time. This is done since we * can have a newer contact inserted in SugarCRM with the same date * since SugarCRM only keeps second accuracy. This means we will * probably get the latest entry also but that's not really a problem. * * 3) Store last modified date into the account user data for future * use. * * NOTE: 3 things about this strategy a) All times used are referenced * to SugarCRM time system which means there are no conflicts between * differing time zones in the server or client. No special care has to * be taken * * b) We never don't need to parse the time in the client so again no * problems when it comes to different time zones, etc * * c) The user can force a full sync using the account preferences to * set the lastSync user data to null. * * Contacts conlict resolution: * * 1. Get newer contacts from SugarCRM * * 2. Get dirty local modified contacts * * 3. Get locally created contacts * * 4. Set of conflicts id found by performing intersection on SourceID * between 1 and 2 * * 5. Update non conflicting data to local * * 6. Send new contacts to remote and update the local source ID's * * 7. Send non conflicting data to remote * * 8. for each conflicting contact in Set of conflicting contacts do One * contact resolution * * * * One contact resolution * * 1. Find set of differing fields * * 2. If field exists locally but not remotely upload to sugar * * 3. If field exists remotely but not locally update locally * * 4. If field different in both server and locally use preference to * decide which one to keep * */ /** * @author vseguip * */ /** * @author vseguip * */ /** * @author vseguip * */ public class SweetContactSync extends AbstractThreadedSyncAdapter { private static final int BATCH_SIZE = 200; Context mContext; private String AUTH_TOKEN_TYPE; private AccountManager mAccountManager; private String mAuthToken; private final String TAG = "SweetContactSync"; static final String LAST_SYNC_KEY = "lastSync"; public SweetContactSync(Context context, boolean autoInitialize) { super(context, autoInitialize); Log.i(TAG, "SweetContactSync"); mContext = context; AUTH_TOKEN_TYPE = mContext.getString(R.string.account_type); mAccountManager = AccountManager.get(mContext); } public interface ISugarRunnable { public void run() throws URISyntaxException, OperationCanceledException, AuthenticatorException, IOException, AuthenticationException; } class SugarRunnable implements Runnable { ISugarRunnable r; Account mAccount; SyncResult mSyncResult; public SugarRunnable(Account acc, SyncResult syncResult, ISugarRunnable _r) { r = _r; mAccount = acc; mSyncResult = syncResult; } @Override public void run() { try { r.run(); } catch (URISyntaxException ex) { if (mAccount != null) mAccountManager.confirmCredentials(mAccount, null, null, null, null); } catch (OperationCanceledException e) { mSyncResult.stats.numConflictDetectedExceptions++; e.printStackTrace(); } catch (AuthenticatorException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (AuthenticationException aex) { // if (SweetContactSync.this.mAuthToken != null) { mAccountManager.invalidateAuthToken(AUTH_TOKEN_TYPE, SweetContactSync.this.mAuthToken); // } else { // // mAccountManager.confirmCredentials(mAccount, null, null, // null, null); // } mSyncResult.stats.numAuthExceptions++; } } } /** * Fetches the list of newer contacts in small batches to avoid out of * memory errors or excessively long query times that can timeout. *Must* be * orderd by modification date to be consistent so the last returned * contacts are always the newer ones. This can lead to a contact being returned two times so we use a hashtable to avoid this. * * @param sugar The Sugar API object * @param lastDate Anchor for newer contacts * @return The list of contacts * @throws IOException */ public List<ISweetContact> fetchContacts(SugarAPI sugar, String lastDate) throws AuthenticationException, IOException { List<ISweetContact> contacts = null; HashMap<String, ISweetContact> allContacts = new HashMap<String, ISweetContact>(); int cursor = 0; do { Log.i(TAG, "Getting batch from " + cursor + " to " + (cursor + BATCH_SIZE) + " contacts"); contacts = sugar.getNewerContacts(mAuthToken, lastDate, cursor, BATCH_SIZE); for (ISweetContact contact : contacts) { allContacts.put(contact.getId(), contact); } cursor += BATCH_SIZE; } while ((contacts != null) && (contacts.size() == BATCH_SIZE)); if (contacts != null) contacts = new ArrayList<ISweetContact>(allContacts.values()); return contacts; } @Override public void onPerformSync(final Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { Log.i(TAG, "onPerformSync()"); // Get preferences SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(mContext); boolean fullSync = settings.getBoolean(mContext.getString(R.string.full_sync), false); if (fullSync) mAccountManager.setUserData(account, LAST_SYNC_KEY, null); performNetOperation(new SugarRunnable(account, syncResult, new ISugarRunnable() { @Override public void run() throws URISyntaxException, OperationCanceledException, AuthenticatorException, IOException, AuthenticationException { Log.i(TAG, "Running PerformSync closure()"); mAuthToken = mAccountManager.blockingGetAuthToken(account, AUTH_TOKEN_TYPE, true); SugarAPI sugar = SugarAPIFactory.getSugarAPI(mAccountManager, account); String lastDate = mAccountManager.getUserData(account, LAST_SYNC_KEY); List<ISweetContact> contacts = null; try { contacts = fetchContacts(sugar, lastDate); } catch (AuthenticationException ex) { // maybe expired session, invalidate token and request new // one mAccountManager.invalidateAuthToken(account.type, mAuthToken); mAuthToken = mAccountManager.blockingGetAuthToken(account, AUTH_TOKEN_TYPE, false); } catch (NullPointerException npe) { // maybe expired session, invalidate token and request new // one mAccountManager.invalidateAuthToken(account.type, mAuthToken); mAuthToken = mAccountManager.blockingGetAuthToken(account, AUTH_TOKEN_TYPE, false); } // try again, it could be due to an expired session if (contacts == null) { contacts = fetchContacts(sugar, lastDate); } List<ISweetContact> modifiedContacts = ContactManager.getLocallyModifiedContacts(mContext, account); List<ISweetContact> createdContacts = ContactManager.getLocallyCreatedContacts(mContext, account); // Get latest date from server for (ISweetContact c : contacts) { String contactDate = c.getDateModified(); if ((lastDate == null) || (lastDate.compareTo(contactDate) < 0)) { lastDate = contactDate; } } // Determine conflicting contacts Set<String> conflictSet = getConflictSet(contacts, modifiedContacts); Map<String, ISweetContact> conflictingSugarContacts = filterIds(contacts, conflictSet); Map<String, ISweetContact> conflictingLocalContacts = filterIds(modifiedContacts, conflictSet); if (modifiedContacts.size() > 0) { // Send modified local non conflicting contacts to the // server List<String> newIds = sugar.sendNewContacts(mAuthToken, modifiedContacts, false); if (newIds.size() != modifiedContacts.size()) { throw new OperationCanceledException("Error updating local contacts in the remote server"); } ContactManager.cleanDirtyFlag(mContext, modifiedContacts); } if (createdContacts.size() > 0) { List<String> newIds = sugar.sendNewContacts(mAuthToken, createdContacts, true); if (newIds.size() != createdContacts.size()) { // something wrong happened, it's probable the user will // have to clear the data throw new OperationCanceledException("Error creating local contacts in the remote server"); } ContactManager.assignSourceIds(mContext, createdContacts, newIds); ContactManager.cleanDirtyFlag(mContext, createdContacts); } // Sync remote contacts locally. if (contacts.size() > 0) { ContactManager.syncContacts(mContext, account, contacts); } // resolve remaining conflicts List<ISweetContact> resolvedContacts = new ArrayList<ISweetContact>(); for (String id : conflictSet) { ISweetContact local = conflictingLocalContacts.get(id); ISweetContact remote = conflictingSugarContacts.get(id); if (local.equals(remote)) { // no need to sync resolvedContacts.add(local); conflictingLocalContacts.remove(id); conflictingSugarContacts.remove(id); } else { Log.i(TAG, "Local contact differs from remote contact " + local.getFirstName() + " " + local.getLastName()); if (local.equalUIFields(remote)) { // Differed in a non visible field like the account // id or similar, use server version and resolve // automatically resolvedContacts.add(remote); conflictingLocalContacts.remove(id); conflictingSugarContacts.remove(id); } } } ContactManager.cleanDirtyFlag(mContext, resolvedContacts); if (conflictingLocalContacts.size() > 0) { // Create a notification that can launch an mActivity to // resolve the pending conflict NotificationManager nm = (NotificationManager) mContext .getSystemService(Context.NOTIFICATION_SERVICE); Notification notify = new Notification(R.drawable.icon, mContext.getString(R.string.notify_sync_conflict_ticket), System.currentTimeMillis()); Intent intent = new Intent(mContext, SweetConflictResolveActivity.class); intent.putExtra("account", account); SweetConflictResolveActivity.storeConflicts(conflictingLocalContacts, conflictingSugarContacts); notify.setLatestEventInfo(mContext, mContext.getString(R.string.notify_sync_conflict_title), mContext.getString(R.string.notify_sync_conflict_message), PendingIntent.getActivity(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT)); nm.notify(SweetConflictResolveActivity.NOTIFY_CONFLICT, SweetConflictResolveActivity.NOTIFY_CONTACT, notify); throw new OperationCanceledException("Pending conflicts"); } // Save the last sync time in the account if all went ok mAccountManager.setUserData(account, LAST_SYNC_KEY, lastDate); } })); } Map<String, ISweetContact> filterIds(List<ISweetContact> contacts, Set<String> filter) { Map<String, ISweetContact> filteredContacts = new HashMap<String, ISweetContact>(); Iterator<ISweetContact> it = contacts.iterator(); while (it.hasNext()) { ISweetContact contact = it.next(); if (filter.contains(contact.getId())) { it.remove(); filteredContacts.put(contact.getId(), contact); } } return filteredContacts; } Set<String> getConflictSet(List<ISweetContact> list1, List<ISweetContact> list2) { Set<String> set1 = getSetOfIds(list1); Set<String> set2 = getSetOfIds(list2); set1.retainAll(set2); return set1; } Set<String> getSetOfIds(List<ISweetContact> contacts) { Set<String> ids = new HashSet<String>(); if (contacts != null) { for (ISweetContact c : contacts) { ids.add(c.getId()); } } return ids; } void performNetOperation(Runnable r) { r.run(); } }