package fr.pagesjaunes.mdm.authenticator;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import com.parse.ParseException;
import com.parse.ParseUser;
import com.parse.SaveCallback;
import com.squareup.otto.Bus;
import com.squareup.otto.Subscribe;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import butterknife.Bind;
import butterknife.ButterKnife;
import fr.pagesjaunes.mdm.Injector;
import fr.pagesjaunes.mdm.R;
import fr.pagesjaunes.mdm.R.id;
import fr.pagesjaunes.mdm.R.layout;
import fr.pagesjaunes.mdm.R.string;
import fr.pagesjaunes.mdm.core.BootstrapService;
import fr.pagesjaunes.mdm.core.Constants;
import fr.pagesjaunes.mdm.events.UnAuthorizedErrorEvent;
import fr.pagesjaunes.mdm.ui.TextWatcherAdapter;
import fr.pagesjaunes.mdm.util.Ln;
import fr.pagesjaunes.mdm.util.SafeAsyncTask;
import fr.pagesjaunes.mdm.wishlist.Toaster;
import retrofit.RetrofitError;
import static android.R.layout.simple_dropdown_item_1line;
import static android.accounts.AccountManager.KEY_ACCOUNT_NAME;
import static android.accounts.AccountManager.KEY_ACCOUNT_TYPE;
import static android.accounts.AccountManager.KEY_AUTHTOKEN;
import static android.accounts.AccountManager.KEY_BOOLEAN_RESULT;
import static android.view.KeyEvent.ACTION_DOWN;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
/**
* Activity to authenticate the user against an API (example API on Parse.com)
*/
public class BootstrapAuthenticatorActivity extends ActionBarAccountAuthenticatorActivity {
/**
* PARAM_CONFIRM_CREDENTIALS
*/
public static final String PARAM_CONFIRM_CREDENTIALS = "confirmCredentials";
/**
* PARAM_PASSWORD
*/
public static final String PARAM_PASSWORD = "password";
/**
* PARAM_USERNAME
*/
public static final String PARAM_USERNAME = "username";
/**
* PARAM_AUTHTOKEN_TYPE
*/
public static final String PARAM_AUTHTOKEN_TYPE = "authtokenType";
@Bind(id.et_username) protected AutoCompleteTextView usernameText;
@Bind(id.et_email) protected AutoCompleteTextView emailText;
@Bind(id.et_password) protected EditText passwordText;
@Bind(id.b_signin) protected Button signInButton;
private final TextWatcher watcher = validationTextWatcher();
@Bind(id.b_signup_ui) protected Button signUpUIButton;
@Bind(id.b_signup) protected Button signUpButton;
private final TextWatcher signupWatcher = signupTextWatcher();
/**
* Was the original caller asking for an entirely new account?
*/
protected boolean requestNewAccount = false;
@Inject BootstrapService bootstrapService;
@Inject Bus bus;
private AccountManager accountManager;
private SafeAsyncTask<Boolean> authenticationTask;
private String authToken;
private String authTokenType;
/**
* If set we are just checking that the user knows their credentials; this
* doesn't cause the user's password to be changed on the device.
*/
private Boolean confirmCredentials = false;
private String email;
private String password;
/**
* In this instance the token is simply the sessionId returned from Parse.com. This could be a
* oauth token or some other type of timed token that expires/etc. We're just using the parse.com
* sessionId to prove the example of how to utilize a token.
*/
private String token;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
Injector.inject(this);
accountManager = AccountManager.get(this);
final Intent intent = getIntent();
email = intent.getStringExtra(PARAM_USERNAME);
authTokenType = intent.getStringExtra(PARAM_AUTHTOKEN_TYPE);
confirmCredentials = intent.getBooleanExtra(PARAM_CONFIRM_CREDENTIALS, false);
requestNewAccount = email == null;
setContentView(layout.login_activity);
ButterKnife.bind(this);
emailText.setAdapter(new ArrayAdapter<String>(this, simple_dropdown_item_1line, userEmailAccounts()));
passwordText.setOnKeyListener(new OnKeyListener() {
public boolean onKey(final View v, final int keyCode, final KeyEvent event) {
if (event != null && ACTION_DOWN == event.getAction()
&& keyCode == KEYCODE_ENTER && signInButton.isEnabled()) {
handleLogin(signInButton);
return true;
}
return false;
}
});
passwordText.setOnEditorActionListener(new OnEditorActionListener() {
public boolean onEditorAction(final TextView v, final int actionId,
final KeyEvent event) {
if (actionId == IME_ACTION_DONE && signInButton.isEnabled()) {
handleLogin(signInButton);
return true;
}
return false;
}
});
usernameText.addTextChangedListener(watcher);
passwordText.addTextChangedListener(watcher);
final TextView signUpText = (TextView) findViewById(id.tv_signup);
signUpText.setMovementMethod(LinkMovementMethod.getInstance());
signUpText.setText(Html.fromHtml(getString(string.signup_link)));
}
private List<String> userEmailAccounts() {
final Account[] accounts = accountManager.getAccountsByType("com.google");
final List<String> emailAddresses = new ArrayList<String>(accounts.length);
for (final Account account : accounts) {
emailAddresses.add(account.name);
}
return emailAddresses;
}
private TextWatcher validationTextWatcher() {
return new TextWatcherAdapter() {
public void afterTextChanged(final Editable gitDirEditText) {
updateUIWithValidation();
}
};
}
private TextWatcher signupTextWatcher() {
return new TextWatcherAdapter() {
public void afterTextChanged(final Editable gitDirEditText) {
updateUISignupWithValidation();
}
};
}
@Override
protected void onPause() {
super.onPause();
bus.unregister(this);
}
@Override
protected void onResume() {
super.onResume();
bus.register(this);
updateUIWithValidation();
}
private void updateUIWithValidation() {
final boolean populated = populated(usernameText) && populated(passwordText);
signInButton.setEnabled(populated);
}
private void updateUISignupWithValidation() {
final boolean populated = populated(usernameText) && populated(passwordText) && populated(emailText);
signUpButton.setEnabled(populated);
}
private boolean populated(final EditText editText) {
return editText.length() > 0;
}
@Override
protected Dialog onCreateDialog(int id) {
final ProgressDialog dialog = new ProgressDialog(this);
dialog.setMessage(getText(string.message_signing_in));
dialog.setIndeterminate(true);
dialog.setCancelable(true);
dialog.setOnCancelListener(new DialogInterface.OnCancelListener()
{
public void onCancel(final DialogInterface dialog)
{
if (authenticationTask != null)
{
authenticationTask.cancel(true);
}
}
});
return dialog;
}
@Subscribe
public void onUnAuthorizedErrorEvent(UnAuthorizedErrorEvent unAuthorizedErrorEvent) {
// Could not authorize for some reason.
Toaster.showLong(BootstrapAuthenticatorActivity.this, R.string.message_bad_credentials);
}
public void handleSignupUI(final View view)
{
View signup = ButterKnife.findById((View) view.getParent(),R.id.v_signup);
usernameText.addTextChangedListener(signupWatcher);
passwordText.addTextChangedListener(signupWatcher);
emailText.addTextChangedListener(signupWatcher);
signup.setVisibility(View.VISIBLE);
updateUISignupWithValidation();
signUpUIButton.setVisibility(View.GONE);
signInButton.setVisibility(View.GONE);
}
public void handleSignup(final View view)
{
showProgress();
ParseUser user = ParseUser.getCurrentUser();
user.setUsername(usernameText.getText().toString());
user.setPassword(passwordText.getText().toString());
user.setEmail(emailText.getText().toString());
user.saveInBackground(new SaveCallback()
{
public void done(ParseException e)
{
if (e == null)
{
// Hooray! Let them use the app now.
Ln.d("Hooray!");
handleLogin(view);
}
else
{
// Sign up didn't succeed. Look at the ParseException
// to figure out what went wrong
e.printStackTrace();
}
}
});
}
/**
* Handles onClick event on the Submit button. Sends username/password to
* the server for authentication.
* <p/>
* Specified by android:onClick="handleLogin" in the layout xml
*
* @param view
*/
public void handleLogin(final View view) {
if (authenticationTask != null) {
return;
}
if (requestNewAccount) {
email = usernameText.getText().toString();
}
password = passwordText.getText().toString();
showProgress();
authenticationTask = new SafeAsyncTask<Boolean>() {
public Boolean call() throws Exception {
bootstrapService.authenticate(email, password);
ParseUser loginResponse = ParseUser.getCurrentUser();
token = loginResponse.getSessionToken();
return true;
}
@Override
public void onSuccess(final Boolean authSuccess) {
onAuthenticationResult(authSuccess);
}
@Override
protected void onException(final Exception e) throws RuntimeException {
// Retrofit Errors are handled inside of the {
if(!(e instanceof RetrofitError)) {
final Throwable cause = e.getCause() != null ? e.getCause() : e;
if(cause != null) {
Toaster.showLong(BootstrapAuthenticatorActivity.this, cause.getMessage());
Ln.d("onException: failed to authenticate :%s", cause.getLocalizedMessage());
}
}
}
@Override
protected void onFinally() throws RuntimeException {
hideProgress();
authenticationTask = null;
}
};
authenticationTask.execute();
}
/**
* Called when response is received from the server for confirm credentials
* request. See onAuthenticationResult(). Sets the
* AccountAuthenticatorResult which is sent back to the caller.
*
* @param result
*/
protected void finishConfirmCredentials(final boolean result) {
final Account account = new Account(email, Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE);
accountManager.setPassword(account, password);
final Intent intent = new Intent();
intent.putExtra(KEY_BOOLEAN_RESULT, result);
setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK, intent);
finish();
}
/**
* Called when response is received from the server for authentication
* request. See onAuthenticationResult(). Sets the
* AccountAuthenticatorResult which is sent back to the caller. Also sets
* the authToken in AccountManager for this account.
*/
protected void finishLogin() {
final Account account = new Account(email, Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE);
if (requestNewAccount) {
accountManager.addAccountExplicitly(account, password, null);
} else {
accountManager.setPassword(account, password);
}
authToken = token;
final Intent intent = new Intent();
intent.putExtra(KEY_ACCOUNT_NAME, email);
intent.putExtra(KEY_ACCOUNT_TYPE, Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE);
if (authTokenType != null
&& authTokenType.equals(Constants.Auth.AUTHTOKEN_TYPE)) {
intent.putExtra(KEY_AUTHTOKEN, authToken);
}
setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK, intent);
finish();
}
/**
* Hide progress dialog
*/
@SuppressWarnings("deprecation")
protected void hideProgress() {
dismissDialog(0);
}
/**
* Show progress dialog
*/
@SuppressWarnings("deprecation")
protected void showProgress() {
showDialog(0);
}
/**
* Called when the authentication process completes (see attemptLogin()).
*
* @param result
*/
public void onAuthenticationResult(final boolean result) {
if (result) {
if (!confirmCredentials) {
finishLogin();
} else {
finishConfirmCredentials(true);
}
} else {
Ln.d("onAuthenticationResult: failed to authenticate");
if (requestNewAccount) {
Toaster.showLong(BootstrapAuthenticatorActivity.this,
string.message_auth_failed_new_account);
} else {
Toaster.showLong(BootstrapAuthenticatorActivity.this,
string.message_auth_failed);
}
}
}
}