/* * Copyright 2012 The Stanford MobiSocial Laboratory * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package mobisocial.musubi.service; import gnu.trove.list.linked.TLongLinkedList; import gnu.trove.map.hash.TLongObjectHashMap; import gnu.trove.procedure.TLongProcedure; import java.util.ArrayList; import java.util.Date; import mobisocial.crypto.IBHashedIdentity.Authority; import mobisocial.crypto.IBIdentity; import mobisocial.musubi.R; import mobisocial.musubi.model.MFeed; import mobisocial.musubi.model.MIdentity; import mobisocial.musubi.model.MMyAccount; import mobisocial.musubi.model.PresenceAwareNotify; import mobisocial.musubi.model.helpers.FeedManager; import mobisocial.musubi.model.helpers.IdentitiesManager; import mobisocial.musubi.model.helpers.MyAccountManager; import mobisocial.musubi.social.FacebookFriendFetcher; import mobisocial.musubi.ui.SettingsActivity; import mobisocial.musubi.ui.fragments.AccountLinkDialog; import mobisocial.musubi.util.Util; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import android.accounts.Account; import android.app.PendingIntent; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.database.ContentObserver; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.provider.ContactsContract; import android.util.Log; import com.facebook.android.Facebook; public class FacebookUpdateHandler extends ContentObserver { public static final String ACCOUNT_TYPE_FACEBOOK = "com.facebook.auth.login"; public final static String TAG = "FacebookUpdateHandler"; public static final boolean DEBUG = false; private final Context mContext; private final SQLiteOpenHelper mHelper; private final HandlerThread mThread; private final IdentitiesManager mIdentityManager; private final MyAccountManager mAccountManager; //private final ContactDataVersionManager mContactDataVersionManager; private FeedManager mFeedManager; private boolean mProfileDataChanged; private boolean mIdentityAdded; private static final int BATCH_SIZE = 50; private static final long MINIMUM_BACKOFF = 10 * 1000; private static final long MAXIMUM_BACKOFF = 30 * 60 * 1000; private Long mBackoff; public static FacebookUpdateHandler newInstance(Context context, SQLiteOpenHelper dbh) { HandlerThread thread = new HandlerThread("FacebookUpdateThread"); thread.setPriority(Thread.MIN_PRIORITY); thread.start(); return new FacebookUpdateHandler(context, dbh, thread); } private FacebookUpdateHandler(Context context, SQLiteOpenHelper dbh, HandlerThread thread) { super(new Handler(thread.getLooper())); mContext = context; mHelper = dbh; mThread = thread; mIdentityManager = new IdentitiesManager(dbh); mAccountManager = new MyAccountManager(dbh); mFeedManager = new FeedManager(dbh); mBackoff = 0L; context.getContentResolver().registerContentObserver(MusubiService.NETWORK_CHANGED, false, new ResetBackOffAndReconnectIfNotConnected(new Handler(thread.getLooper()))); } public class ResetBackOffAndReconnectIfNotConnected extends ContentObserver { Handler mHandler; public ResetBackOffAndReconnectIfNotConnected(Handler handler) { super(handler); mHandler = handler; } @Override public void onChange(boolean selfChange) { mBackoff = 0L; FacebookUpdateHandler.this.dispatchChange(false); } } private void retryAfterBackoff() { Long backoff = MINIMUM_BACKOFF; synchronized (mBackoff) { backoff = mBackoff * 2; backoff = (backoff > MAXIMUM_BACKOFF) ? MAXIMUM_BACKOFF : backoff; mBackoff = backoff; } new Handler(mThread.getLooper()).postDelayed(new Runnable() { @Override public void run() { mContext.getContentResolver().notifyChange( MusubiService.FACEBOOK_FRIEND_REFRESH, FacebookUpdateHandler.this); } }, mBackoff); } private void notifyOnAuthError() { Intent launch = new Intent(mContext, SettingsActivity.class); PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, launch, PendingIntent.FLAG_CANCEL_CURRENT); (new PresenceAwareNotify(mContext)).notify("Sign-In Required", "Facebook account failed to connect", contentIntent); } @Override public void onChange(boolean selfChange) { Facebook fb = AccountLinkDialog.getFacebookInstance(mContext); FacebookFriendFetcher fetcher = new FacebookFriendFetcher(fb); final Date start = new Date(); // only get friends' info after last updated time // TODO: find another way to keep track of data version /*long lastUpdateTime = mContactDatenaVersionManager.getLastFacebookUpdateTime(); long nextUpdateTime = lastUpdateTime;*/ JSONArray friendList = null; try { friendList = fetcher.getFriendInfo(); } catch (Exception e) { Log.i(TAG, "Non-auth facebook error. Retrying."); retryAfterBackoff(); return; } mBackoff = 0L; if(friendList == null) { Log.i(TAG, "not connected to facebook. cannot get updates"); if (fb.getAccessToken() != null) { notifyOnAuthError(); } return; } else if (friendList.length() == 0) { Log.i(TAG, "no friends found"); //AccountLinkDialog.refreshFacebookToken(mContext); return; } Log.i(TAG, "found " + friendList.length() + " friends"); TLongLinkedList ids = new TLongLinkedList(); // split up list and do batch update to our database final SQLiteDatabase db = mHelper.getWritableDatabase(); try { TLongObjectHashMap<String> photo_uris = new TLongObjectHashMap<String>(BATCH_SIZE); ArrayList<MIdentity> idents = new ArrayList<MIdentity>(BATCH_SIZE); for(int i = 0; i < friendList.length(); ) { int max = i + BATCH_SIZE; photo_uris.clear(); idents.clear(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { db.beginTransactionNonExclusive(); } else { db.beginTransaction(); } for(; i < max && i < friendList.length(); ++i) { JSONObject friend = friendList.getJSONObject(i); long fb_id = friend.getLong("uid"); IBIdentity ibid = new IBIdentity(Authority.Facebook, String.valueOf(fb_id), 0); MIdentity ident = ensureIdentity(fb_id, friend.getString("name"), ibid); idents.add(ident); ids.add(ident.id_); photo_uris.put(ident.id_, friend.getString("pic_square")); } db.setTransactionSuccessful(); db.endTransaction(); //TODO: update profile photos? for(MIdentity ident : idents) { if(mIdentityManager.hasThumbnail(ident)) { continue; } ident.thumbnail_ = FacebookFriendFetcher.getImageFromURL(photo_uris.get(ident.id_)); if(ident.thumbnail_ != null) { mIdentityManager.updateThumbnail(ident); mProfileDataChanged = true; } } } } catch(JSONException e) { Log.e(TAG, e.toString()); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { db.beginTransactionNonExclusive(); } else { db.beginTransaction(); } //add all detected members to account feed final String email = fetcher.getLoggedinUserEmail(); final String fb_id = fetcher.getLoggedinUserId(); if (email != null && fb_id != null) { MMyAccount cached_account = mAccountManager.lookupAccount(email, ACCOUNT_TYPE_FACEBOOK); if(cached_account == null) { IBIdentity ibid; ibid = new IBIdentity(Authority.Facebook, fb_id, 0); cached_account = new MMyAccount(); cached_account.accountName_ = email; cached_account.accountType_ = ACCOUNT_TYPE_FACEBOOK; MIdentity existingId = mIdentityManager.getIdentityForIBHashedIdentity(ibid); if (existingId != null) { cached_account.identityId_ = existingId.id_; } mAccountManager.insertAccount(cached_account); } final MMyAccount account = cached_account; if(account.feedId_ == null) { MFeed feed = new MFeed(); feed.accepted_ = false; //not visible feed.type_ = MFeed.FeedType.ASYMMETRIC; feed.name_ = MFeed.LOCAL_WHITELIST_FEED_NAME; mFeedManager.insertFeed(feed); account.feedId_ = feed.id_; mAccountManager.updateAccount(account); } ids.forEach(new TLongProcedure() { @Override public boolean execute(long id) { MIdentity ident = mIdentityManager.getIdentityForIBHashedIdentity( new IBIdentity(Authority.Facebook, id + "", 0)); if (ident != null && ident.id_ != account.identityId_) { mFeedManager.ensureFeedMember(account.feedId_, ident.id_); } return true; } }); } db.setTransactionSuccessful(); db.endTransaction(); // set last update time the id that has the lastest profile_update_time //mContactDataVersionManager.setLastFacebookUpdateTime(nextUpdateTime); Date end = new Date(); double time = end.getTime() - start.getTime(); time /= 1000; Log.i(TAG, "update address book took " + time + " seconds"); // wake up content observers if (mIdentityAdded) { //wake up the profile push mContext.getContentResolver().notifyChange(MusubiService.WHITELIST_APPENDED, this); } if (mProfileDataChanged) { //refresh the ui... mContext.getContentResolver().notifyChange(MusubiService.PRIMARY_CONTENT_CHANGED, this); } if(mIdentityAdded | mProfileDataChanged) { //update the our musubi address book as needed. String accountName = mContext.getString(R.string.account_name); String accountType = mContext.getString(R.string.account_type); Account ac = new Account(accountName, accountType); ContentResolver.requestSync(ac, ContactsContract.AUTHORITY, new Bundle()); } // Refresh Facebook token in case it has expired // Do this last because the token should still be good at this point, but // may be close to expiring. //AccountLinkDialog.refreshFacebookToken(mContext); } MIdentity ensureIdentity(long contact_id, String display_name, IBIdentity id) { MIdentity ident = mIdentityManager.getIdentityForIBHashedIdentity(id); boolean changed = false; boolean insert = false; if(ident == null) { ident = new MIdentity(); insert = true; //stuff that lets us reach them ident.type_ = id.authority_; ident.principal_ = id.principal_; ident.principalHash_ = id.hashed_; ident.principalShortHash_ = Util.shortHash(id.hashed_); //stuff that makes them pretty ident.name_ = display_name; mIdentityAdded = true; } if(!ident.whitelisted_) { changed = true; ident.whitelisted_ = true; //dont' change the blocked flag here, because it could only have //been set through explicit user interaction mIdentityAdded = true; } if(display_name != null && ident.name_ == null) { changed = true; ident.name_ = display_name; } if(insert) { if(DEBUG) Log.i(TAG, "insert facebook user " + display_name); ident.whitelisted_ = true; mIdentityManager.insertIdentity(ident); mFeedManager.acceptFeedsFromMember(mContext, ident.id_); } else if(changed) { if(DEBUG) Log.i(TAG, "update facebook user " + display_name); mIdentityManager.updateIdentity(ident); mProfileDataChanged = true; } return ident; } }