package com.joelapenna.foursquared; import android.accounts.*; import android.content.*; import android.database.ContentObserver; import android.os.Handler; import android.preference.PreferenceManager; import com.joelapenna.foursquare.Foursquare; import com.joelapenna.foursquare.error.FoursquareError; import com.joelapenna.foursquare.error.FoursquareException; import com.joelapenna.foursquare.types.Checkin; import com.joelapenna.foursquare.types.Group; import com.joelapenna.foursquare.types.User; import com.joelapenna.foursquared.location.LocationUtils; import com.joelapenna.foursquared.preferences.Preferences; import com.joelapenna.foursquared.util.StringFormatters; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.RemoteException; import android.provider.ContactsContract; import android.provider.ContactsContract.Data; import android.provider.ContactsContract.RawContacts; import android.util.Log; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; final class SyncImpl implements Sync { final private static String TAG = "Sync"; static void syncFriends(Foursquared mFoursquared, Foursquare mFoursquare, ContentResolver resolver, AccountManager mAccountManager, Account account) { String password = null; try { Log.i(TAG, "getting password from account manager"); password = mAccountManager.blockingGetAuthToken(account, AuthenticatorService.ACCOUNT_TYPE, true); } catch (OperationCanceledException e) { Log.w(TAG, "operation cancelled while getting auth token", e); } catch (AuthenticatorException e) { Log.e(TAG, "authenticator exception while getting auth token", e); } catch (IOException e) { Log.e(TAG, "ioexception while getting auth token", e); } mFoursquare.setCredentials(account.name, password); final HashMap<String,User> friends = new HashMap<String,User>(); final HashMap<String,Checkin> checkinsByUserId = new HashMap<String,Checkin>(); Foursquare.Location loc = LocationUtils.createFoursquareLocation(mFoursquared.getLastKnownLocation()); try { User user = mFoursquare.user(null, false, false, loc); friends.put(user.getId(), user); Group<User> friendsFromServer = mFoursquare.friends(user.getId(), loc); for ( User friend : friendsFromServer ) { Log.i(TAG, "Stashed friend " + friend.getId()); friends.put(friend.getId(), friend); } } catch (FoursquareError e) { Log.e(TAG, "error fetching friends", e); } catch (FoursquareException e) { Log.e(TAG, "exception fetching friends", e); } catch (IOException e) { Log.e(TAG, "ioexception fetching friends", e); } Log.i(TAG, "got " + friends.size() + " friends from server"); final Group<Checkin> checkins = new Group<Checkin>(); try { checkins.addAll(mFoursquare.checkins(loc)); } catch (FoursquareError e) { Log.e(TAG, "error fetching checkins", e); } catch (FoursquareException e) { Log.e(TAG, "error fetching checkins", e); } catch (IOException e) { Log.e(TAG, "error fetching checkins", e); } for ( Checkin checkin : checkins ) { checkinsByUserId.put(checkin.getUser().getId(), checkin); } ArrayList<ContentProviderOperation> opList = new ArrayList<ContentProviderOperation>(); ArrayList<User> justAdded = new ArrayList<User>(); for ( User friend : friends.values() ) { long rawContactId = ((SyncImpl)mFoursquared.getSync()).getRawContactId(resolver, friend); if ( rawContactId == 0 ) { opList.addAll(addContact(mFoursquared, account, friend, opList.size())); justAdded.add(friend); } else { opList.addAll(updateContact(mFoursquared, resolver, rawContactId, friend, checkinsByUserId.get(friend.getId()))); } } try { resolver.applyBatch(ContactsContract.AUTHORITY, opList); } catch (Exception e) { Log.e(TAG, "Something went wrong during creation!", e); e.printStackTrace(); } opList.clear(); for ( User friend : justAdded ) { Log.i(TAG, "added friend " + friend.getFirstname() + " with id " + ((SyncImpl)mFoursquared.getSync()).getRawContactId(resolver, friend)); opList.addAll(mFoursquared.getSync().updateStatus(resolver, friend, checkinsByUserId.get(friend.getId()))); } try { resolver.applyBatch(ContactsContract.AUTHORITY, opList); } catch (Exception e) { Log.e(TAG, "Something went wrong while updating status for new contacts", e); e.printStackTrace(); } } private static ArrayList<ContentProviderOperation> addContact(Foursquared foursquared, Account account, User friend, int backReference) { ArrayList<ContentProviderOperation> opList = new ArrayList<ContentProviderOperation>(); ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI); builder.withValue(RawContacts.ACCOUNT_NAME, account.name); builder.withValue(RawContacts.ACCOUNT_TYPE, account.type); builder.withValue(RawContacts.SYNC1, friend.getId()); builder.withValue(RawContacts.SOURCE_ID, friend.getId()); opList.add(builder.build()); builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, backReference); builder.withValue(Data.MIMETYPE, ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE); builder.withValue(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, friend.getFirstname()); String last = friend.getLastname(); if ( last != null ) { builder.withValue(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, friend.getLastname()); } builder.withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, friend.getFirstname() + (last==null ? " "+friend.getLastname() : "")); opList.add(builder.build()); if ( friend.getPhone() != null ) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, backReference); builder.withValue(Data.MIMETYPE, ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE); builder.withValue(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM); builder.withValue(ContactsContract.CommonDataKinds.Phone.LABEL, "foursquare"); builder.withValue(ContactsContract.CommonDataKinds.Phone.NUMBER, friend.getPhone()); opList.add(builder.build()); } if ( friend.getEmail() != null ) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, backReference); builder.withValue(Data.MIMETYPE, ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE); builder.withValue(ContactsContract.CommonDataKinds.Email.DATA, friend.getEmail()); opList.add(builder.build()); } if ( friend.getPhoto() != null ) { builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(ContactsContract.CommonDataKinds.StructuredName.RAW_CONTACT_ID, backReference); builder.withValue(Data.MIMETYPE, ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE); try { Uri photoUri = Uri.parse(friend.getPhoto()); InputStream photoIn = foursquared.getRemoteResourceManager().getInputStream(photoUri); ByteArrayOutputStream photoOut = new ByteArrayOutputStream(); byte[] buf = new byte[64]; int r = 0; while ( (r = photoIn.read(buf)) >= 0) { photoOut.write(buf, 0, r); } byte[] photoBytes = photoOut.toByteArray(); builder.withValue(ContactsContract.CommonDataKinds.Photo.PHOTO, photoBytes); } catch (IOException e) { Log.w(TAG, "failed to fetch or read friend photo", e); } opList.add(builder.build()); } // create a Data record with custom type to point at Foursquare profile builder = ContentProviderOperation.newInsert(Data.CONTENT_URI); builder.withValueBackReference(Data.RAW_CONTACT_ID, backReference); builder.withValue(Data.MIMETYPE, "vnd.android.cursor.item/com.joelapenna.foursquared.profile"); builder.withValue(Data.DATA1, friend.getId()); builder.withValue(Data.DATA2, "Foursquare Profile"); builder.withValue(Data.DATA3, "View profile"); opList.add(builder.build()); return opList; } private static ArrayList<ContentProviderOperation> updateContact(Foursquared foursquared, ContentResolver resolver, long rawContactId, User friend, Checkin checkin) { Cursor c = resolver.query(Data.CONTENT_URI, RawContactDataQuery.PROJECTION, RawContactDataQuery.SELECTION, new String[] { String.valueOf(rawContactId) }, null); ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>(); Log.i(TAG, "updateContact passed rawContactId=" + rawContactId); try { while (c.moveToNext()) { Log.i(TAG, "processing row with raw_contact_id=" + c.getLong(5)); Uri uri = ContentUris.withAppendedId(Data.CONTENT_URI, rawContactId); long id = c.getLong(RawContactDataQuery.COLUMN_ID); String mimeType = c.getString(RawContactDataQuery.COLUMN_MIMETYPE); ContentValues values = new ContentValues(); if ( ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)) { // TODO: will this ever be null? what if it's null, and we want to clear the column? StringBuilder newDisplayName = new StringBuilder(); String contactGivenName = c.getString(RawContactDataQuery.COLUMN_GIVEN_NAME); boolean changeDisplayName = false; if ( friend.getFirstname() != null && !friend.getFirstname().equals(contactGivenName)) { Log.i(TAG, "updating given name from '" + contactGivenName + "' to '" + friend.getFirstname() + "'"); values.put(ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, friend.getFirstname()); newDisplayName.append(friend.getFirstname()); changeDisplayName = true; } else { newDisplayName.append(contactGivenName); } String contactFamilyName = c.getString(RawContactDataQuery.COLUMN_FAMILY_NAME); if ( friend.getLastname() != null && !friend.getLastname().equals(contactFamilyName)) { Log.i(TAG, "updating family name from '" + contactFamilyName + "' to '" + friend.getLastname() + "'"); values.put(ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME, friend.getLastname()); newDisplayName.append(" "); newDisplayName.append(friend.getLastname()); changeDisplayName = true; } else if (contactFamilyName != null) { newDisplayName.append(" "); newDisplayName.append(contactFamilyName); } if ( changeDisplayName ) { values.put(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME, newDisplayName.toString()); } } else if ( ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE.equals(mimeType) ) { if ( friend.getPhone() != null && !friend.getPhone().equals(c.getString(RawContactDataQuery.COLUMN_PHONE_NUMBER))) { Log.i(TAG, "updating phone to '" + friend.getPhone() + "'"); values.put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM); values.put(ContactsContract.CommonDataKinds.Phone.LABEL, "foursquare"); values.put(ContactsContract.CommonDataKinds.Phone.NUMBER, friend.getPhone()); } } else if ( ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE.equals(mimeType)) { if ( friend.getEmail() != null && !friend.getEmail().equals(c.getString(RawContactDataQuery.COLUMN_EMAIL_ADDRESS))) { Log.i(TAG, "updating email to '" + friend.getEmail() + "'"); values.put(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE, friend.getEmail()); } } if ( values.size() > 0) { ContentProviderOperation.Builder op = ContentProviderOperation.newUpdate(uri); op.withValues(values); Log.i(TAG, "updating " + values.size() + " values; building op"); ops.add(op.build()); } if ( checkin != null ) { ContentProviderOperation.Builder updateStatus = ContentProviderOperation.newInsert(ContactsContract.StatusUpdates.CONTENT_URI); updateStatus.withValue(ContactsContract.StatusUpdates.DATA_ID, id); String status = ((SyncImpl)foursquared.getSync()).createStatus(checkin); updateStatus.withValue(ContactsContract.StatusUpdates.STATUS, status); long created = new Date(checkin.getCreated()).getTime(); updateStatus.withValue(ContactsContract.StatusUpdates.STATUS_TIMESTAMP, created); ops.add(updateStatus.build()); } } } finally { c.close(); } return ops; } static class RawContactDataQuery { final static String[] PROJECTION = new String[] { Data._ID, Data.MIMETYPE, Data.DATA1, Data.DATA2, Data.DATA3, Data.RAW_CONTACT_ID }; final static String SELECTION = Data.RAW_CONTACT_ID + "=?"; final static int COLUMN_ID = 0; final static int COLUMN_MIMETYPE = 1; final static int COLUMN_DATA1 = 2; final static int COLUMN_DATA2 = 3; final static int COLUMN_DATA3 = 4; final static int COLUMN_PHONE_NUMBER = COLUMN_DATA1; final static int COLUMN_EMAIL_ADDRESS = COLUMN_DATA1; final static int COLUMN_GIVEN_NAME = COLUMN_DATA2; final static int COLUMN_FAMILY_NAME = COLUMN_DATA3; } /** * Watches for any changes to Contacts or Accounts and notifies its own observers that those changes occurred. * Attempts to aggregate events that occur in close temporal proximity. */ private final class SyncObservable extends Observable { final private AtomicBoolean observerRegistered = new AtomicBoolean(false); final private ContentObserver observer; final private ContentResolver resolver; final private AccountManager accountManager; final private OnAccountsUpdateListener listener; final private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); final private Runnable fireAggregate = new Runnable() { @Override public void run() { // invalidate/refresh lookup uri cache Set<String> userIds = new HashSet<String>(contactLookupUriCache.keySet()); contactLookupUriCache.clear(); for ( String userId : userIds ) { getContactLookupUri(resolver, userId); } notifyObservers(); } }; final private AtomicReference<ScheduledFuture<?>> scheduled = new AtomicReference<ScheduledFuture<?>>(); public SyncObservable(AccountManager accountManager, ContentResolver resolver) { this.accountManager = accountManager; this.resolver = resolver; this.observer = new ContentObserver(new Handler()) { @Override public void onChange(boolean selfChange) { scheduleEvent(); } }; this.listener = new OnAccountsUpdateListener() { @Override public void onAccountsUpdated(Account[] accounts) { scheduleEvent(); } }; } private void scheduleEvent() { ScheduledFuture sf = scheduled.get(); if ( sf != null ) { sf.cancel(false); } setChanged(); scheduler.schedule(fireAggregate, 1, TimeUnit.SECONDS); } @Override public void addObserver(Observer observer) { super.addObserver(observer); ensureContentObserver(); } @Override public void deleteObserver(Observer observer) { super.deleteObserver(observer); if (countObservers() == 0) { removeContentObserver(); } } @Override public void deleteObservers() { super.deleteObservers(); removeContentObserver(); } private void ensureContentObserver() { synchronized(observerRegistered) { if ( !observerRegistered.get() ) { accountManager.addOnAccountsUpdatedListener(listener, new Handler(), true); Uri observe = ContactsContract.Data.CONTENT_URI; resolver.registerContentObserver(observe, true, observer); observerRegistered.set(true); } } } private void removeContentObserver() { synchronized(observerRegistered) { accountManager.removeOnAccountsUpdatedListener(listener); resolver.unregisterContentObserver(observer); observerRegistered.set(false); } } } private final class SyncTask extends AsyncTask<Object, Void, Void> { @Override protected Void doInBackground(Object... params) { if (isEnabled()) { syncFriends(getAccount()); } return (Void)null; } } private static class RawContactIdQuery { static final String[] PROJECTION = new String[] { RawContacts._ID, RawContacts.CONTACT_ID }; static final String SELECTION = RawContacts.ACCOUNT_TYPE+"='"+AuthenticatorService.ACCOUNT_TYPE+"'" + " AND " + RawContacts.SOURCE_ID+"=?"; public final static int COLUMN_ID = 0; public final static int COLUMN_CONTACT_ID = 1; } private static class ContactLookupKeyQuery { static final String[] PROJECTION = new String[] { ContactsContract.Contacts.LOOKUP_KEY }; static final String SELECTION = ContactsContract.Contacts._ID + "=?"; public final static int COLUMN_LOOKUP_KEY = 0; } final private Foursquared mFoursquared; final private Context mContext; final private SyncObservable mObservable; private Boolean isEnabled = null; final private Map<String,Uri> contactLookupUriCache = new ConcurrentHashMap<String,Uri>(); SyncImpl(Foursquared foursquared) { this.mFoursquared = foursquared; this.mContext = foursquared; this.mObservable = new SyncObservable(AccountManager.get(mContext), mContext.getContentResolver()); } @Override public AsyncTask<?,?,?> createSyncTask() { return new SyncTask(); } @Override public void syncFriends(Account account) { syncFriends(mFoursquared, mFoursquared.getFoursquare(), mContext.getContentResolver(), AccountManager.get(mContext), account); } String createStatus(Checkin checkin) { if ( checkin.getVenue() != null ) { return " @ " + checkin.getVenue().getName(); } if ( checkin.getShout() != null ) { return "\"" + checkin.getShout() + "\""; } return StringFormatters.getCheckinMessageLine1(checkin, true); } private Account getAccount() { String login = PreferenceManager.getDefaultSharedPreferences(mContext).getString(Preferences.PREFERENCE_LOGIN, ""); return new Account(login, AuthenticatorService.ACCOUNT_TYPE); } @Override public boolean isEnabled() { return ContentResolver.getSyncAutomatically(getAccount(), ContactsContract.AUTHORITY); } @Override public boolean setEnabled(boolean enabled) { Account account = getAccount(); if (enabled) { String password = PreferenceManager.getDefaultSharedPreferences(mContext).getString(Preferences.PREFERENCE_PASSWORD, ""); if ("".equals(password)) { return false; } AccountManager.get(mContext).addAccountExplicitly(account, password, null); ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true); ContentProviderClient client = mContext.getContentResolver().acquireContentProviderClient(ContactsContract.AUTHORITY_URI); ContentValues cv = new ContentValues(); cv.put(ContactsContract.Groups.ACCOUNT_NAME, account.name); cv.put(ContactsContract.Groups.ACCOUNT_TYPE, account.type); cv.put(ContactsContract.Settings.UNGROUPED_VISIBLE, true); try { client.insert(ContactsContract.Settings.CONTENT_URI, cv); } catch (RemoteException e) { return false; } } else { // TODO: callback and handler should not be null; if something goes wrong, we should not set the pref AccountManager.get(mContext).removeAccount(account, null, null); } // if ( (isEnabled == null) || (isEnabled != enabled) ) { // mObservable.setChanged(); // mObservable.notifyObservers(); // } return true; } @Override public Observable getObservable() { Log.i(TAG, "observable requested"); return mObservable; } @Override public List<ContentProviderOperation> updateStatus(ContentResolver resolver, User friend, Checkin checkin) { if ( friend == null || checkin == null ) { return Collections.emptyList(); } long rawContactId = getRawContactId(resolver, friend); if ( rawContactId == 0 ) { return Collections.emptyList(); } ArrayList<ContentProviderOperation> optionOp = new ArrayList<ContentProviderOperation>(1); Cursor c = resolver.query(ContactsContract.Data.CONTENT_URI, SyncImpl.RawContactDataQuery.PROJECTION, SyncImpl.RawContactDataQuery.SELECTION, new String[] { String.valueOf(rawContactId) }, null); try { while (c.moveToNext()) { long id = c.getLong(SyncImpl.RawContactDataQuery.COLUMN_ID); ContentProviderOperation.Builder updateStatus = ContentProviderOperation.newInsert(ContactsContract.StatusUpdates.CONTENT_URI); updateStatus.withValue(ContactsContract.StatusUpdates.DATA_ID, id); String status = createStatus(checkin); updateStatus.withValue(ContactsContract.StatusUpdates.STATUS, status); long created = new Date(checkin.getCreated()).getTime(); updateStatus.withValue(ContactsContract.StatusUpdates.STATUS_TIMESTAMP, created); optionOp.add(updateStatus.build()); } } finally { c.close(); } return optionOp; } /** * * @return raw contact id, or 0 if not found */ long getRawContactId(ContentResolver resolver, User friend) { long rawContactId = 0; Cursor c = resolver.query(RawContacts.CONTENT_URI, RawContactIdQuery.PROJECTION, RawContactIdQuery.SELECTION, new String[] { friend.getId() }, null); try { if (c.moveToFirst()) { rawContactId = c.getLong(RawContactIdQuery.COLUMN_ID); } } finally { if ( c != null) { c.close(); } } return rawContactId; } long getContactId(ContentResolver resolver, String userId) { long contactId = 0; Cursor c = resolver.query(RawContacts.CONTENT_URI, RawContactIdQuery.PROJECTION, RawContactIdQuery.SELECTION, new String[] { userId }, null); try { if (c.moveToFirst()) { contactId = c.getLong(RawContactIdQuery.COLUMN_CONTACT_ID); } } finally { if ( c != null) { c.close(); } } return contactId; } @Override public Uri getContactLookupUri(ContentResolver resolver, String userId) { if ( contactLookupUriCache.containsKey(userId) ) { return contactLookupUriCache.get(userId); } long contactId = getContactId(resolver, userId); if ( contactId == 0 ) { return null; } Cursor c = resolver.query(ContactsContract.Contacts.CONTENT_URI, ContactLookupKeyQuery.PROJECTION, ContactLookupKeyQuery.SELECTION, new String[] { String.valueOf(contactId) }, null); String lookupKey = null; try { if ( c.moveToFirst() ) { lookupKey = c.getString(ContactLookupKeyQuery.COLUMN_LOOKUP_KEY); } } finally { if (c != null) { c.close(); } } if (lookupKey == null) { return null; } Uri uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_LOOKUP_URI, lookupKey+"/"+contactId); contactLookupUriCache.put(userId, uri); return uri; } }