/*
* 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.ui.fragments;
import gnu.trove.set.TLongSet;
import gnu.trove.set.hash.TLongHashSet;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import mobisocial.crypto.IBHashedIdentity.Authority;
import mobisocial.crypto.IBIdentity;
import mobisocial.metrics.MusubiMetrics;
import mobisocial.musubi.App;
import mobisocial.musubi.R;
import mobisocial.musubi.facebook.SessionStore;
import mobisocial.musubi.model.MDevice;
import mobisocial.musubi.model.MFeed;
import mobisocial.musubi.model.MIdentity;
import mobisocial.musubi.model.MMyAccount;
import mobisocial.musubi.model.MPendingIdentity;
import mobisocial.musubi.model.helpers.DeviceManager;
import mobisocial.musubi.model.helpers.FeedManager;
import mobisocial.musubi.model.helpers.IdentitiesManager;
import mobisocial.musubi.model.helpers.MyAccountManager;
import mobisocial.musubi.model.helpers.PendingIdentityManager;
import mobisocial.musubi.service.AddressBookUpdateHandler;
import mobisocial.musubi.service.MusubiService;
import mobisocial.musubi.service.WizardStepHandler;
import mobisocial.musubi.social.FacebookFriendFetcher;
import mobisocial.musubi.ui.MusubiBaseActivity;
import mobisocial.musubi.ui.SettingsActivity;
import mobisocial.musubi.ui.fragments.AccountLinkDialog.AccountLooperThread.Job;
import mobisocial.musubi.ui.util.UiUtil;
import mobisocial.musubi.util.CommonLayouts;
import mobisocial.musubi.util.InstrumentedActivity;
import org.json.JSONException;
import org.json.JSONObject;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.provider.Settings;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.SupportActivity;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.facebook.android.DialogError;
import com.facebook.android.Facebook;
import com.facebook.android.Facebook.ServiceListener;
import com.facebook.android.FacebookError;
import com.facebook.android.Util;
public class AccountLinkDialog extends DialogFragment {
final static String TAG = "AccountLinkDialog";
final boolean DBG = MusubiBaseActivity.DBG;
public static final String ACCOUNT_TYPE_FACEBOOK = "com.facebook.auth.login";
public static final String ACCOUNT_TYPE_GOOGLE = "com.google";
public static final String ACCOUNT_TYPE_PHONE = "mobisocial.musubi.phone";
static final int MSG_CONNECT_GOOGLE = 1;
static final int MSG_CONNECT_FB = 2;
static final int MSG_ADD_TO_DATABASE = 3;
static final int MSG_CONNECT_PHONE = 4;
public static final String GOOGLE_OAUTH_SCOPE = "oauth2:https://www.google.com/m8/feeds/";
public static final String FACEBOOK_APP_ID = "111111111111";
public static final String[] FACEBOOK_PERMISSIONS =
new String[] {"read_friendlists", "email", "offline_access","publish_stream"};
private static final int REQUEST_GOOGLE_ACCOUNT = 97;
private static final int REQUEST_FACEBOOK = 98;
private static final int REQUEST_GOOGLE_AUTHENTICATE = 99;
private static final int REQUEST_PHONE_NUMBER = 100;
private static final String EXTRA_ACCOUNT = "account";
private static final String TEXT_CONNECTED = "Connected!";
private static final String TEXT_CHECKING = "Checking status...";
private static final String TEXT_ERROR_CONNECTING = "Failed to connect.";
private static final String TEXT_UNVERIFIED = "Verification Required.";
private static final int DISPLAYED_SERVICES = 2;
private static final int SHORTEST_PHONE_NUMBER = 6;
private static final int LONGEST_PHONE_NUMBER = 14;
// TODO: better support for facebook/google synchronisity issues.
private Activity mActivity;
private MyAccountManager mAccountManager;
private ListView mAccountList;
private AccountAdapter mAccountAdapter;
private String mPendingAccountName = null;
private String mPendingAccountType = null;
private static AccountLooperThread sAccountLooperThread;
enum AccountStatus { PENDING, CONNECTED, ERROR, UNVERIFIED };
public static AccountLinkDialog newInstance() {
AccountLinkDialog frag = new AccountLinkDialog();
Bundle args = new Bundle();
frag.setArguments(args);
return frag;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NORMAL, R.style.Theme_D1dialog);
}
@Override
public void onResume() {
super.onResume();
if(mPendingAccountType != null && mPendingAccountName != null && mPendingAccountType.equals(ACCOUNT_TYPE_GOOGLE)) {
String accountName = mPendingAccountName;
mPendingAccountName = null;
mPendingAccountType = null;
tryGoogleAccount(mActivity, accountName);
}
}
@Override
public void onAttach(SupportActivity activity) {
super.onAttach(activity);
mActivity = activity.asActivity();
SQLiteOpenHelper databaseSource = App.getDatabaseSource(mActivity);
mAccountManager = new MyAccountManager(databaseSource);
if(sAccountLooperThread == null) {
sAccountLooperThread = new AccountLooperThread();
sAccountLooperThread.start();
}
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
LinearLayout window = new LinearLayout(mActivity);
window.setLayoutParams(CommonLayouts.FULL_SCREEN);
window.setOrientation(LinearLayout.VERTICAL);
LinearLayout socialBox = new LinearLayout(mActivity);
socialBox.setLayoutParams(CommonLayouts.FULL_WIDTH);
socialBox.setOrientation(LinearLayout.HORIZONTAL);
socialBox.setWeightSum(1.0f * DISPLAYED_SERVICES);
/** Google **/
ImageButton google = new ImageButton(mActivity);
google.setImageResource(R.drawable.google);
google.setOnClickListener(mGoogleClickListener);
google.setLayoutParams(new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f));
google.setAdjustViewBounds(true);
socialBox.addView(google);
/** Facebook **/
ImageButton facebook = new ImageButton(mActivity);
facebook.setImageResource(R.drawable.facebook);
facebook.setOnClickListener(mFacebookClickListener);
facebook.setLayoutParams(new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f));
facebook.setAdjustViewBounds(true);
socialBox.addView(facebook);
/** Phone Number **/
ImageButton phone = new ImageButton(mActivity);
phone.setImageResource(R.drawable.phone);
phone.setOnClickListener(mPhoneClickListener);
phone.setLayoutParams(new LinearLayout.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 1.0f));
phone.setAdjustViewBounds(true);
//socialBox.addView(phone);
/** List of known accounts **/
TextView chooseService = new TextView(mActivity);
chooseService.setText("Choose a service to connect.");
chooseService.setVisibility(View.GONE);
chooseService.setLayoutParams(CommonLayouts.FULL_SCREEN);
chooseService.setTextSize(20);
mAccountAdapter = new AccountAdapter(getActivity());
mAccountList = new ListView(getActivity());
mAccountList.setAdapter(mAccountAdapter);
mAccountList.setPadding(6, 10, 6, 0);
mAccountList.setLayoutParams(CommonLayouts.FULL_SCREEN);
mAccountList.setEmptyView(chooseService);
/** Put it together **/
window.addView(socialBox);
window.addView(mAccountList);
window.addView(chooseService);
initialize();
return window;
}
void initialize() {
MMyAccount[] accounts = mAccountManager.getClaimedAccounts(ACCOUNT_TYPE_GOOGLE);
for (MMyAccount account : accounts) {
mAccountAdapter.add(account);
Message m = sAccountLooperThread.obtainMessage();
m.what = MSG_CONNECT_GOOGLE;
Job job = new AccountLooperThread.Job();
job.mAccount = account.accountName_;
job.mDialog = this;
m.obj = job;
sAccountLooperThread.sendMessage(m);
}
accounts = mAccountManager.getClaimedAccounts(ACCOUNT_TYPE_FACEBOOK);
for (MMyAccount account : accounts) {
mAccountAdapter.add(account);
Message m = sAccountLooperThread.obtainMessage();
m.what = MSG_CONNECT_FB;
Job job = new AccountLooperThread.Job();
job.mId = account.id_;
job.mDialog = this;
m.obj = job;
sAccountLooperThread.sendMessage(m);
}
accounts = mAccountManager.getMyAccounts(ACCOUNT_TYPE_PHONE);
for (MMyAccount account : accounts) {
mAccountAdapter.add(account);
Message m = sAccountLooperThread.obtainMessage();
m.what = MSG_CONNECT_PHONE;
Job job = new AccountLooperThread.Job();
job.mId = account.id_;
job.mAccount = account.accountName_;
job.mDialog = this;
m.obj = job;
sAccountLooperThread.sendMessage(m);
}
}
public static class AccountDetails {
public String principal;
public String accountName;
public String accountType;
public boolean owned;
public AccountDetails(String principal, String accountName,
String accountType, boolean owned) {
this.principal = principal;
this.accountName = accountName;
this.accountType = accountType;
this.owned = owned;
}
}
/**
* Adds an account to the local database. Must not be called on the main thread.
*/
public static MMyAccount addAccountToDatabase(Activity activity, AccountDetails accountDetails) {
SQLiteOpenHelper databaseSource = App.getDatabaseSource(activity);
IdentitiesManager im = new IdentitiesManager(databaseSource);
MyAccountManager am = new MyAccountManager(databaseSource);
DeviceManager dm = new DeviceManager(databaseSource);
FeedManager fm = new FeedManager(databaseSource);
String accountType = accountDetails.accountType;
String accountName = accountDetails.accountName;
String principal = accountDetails.principal;
boolean owned = accountDetails.owned;
IBIdentity ibid;
if (accountType.equals(ACCOUNT_TYPE_GOOGLE)) {
ibid = new IBIdentity(Authority.Email, principal, 0);
} else if (accountType.equals(ACCOUNT_TYPE_FACEBOOK)) {
ibid = new IBIdentity(Authority.Facebook, principal, 0);
} else if (accountType.equals(ACCOUNT_TYPE_PHONE)) {
ibid = new IBIdentity(Authority.PhoneNumber, principal, 0);
} else {
throw new RuntimeException("Unsupported account type " + accountType);
}
SQLiteDatabase db = databaseSource.getWritableDatabase();
db.beginTransaction();
try {
// Ensure identity in the database
MIdentity id = im.getIdentityForIBHashedIdentity(ibid);
//don't repeatedly add profile broadcast groups or do any
//of this processing if the account is already owned.
if (id != null && id.owned_) {
return null;
}
MIdentity original = im.getOwnedIdentities().get(0);
//if this identity wasnt already in the contact book, we need to update it
if (id == null) {
id = new MIdentity();
populateMyNewIdentity(activity, principal, im, ibid, id, original, owned);
im.insertIdentity(id);
} else {
populateMyNewIdentity(activity, principal, im, ibid, id, original, owned);
im.updateIdentity(id);
}
im.updateMyProfileName(activity, id.musubiName_, false);
im.updateMyProfileThumbnail(activity, id.musubiThumbnail_, false);
// Ensure account entry exists
MMyAccount account = am.lookupAccount(accountName, accountType);
if (account == null) {
//create the account
account = new MMyAccount();
account.accountName_ = accountName;
account.accountType_ = accountType;
account.identityId_ = id.id_;
am.insertAccount(account);
} else {
account.identityId_ = id.id_;
am.updateAccount(account);
}
// For accounts linked to identities that are not yet owned,
// skip further initialization
if (owned) {
MDevice dev = dm.getDeviceForName(id.id_, dm.getLocalDeviceName());
// Ensure device exists
if(dev == null) {
dev = new MDevice();
dev.deviceName_ = dm.getLocalDeviceName();
dev.identityId_ = id.id_;
dm.insertDevice(dev);
}
//this feed will contain all members who should receive
//a profile for the account because of a friend introduction
MFeed provisional = new MFeed();
provisional.name_ = MFeed.PROVISONAL_WHITELIST_FEED_NAME;
provisional.type_ = MFeed.FeedType.ASYMMETRIC;
fm.insertFeed(provisional);
//XXX
//TODO: in other places in the code, we should be pruning the
//provisional whitelist feed as people become whitelisted..
//these get inserted for owned identities to allow profile
//broadcasts to go out
MMyAccount provAccount = new MMyAccount();
provAccount.accountName_ = MMyAccount.PROVISIONAL_WHITELIST_ACCOUNT;
provAccount.accountType_ = MMyAccount.INTERNAL_ACCOUNT_TYPE;
provAccount.identityId_ = id.id_;
provAccount.feedId_ = provisional.id_;
am.insertAccount(provAccount);
//this feed will contain all members who should receive
//a profile for the account because they are whitelisted
//and contacted you on one of your accounts.
MFeed accountBroadcastFeed = new MFeed();
accountBroadcastFeed.name_ = MFeed.LOCAL_WHITELIST_FEED_NAME;
accountBroadcastFeed.type_ = MFeed.FeedType.ASYMMETRIC;
fm.insertFeed(accountBroadcastFeed);
MMyAccount localAccount = new MMyAccount();
localAccount.accountName_ = MMyAccount.LOCAL_WHITELIST_ACCOUNT;
localAccount.accountType_ = MMyAccount.INTERNAL_ACCOUNT_TYPE;
localAccount.identityId_ = id.id_;
localAccount.feedId_ = accountBroadcastFeed.id_;
am.insertAccount(localAccount);
db.setTransactionSuccessful();
ContentResolver resolver = activity.getContentResolver();
// Notify interested services (identity available makes AMQP wake up for example)
resolver.notifyChange(MusubiService.OWNED_IDENTITY_AVAILABLE, null);
resolver.notifyChange(MusubiService.MY_PROFILE_UPDATED, null);
// Makes key update wake up
resolver.notifyChange(MusubiService.AUTH_TOKEN_REFRESH, null);
WizardStepHandler.accomplishTask(activity, WizardStepHandler.TASK_LINK_ACCOUNT);
App.getUsageMetrics(activity).report(MusubiMetrics.ACCOUNT_CONNECTED,
account.accountType_);
} else {
db.setTransactionSuccessful();
}
return account;
} finally {
db.endTransaction();
}
}
private static void populateMyNewIdentity(Activity activity, String accountName,
IdentitiesManager im, IBIdentity ibid, MIdentity id,
MIdentity original, boolean owned) {
//its ours and we are on the network
id.claimed_ = true;
id.owned_ = owned;
id.whitelisted_ = true;
id.hasSentEmail_ = true;
//set up the identity data
id.principal_ = accountName;
id.type_ = ibid.authority_;
id.principalHash_ = ibid.hashed_;
id.principalShortHash_ = mobisocial.musubi.util.Util.shortHash(ibid.hashed_);
if (owned) {
//mark us for one way push
id.sentProfileVersion_ = 1;
//clone the profile fields
id.musubiName_ = original.musubiName_;
id.musubiThumbnail_ = im.getMusubiThumbnail(original);
id.receivedProfileVersion_ = original.receivedProfileVersion_;
//force some kind of name to be set
if(id.type_ == Authority.Facebook) {
Facebook facebook = getFacebookInstance(activity);
FacebookFriendFetcher fetcher = new FacebookFriendFetcher(facebook);
String name = fetcher.getLoggedinUserName();
byte[] thumbnail = fetcher.getLoggedinUserPhoto();
if(name != null) {
id.name_ = name;
if(id.musubiName_ == null) {
id.musubiName_ = name;
}
}
if(thumbnail != null) {
id.thumbnail_ = thumbnail;
if(id.musubiThumbnail_ == null) {
id.musubiThumbnail_ = thumbnail;
}
}
} else {
if(id.musubiName_ == null) {
//use the real name extracted from facebook or the local contact
//book if possible
if(id.musubiName_ == null) {
id.musubiName_ = id.name_;
}
//otherwise just make up something cute
if(id.musubiName_ == null) {
id.musubiName_ = UiUtil.randomFunName();
}
}
if(id.musubiThumbnail_ == null) {
id.musubiThumbnail_ = id.thumbnail_;
}
}
}
}
View.OnClickListener mGoogleClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Account[] accounts = AccountManager.get(mActivity)
.getAccountsByType(ACCOUNT_TYPE_GOOGLE);
if (accounts == null) {
accounts = new Account[0];
}
((InstrumentedActivity)mActivity).showDialog(
GoogleAccountPickerDialog.newInstance(AccountLinkDialog.this, accounts));
}
};
static Facebook facebook = new Facebook(FACEBOOK_APP_ID);
public static Facebook getFacebookInstance(Context context) {
// Load the current instance if available
SessionStore.restore(facebook, context);
return facebook;
}
/*
* Refresh the current known Facebook token asynchronously.
*/
public static void refreshFacebookToken(final Context context) {
new Thread() {
@Override
public void run() {
// Get a new instance if the current one is null
final Facebook facebook = getFacebookInstance(context);
// Extend the token as needed
facebook.extendAccessTokenIfNeeded(context, new ServiceListener() {
@Override
public void onComplete(Bundle values) {
// If the token retrieval was successful, report it and save the state
long expiration = values.getLong(Facebook.EXPIRES);
Log.i(TAG, "New Facebook token expiration time: " + expiration);
SessionStore.save(facebook, context);
}
@Override
public void onFacebookError(FacebookError e) {
Log.i(TAG, "Facebook API error", e);
}
@Override
public void onError(Error e) {
Log.w(TAG, "Error on call to Facebook", e);
}
});
}
}.start();
}
public void postActivityToFeed() {
SharedPreferences p = mActivity.getSharedPreferences(SettingsActivity.PREFS_NAME, 0);
if (p.getBoolean(SettingsActivity.PREF_ALREADY_SAW_FACEBOOK_POST, false)) {
return;
}
p.edit().putBoolean(SettingsActivity.PREF_ALREADY_SAW_FACEBOOK_POST, true).commit();
Bundle post = new Bundle();
post.putString("picture", "https://lh5.ggpht.com/hRTJJv7H9dpLXhHTTqiiNY2DD2wWO0hZFWEWPv1g-WArcUYLsWk-aQYUS0UgZfVIqtXm=w124");
post.putString("link", "https://market.android.com/details?id=mobisocial.musubi");
post.putString("caption", "Musubi");
post.putString("description", "I'm using Musubi, a social network without the Cloud for smartphones.");
Facebook facebook = getFacebookInstance(mActivity);
facebook.dialog(mActivity, "feed", post, new Facebook.DialogListener() {
@Override
public void onComplete(Bundle values) {
Log.i(TAG, values.toString());
}
@Override
public void onFacebookError(FacebookError e) {
Log.e(TAG, e.toString());
}
@Override
public void onError(DialogError e) {
Log.e(TAG, e.toString());
}
@Override
public void onCancel() {
Log.i(TAG, "User canceled post to Facebook.");
}
});
}
/**
* Returns the currently-active Facebook token for the local user,
* or null if none available.
*/
public static String getActiveFacebookToken(Context context) {
Facebook facebook = getFacebookInstance(context);
if (facebook.isSessionValid()) {
return facebook.getAccessToken();
}
return null;
}
/**
* Returns a current token for the given Google account, or
* null if a token isn't available without user interaction.
*/
public static String silentBlockForGoogleToken(Context context, String accountName)
throws IOException {
Account account = new Account(accountName, ACCOUNT_TYPE_GOOGLE);
AccountManager accountManager = AccountManager.get(context);
// Need to get cached token, invalidate it, then get the token again
String token = blockForCachedGoogleToken(context, account, accountManager);
if (token != null) {
accountManager.invalidateAuthToken(ACCOUNT_TYPE_GOOGLE, token);
}
token = blockForCachedGoogleToken(context, account, accountManager);
return token;
}
private static String blockForCachedGoogleToken(Context context,
Account account, AccountManager accountManager) throws IOException {
AccountManagerFuture<Bundle> future = accountManager.getAuthToken(account,
GOOGLE_OAUTH_SCOPE, true, null, null);
if (future != null) {
try {
Bundle result = future.getResult();
if (result.containsKey(AccountManager.KEY_AUTHTOKEN)) {
String cachedGoogleToken = result.getString(AccountManager.KEY_AUTHTOKEN);
return cachedGoogleToken;
}
} catch (IOException e) {
throw e;
} catch (Exception e) {
}
}
return null;
}
private AccountManagerFuture<Bundle> tryGoogleAccount(Context context, String accountName) {
if (accountName == null) {
Log.e(TAG, "No selected Google account.");
return null;
}
Account account = new Account(accountName, ACCOUNT_TYPE_GOOGLE);
AccountManager accountManager = AccountManager.get(context);
return accountManager.getAuthToken(account, GOOGLE_OAUTH_SCOPE,
true, new GoogleAccountManagerCallback(account), null);
}
class GoogleAccountManagerCallback implements AccountManagerCallback<Bundle> {
private Account mAccount;
public GoogleAccountManagerCallback(Account account) {
mAccount = account;
}
@Override
public void run(AccountManagerFuture<Bundle> future) {
String authToken = null;
//callback can happen after the dialog is gone...
try {
Bundle bundle = future.getResult();
authToken = bundle.getString(AccountManager.KEY_AUTHTOKEN);
if (authToken != null) {
Message m = sAccountLooperThread.obtainMessage();
m.what = MSG_ADD_TO_DATABASE;
Job job = new AccountLooperThread.Job();
job.mDialog = AccountLinkDialog.this;
job.mDetails = new AccountDetails(mAccount.name, mAccount.name, mAccount.type, true);
m.obj = job;
sAccountLooperThread.sendMessage(m);
} else if (bundle.containsKey(AccountManager.KEY_INTENT)) {
Intent intent = bundle.getParcelable(AccountManager.KEY_INTENT);
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK);
mPendingAccountType = mAccount.type;
mPendingAccountName = mAccount.name;
startActivityForResult(intent, REQUEST_GOOGLE_AUTHENTICATE);
} else {
// handle errors in one block
throw new Exception();
}
} catch (Exception e) {
if (mAccount.type != null) {
toast("Failed to connect.");
mAccountAdapter.setAccountStatus(mAccount.name, mAccount.type, AccountStatus.ERROR);
} else {
toast("Failed to connect " + mAccount.name + ".");
}
Log.i(TAG, "Invalidating auth token from error " + authToken);
try {
AccountManager accountManager = AccountManager.get(mActivity);
accountManager.invalidateAuthToken(mAccount.type, authToken);
} catch (Exception e2) {
}
}
}
};
View.OnClickListener mFacebookClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
Facebook facebook = getFacebookInstance(mActivity);
if (!facebook.isSessionValid()) {
facebook.authorize(mActivity, FACEBOOK_PERMISSIONS,
REQUEST_FACEBOOK, mFacebookCallback);
}
}
};
Facebook.DialogListener mFacebookCallback = new Facebook.DialogListener() {
@Override
public void onFacebookError(FacebookError e) {
Log.e(TAG, "Facebook error", e);
toast("Error connecting to Facebook.");
}
@Override
public void onError(DialogError e) {
Log.e(TAG, "error", e);
toast("Error connecting to Facebook.");
}
class FacebookLoadMyProfileTask extends AsyncTask<Void, Void, Void> {
final Facebook facebook = getFacebookInstance(mActivity);
Bundle mValues;
Throwable mError = null;
FacebookLoadMyProfileTask(Bundle values) {
mValues = values;
}
@Override
protected Void doInBackground(Void... params) {
facebook.setAccessToken(mValues.getString(Facebook.TOKEN));
facebook.setAccessExpiresIn(mValues.getString(Facebook.EXPIRES));
try {
JSONObject json = Util.parseJson(facebook.request("me"));
String userId = json.getString("id");
String accountName = json.getString("email");
//String name = json.getString("name");
Log.d(TAG, "Facebook success");
SessionStore.save(facebook, mActivity);
Message m = sAccountLooperThread.obtainMessage();
m.what = MSG_ADD_TO_DATABASE;
Job job = new AccountLooperThread.Job();
job.mDialog = AccountLinkDialog.this;
job.mDetails = new AccountDetails(userId, accountName, ACCOUNT_TYPE_FACEBOOK, true);
m.obj = job;
sAccountLooperThread.sendMessage(m);
mActivity.getContentResolver().notifyChange(MusubiService.FACEBOOK_FRIEND_REFRESH, null);
} catch (JSONException e) {
Log.e(TAG, "JSONException", e);
} catch (Throwable e) {
Log.e(TAG, "Failed to log in with facebook", e);
mError = e;
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if(mError != null)
toast("Couldn't connect to Facebook.");
else
postActivityToFeed();
};
}
@Override
public void onComplete(final Bundle values) {
new FacebookLoadMyProfileTask(values).execute();
}
@Override
public void onCancel() {
Log.i(TAG, "user cancelled facebook auth");
}
};
View.OnClickListener mPhoneClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
// Show the phone number from the API if there is a valid one
TelephonyManager tMgr = (TelephonyManager)mActivity.getSystemService(Context.TELEPHONY_SERVICE);
Set<Account> accounts = new HashSet<Account>();
String phoneNumber = tMgr.getLine1Number();
if (phoneNumber != null && validatePhoneNumber(phoneNumber) != null) {
Log.d(TAG, "Phone number: " + phoneNumber);
accounts.add(new Account(validatePhoneNumber(phoneNumber), ACCOUNT_TYPE_PHONE));
}
// Also show any previously claimed phone numbers
MMyAccount[] claimedAccounts = mAccountManager.getClaimedAccounts(ACCOUNT_TYPE_PHONE);
for (MMyAccount acc : claimedAccounts) {
accounts.add(new Account(acc.accountName_, acc.accountType_));
}
((InstrumentedActivity)mActivity).showDialog(
PhoneNumberPickerDialog.newInstance(AccountLinkDialog.this, accounts));
}
};
public static class PhoneNumberPickerDialog extends DialogFragment
implements DialogInterface.OnClickListener {
private static final String ENTER_ALTERNATE_NUMBER = "Enter a phone number";
Activity mActivity;
AccountLinkDialog mTarget;
public static PhoneNumberPickerDialog newInstance(AccountLinkDialog target,
Set<Account> accounts) {
PhoneNumberPickerDialog frag = new PhoneNumberPickerDialog();
Bundle args = new Bundle();
String[] accountNames = new String[accounts.size() + 1];
int i = 0;
for (Account a : accounts) {
accountNames[i++] = a.name;
}
accountNames[i++] = ENTER_ALTERNATE_NUMBER;
args.putStringArray("accountNames", accountNames);
frag.setArguments(args);
frag.setTargetFragment(target, REQUEST_PHONE_NUMBER);
return frag;
}
public PhoneNumberPickerDialog() {
super();
}
@Override
public void onAttach(SupportActivity activity) {
super.onAttach(activity);
mActivity = activity.asActivity();
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(mActivity)
.setTitle("Connect Phone Number")
.setItems(getArguments().getStringArray("accountNames"), this)
.create();
}
@Override
public void onClick(DialogInterface dialog, int which) {
String account = getArguments().getStringArray("accountNames")[which];
if(account.equals(ENTER_ALTERNATE_NUMBER)) {
// TODO: this dialog comes back, but it shouldn't
dismiss();
((InstrumentedActivity)mActivity).showDialog(
EnterPhoneNumberDialog.newInstance(
(AccountLinkDialog)getTargetFragment()));
} else {
Intent data = new Intent();
data.putExtra(AccountManager.KEY_ACCOUNT_NAME, account);
getTargetFragment().onActivityResult(REQUEST_PHONE_NUMBER, Activity.RESULT_OK, data);
dismiss();
}
}
}
public static class EnterPhoneNumberDialog extends DialogFragment
implements View.OnClickListener {
Activity mActivity;
Dialog mDialog;
public static EnterPhoneNumberDialog newInstance(AccountLinkDialog target) {
EnterPhoneNumberDialog frag = new EnterPhoneNumberDialog();
frag.setTargetFragment(target, REQUEST_PHONE_NUMBER);
return frag;
}
public EnterPhoneNumberDialog() {
super();
}
@Override
public void onAttach(SupportActivity activity) {
super.onAttach(activity);
mActivity = activity.asActivity();Dialog numberDlg = new Dialog(mActivity);
numberDlg.setContentView(R.layout.phone_number_dialog);
numberDlg.setTitle("Enter a Phone Number");
Button go = (Button)numberDlg.findViewById(R.id.button1);
go.setOnClickListener(this);
mDialog = numberDlg;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return mDialog;
}
@Override
public void onClick(View v) {
EditText country = (EditText)mDialog.findViewById(R.id.editText1);
EditText primary = (EditText)mDialog.findViewById(R.id.editText2);
String number = country.getText().toString() + primary.getText().toString();
Log.d(TAG, "Entered number: " + number);
if (number != "") {
Intent data = new Intent();
data.putExtra(AccountManager.KEY_ACCOUNT_NAME, number);
getTargetFragment().onActivityResult(REQUEST_PHONE_NUMBER, Activity.RESULT_OK, data);
}
dismiss();
}
}
public static class GoogleAccountPickerDialog extends DialogFragment
implements DialogInterface.OnClickListener {
private static final String REGISTER_AN_ACCOUNT = "Use other email address via Google";
private static final String ADD_AN_ACCOUNT = "Add a Google account";
public static GoogleAccountPickerDialog newInstance(AccountLinkDialog target,
Account[] accounts) {
GoogleAccountPickerDialog frag = new GoogleAccountPickerDialog();
Bundle args = new Bundle();
String[] accountNames = new String[accounts.length + 2];
int i = 0;
for (Account a : accounts) {
accountNames[i++] = a.name;
}
accountNames[i++] = ADD_AN_ACCOUNT;
accountNames[i++] = REGISTER_AN_ACCOUNT;
args.putStringArray("accountNames", accountNames);
frag.setArguments(args);
frag.setTargetFragment(target, REQUEST_GOOGLE_ACCOUNT);
return frag;
}
public GoogleAccountPickerDialog() {
}
Activity mActivity;
@Override
public void onAttach(SupportActivity activity) {
super.onAttach(activity);
mActivity = activity.asActivity();
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(mActivity)
.setTitle("Connect Account")
.setItems(getArguments().getStringArray("accountNames"), this)
.create();
}
@Override
public void onClick(DialogInterface dialog, int which) {
String account = getArguments().getStringArray("accountNames")[which];
if(account.equals(ADD_AN_ACCOUNT)) {
try {
Intent intent = new Intent();
intent.setClassName( "com.google.android.gsf", "com.google.android.gsf.login.AccountIntroActivity" );
mActivity.startActivity( intent );
dismiss();
return;
} catch(Throwable t) {}
try {
Intent intent = new Intent(Settings.ACTION_ADD_ACCOUNT);
mActivity.startActivity( intent );
dismiss();
return;
} catch(Throwable t) {}
Toast.makeText(mActivity, "Failed to invoke Google account services", Toast.LENGTH_SHORT).show();
dismiss();
return;
} else if(account.equals(REGISTER_AN_ACCOUNT)) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://accounts.google.com/NewAccount"));
mActivity.startActivity( intent );
dismiss();
return;
} else {
Intent data = new Intent();
data.putExtra(EXTRA_ACCOUNT, account);
getTargetFragment().onActivityResult(REQUEST_GOOGLE_ACCOUNT, Activity.RESULT_OK, data);
}
}
}
/*
* In some cases, the user may want Musubi to resend text verification
*/
public static class RetryPhoneDialog extends DialogFragment {
Set<MPendingIdentity> mPendingIdents;
PendingIdentityManager mManager;
Activity mActivity;
public static RetryPhoneDialog newInstance(
PendingIdentityManager manager, Set<MPendingIdentity> pendingIdents) {
RetryPhoneDialog d = new RetryPhoneDialog(manager, pendingIdents);
return d;
}
public RetryPhoneDialog(PendingIdentityManager manager, Set<MPendingIdentity> pendingIdents) {
super();
mPendingIdents = pendingIdents;
}
@Override
public void onAttach(SupportActivity activity) {
super.onAttach(activity);
mActivity = activity.asActivity();
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(mActivity)
.setTitle("Resend Verification")
.setMessage("Would you like Musubi to resend the verification text?")
.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
for (MPendingIdentity pendingIdent : mPendingIdents) {
if (pendingIdent.notified_) {
pendingIdent.notified_ = false;
mManager.updateIdentity(pendingIdent);
}
}
mActivity.getContentResolver().notifyChange(MusubiService.AUTH_TOKEN_REFRESH, null);
dismiss();
}
})
.setNegativeButton("No", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dismiss();
}
})
.create();
}
}
private String validatePhoneNumber(String original) {
// Strip non-numeric characters and leading zeros
original.replaceAll("[^\\d]", "");
long numerical = Long.parseLong(original);
// Check to make sure the length is feasible
String converted = Long.toString(numerical);
if (converted.length() < SHORTEST_PHONE_NUMBER ||
converted.length() > LONGEST_PHONE_NUMBER) {
return null;
}
else {
return "+" + converted;
}
}
private void setupPhoneAccount(String phoneNumber) {
// See if this phone number is known, and add it if it isn't
Log.d(TAG, "Setting up " + phoneNumber);
IBIdentity toAdd = new IBIdentity(Authority.PhoneNumber, phoneNumber, 0);
SQLiteOpenHelper databaseSource = App.getDatabaseSource(mActivity);
IdentitiesManager im = new IdentitiesManager(databaseSource);
MIdentity id = im.getIdentityForIBHashedIdentity(toAdd);
if (id == null) {
id = new MIdentity();
id.claimed_ = false;
id.owned_ = false;
id.whitelisted_ = true;
id.hasSentEmail_ = true;
//set up the identity data
id.principal_ = toAdd.principal_;
id.type_ = toAdd.authority_;
id.principalHash_ = toAdd.hashed_;
id.principalShortHash_ = mobisocial.musubi.util.Util.shortHash(toAdd.hashed_);
im.insertIdentity(id);
}
// Get the corresponding pending identity
PendingIdentityManager pManager = new PendingIdentityManager(databaseSource);
Set<MPendingIdentity> pendingIdents = pManager.lookupIdentities(id.id_);
if (!id.owned_) {
// Add this account so that it can be tracked
Message m = sAccountLooperThread.obtainMessage();
m.what = MSG_ADD_TO_DATABASE;
Job job = new AccountLooperThread.Job();
job.mDialog = AccountLinkDialog.this;
job.mDetails = new AccountDetails(toAdd.principal_, toAdd.principal_, ACCOUNT_TYPE_PHONE, id.owned_);
m.obj = job;
sAccountLooperThread.sendMessage(m);
MPendingIdentity pendingIdent = pManager.lookupIdentity(id.id_, toAdd.temporalFrame_);
if (pendingIdent == null) {
pendingIdent = pManager.fillPendingIdentity(id.id_, toAdd.temporalFrame_);
pManager.insertIdentity(pendingIdent);
}
pendingIdents.add(pendingIdent);
}
boolean anyUnnotified = false;
for (MPendingIdentity pident : pendingIdents) {
if (!pident.notified_) {
anyUnnotified = true;
}
}
// If any of these are unnotified, ask to resend
if (!anyUnnotified) {
((InstrumentedActivity)mActivity).showDialog(
RetryPhoneDialog.newInstance(pManager, pendingIdents));
}
else {
// Start the verification process
mActivity.getContentResolver().notifyChange(MusubiService.AUTH_TOKEN_REFRESH, null);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (data == null) {
return;
}
switch (requestCode) {
case REQUEST_GOOGLE_ACCOUNT:
if (resultCode == Activity.RESULT_OK) {
String account = data.getStringExtra(EXTRA_ACCOUNT);
tryGoogleAccount(mActivity, account);
}
break;
case REQUEST_GOOGLE_AUTHENTICATE:
//this doesnt really seem to be called because of the account manager having a
//weird api
if (resultCode == Activity.RESULT_OK) {
String accountName = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
tryGoogleAccount(mActivity, accountName);
}
break;
case REQUEST_FACEBOOK:
Log.d(TAG, "Authorizing Facebook callback");
Facebook facebook = getFacebookInstance(mActivity);
facebook.authorizeCallback(requestCode, resultCode, data);
break;
case REQUEST_PHONE_NUMBER:
if (resultCode == Activity.RESULT_OK) {
String phoneNumber = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME);
phoneNumber = validatePhoneNumber(phoneNumber);
setupPhoneAccount(phoneNumber);
}
break;
}
}
static class AccountLooperThread extends Thread {
private Handler mHandler;
static class Job {
public long mId;
protected AccountDetails mDetails;
AccountLinkDialog mDialog;
String mAccount;
}
public void run() {
Looper.prepare();
mHandler = new Handler() {
public void handleMessage(Message msg) {
final Job job = (Job)msg.obj;
switch (msg.what) {
case MSG_CONNECT_GOOGLE:
String account = job.mAccount;
String token = null;
try {
token = silentBlockForGoogleToken(job.mDialog.mActivity, account);
} catch (IOException e) {
Log.i(TAG, "Could not get a Google token likely due to a network error");
}
AccountStatus status = (token == null) ?
AccountStatus.ERROR : AccountStatus.CONNECTED;
job.mDialog.mAccountAdapter.setAccountStatus(account, ACCOUNT_TYPE_GOOGLE, status);
break;
case MSG_CONNECT_FB:
try {
// Make sure we can connect, but we don't care about the result.
Facebook facebook = getFacebookInstance(job.mDialog.mActivity);
Util.parseJson(facebook.request("me"));
job.mDialog.mAccountAdapter.setAccountStatus(job.mId, AccountStatus.CONNECTED);
} catch(Throwable e) {
Log.e(TAG, "silent facebook error", e);
job.mDialog.mAccountAdapter.setAccountStatus(job.mId, AccountStatus.ERROR);
}
break;
case MSG_CONNECT_PHONE:
SQLiteOpenHelper databaseSource = App.getDatabaseSource(job.mDialog.mActivity);
IdentitiesManager im = new IdentitiesManager(databaseSource);
MIdentity mid = im.getIdentityForIBHashedIdentity(
new IBIdentity(Authority.PhoneNumber, job.mAccount, 0));
if (mid != null) {
PendingIdentityManager pManager = new PendingIdentityManager(databaseSource);
int unnotified = pManager.getUnnotifiedIdentities(mid.id_).size();
if (mid.owned_ && unnotified == 0) {
job.mDialog.mAccountAdapter.setAccountStatus(job.mId, AccountStatus.CONNECTED);
} else if (unnotified == 0) {
job.mDialog.mAccountAdapter.setAccountStatus(job.mId, AccountStatus.ERROR);
} else {
job.mDialog.mAccountAdapter.setAccountStatus(job.mId, AccountStatus.UNVERIFIED);
}
}
break;
case MSG_ADD_TO_DATABASE:
final Activity activity = job.mDialog.mActivity;
final boolean previouslyOwned = new IdentitiesManager(
App.getDatabaseSource(activity)).hasConnectedAccounts();
final AccountDetails details = job.mDetails;
final MMyAccount dbRow = AccountLinkDialog.addAccountToDatabase(
activity, details);
// Update ui
if (dbRow != null) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
job.mDialog.mAccountAdapter.add(dbRow);
if (details.owned) {
job.mDialog.mAccountAdapter.setAccountStatus(dbRow.id_,
AccountStatus.CONNECTED);
} else {
// In case an account is added without knowing if we
// can get user keys for it
job.mDialog.mAccountAdapter.setAccountStatus(dbRow.id_,
AccountStatus.UNVERIFIED);
}
if (!previouslyOwned) {
new AddressBookUpdateHandler.AddressBookImportTask(activity).execute();
}
}
});
}
}
}
};
synchronized (this) {
notify();
}
Looper.loop();
}
public Message obtainMessage() {
while (mHandler == null) {
try {
// watch for startup race condition
synchronized (this) {
wait(50);
}
} catch (InterruptedException e) {}
}
return mHandler.obtainMessage();
}
public void sendMessage(Message msg) {
mHandler.sendMessage(msg);
}
}
class AccountAdapter extends ArrayAdapter<MMyAccount> {
final Context mContext;
final Map<Long, AccountStatus> mAccountStatus;
final TLongSet mKnownAccounts;
public AccountAdapter(Context context) {
super(context, android.R.layout.simple_list_item_1);
mContext = context;
mAccountStatus = new HashMap<Long, AccountStatus>();
mKnownAccounts = new TLongHashSet();
}
@Override
public View getView(int position, View view, ViewGroup parent) {
if (view == null) {
view = getAccountView();
}
MMyAccount account = getItem(position);
int iconResource = R.drawable.icon;
if (ACCOUNT_TYPE_GOOGLE.equals(account.accountType_)) {
iconResource = R.drawable.google;
} else if (ACCOUNT_TYPE_FACEBOOK.equals(account.accountType_)) {
iconResource = R.drawable.facebook;
}
((ImageView)view.findViewById(R.id.icon)).setImageResource(iconResource);
((TextView)view.findViewById(R.id.text)).setText(account.accountName_);
String status = statusForAccount(account.id_);
((TextView)view.findViewById(R.id.status)).setText(status);
return view;
}
View getAccountView() {
LinearLayout frame = new LinearLayout(mContext);
frame.setOrientation(LinearLayout.HORIZONTAL);
ImageView icon = new ImageView(mContext);
int size = 60;
icon.setLayoutParams(new LinearLayout.LayoutParams(size, size));
icon.setId(R.id.icon);
icon.setPadding(3, 6, 6, 0);
LinearLayout accountView = new LinearLayout(mContext);
accountView.setOrientation(LinearLayout.VERTICAL);
TextView label = new TextView(mContext);
label.setTextSize(20);
label.setId(R.id.text);
accountView.addView(label);
TextView status = new TextView(mContext);
status.setTextSize(14);
status.setId(R.id.status);
accountView.addView(status);
frame.addView(icon);
frame.addView(accountView);
return frame;
}
public synchronized void setAccountStatus(long accountId, AccountStatus status) {
mAccountStatus.put(accountId, status);
mActivity.runOnUiThread(new Runnable() {
@Override
public void run() {
notifyDataSetChanged();
}
});
}
@Override
public synchronized void add(MMyAccount account) {
if (!mKnownAccounts.contains(account.id_)) {
mKnownAccounts.add(account.id_);
super.add(account);
}
};
public synchronized void setAccountStatus(String accountName, String accountType, AccountStatus status) {
MMyAccount account = mAccountManager.lookupAccount(accountName, accountType);
if (account == null) {
Log.e(TAG, "Could not find account " + accountName + "/" + accountType);
return;
}
setAccountStatus(account.id_, status);
}
String statusForAccount(long accountId) {
AccountStatus val = mAccountStatus.get(accountId);
if (val == null) {
return TEXT_CHECKING;
}
switch (val) {
case CONNECTED:
return TEXT_CONNECTED;
case ERROR:
return TEXT_ERROR_CONNECTING;
case UNVERIFIED:
return TEXT_UNVERIFIED;
case PENDING:
default:
return TEXT_CHECKING;
}
}
}
void toast(final String text) {
Toast.makeText(mActivity, text, Toast.LENGTH_SHORT).show();
}
}