package org.wikipedia.login;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.design.widget.PasswordTextInput;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import com.mobsandgeeks.saripaar.ValidationError;
import com.mobsandgeeks.saripaar.Validator;
import com.mobsandgeeks.saripaar.annotation.Pattern;
import org.wikipedia.R;
import org.wikipedia.WikipediaApp;
import org.wikipedia.activity.ThemedActionBarActivity;
import org.wikipedia.analytics.LoginFunnel;
import org.wikipedia.auth.AccountUtil;
import org.wikipedia.createaccount.CreateAccountActivity;
import org.wikipedia.readinglist.sync.ReadingListSynchronizer;
import org.wikipedia.util.FeedbackUtil;
import org.wikipedia.util.log.L;
import org.wikipedia.views.NonEmptyValidator;
import org.wikipedia.views.WikiErrorView;
import java.util.List;
import static org.wikipedia.util.DeviceUtil.hideSoftKeyboard;
import static org.wikipedia.util.FeedbackUtil.setErrorPopup;
public class LoginActivity extends ThemedActionBarActivity {
public static final int RESULT_LOGIN_SUCCESS = 1;
public static final int RESULT_LOGIN_FAIL = 2;
public static final String LOGIN_REQUEST_SOURCE = "login_request_source";
public static final String EDIT_SESSION_TOKEN = "edit_session_token";
public static final String ACTION_CREATE_ACCOUNT = "action_create_account";
@Pattern(regex = "[^#<>\\[\\]|{}\\/@]*", messageResId = R.string.create_account_username_error)
private EditText usernameText;
private EditText passwordText;
private EditText twoFactorText;
private View loginButton;
private ProgressDialog progressDialog;
private WikiErrorView errorView;
@Nullable private String firstStepToken;
private LoginFunnel funnel;
private String loginSource;
private LoginClient loginClient;
private boolean wentStraightToCreateAccount;
private Validator validator;
public static Intent newIntent(@NonNull Context context, @NonNull String source) {
return newIntent(context, source, null);
}
public static Intent newIntent(@NonNull Context context, @NonNull String source,
@Nullable String token) {
return new Intent(context, LoginActivity.class)
.putExtra(LOGIN_REQUEST_SOURCE, source)
.putExtra(EDIT_SESSION_TOKEN, token);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
usernameText = (EditText) findViewById(R.id.login_username_text);
passwordText = ((PasswordTextInput) findViewById(R.id.login_password_input)).getEditText();
twoFactorText = (EditText) findViewById(R.id.login_2fa_text);
View createAccountLink = findViewById(R.id.login_create_account_link);
errorView = (WikiErrorView) findViewById(R.id.view_login_error);
errorView.setBackClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onBackPressed();
}
});
errorView.setRetryClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
errorView.setVisibility(View.GONE);
}
});
// We enable the login button as soon as the username and password fields are filled
// Tapping does further validation
validator = new Validator(this);
validator.setValidationListener(new Validator.ValidationListener() {
@Override
public void onValidationSucceeded() {
doLogin();
}
@Override
public void onValidationFailed(List<ValidationError> errors) {
for (ValidationError error : errors) {
View view = error.getView();
String message = error.getCollatedErrorMessage(view.getContext());
if (view instanceof EditText) {
//Request focus on the EditText before setting error, so that error is visible
view.requestFocus();
setErrorPopup((EditText) view, message);
} else {
throw new RuntimeException("This should not be happening");
}
}
}
});
// Don't allow user to attempt login until they've put in a username and password
new NonEmptyValidator(new NonEmptyValidator.ValidationChangedCallback() {
@Override
public void onValidationChanged(boolean isValid) {
loginButton.setEnabled(isValid);
}
}, usernameText, passwordText);
passwordText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (actionId == EditorInfo.IME_ACTION_DONE) {
validator.validate();
return true;
}
return false;
}
});
loginButton = findViewById(R.id.login_button);
loginButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
validator.validate();
}
});
createAccountLink.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startCreateAccountActivity();
}
});
progressDialog = new ProgressDialog(this);
progressDialog.setMessage(getString(R.string.login_in_progress_dialog_message));
progressDialog.setCancelable(false);
funnel = new LoginFunnel(WikipediaApp.getInstance());
loginSource = getIntent().getStringExtra(LOGIN_REQUEST_SOURCE);
if (getIntent().getBooleanExtra(ACTION_CREATE_ACCOUNT, false)) {
wentStraightToCreateAccount = true;
startCreateAccountActivity();
} else if (savedInstanceState == null) {
// Only send the login start log event if the activity is created for the first time
logLoginStart();
}
// Assume no login by default
setResult(RESULT_LOGIN_FAIL);
}
public void showPrivacyPolicy(View v) {
FeedbackUtil.showPrivacyPolicy(this);
}
@Override
protected void setTheme() {
setActionBarTheme();
}
private void logLoginStart() {
if (loginSource.equals(LoginFunnel.SOURCE_EDIT)) {
funnel.logStart(
LoginFunnel.SOURCE_EDIT,
getIntent().getStringExtra(EDIT_SESSION_TOKEN)
);
} else {
funnel.logStart(loginSource);
}
}
private void startCreateAccountActivity() {
funnel.logCreateAccountAttempt();
Intent intent = new Intent(this, CreateAccountActivity.class);
intent.putExtra(CreateAccountActivity.LOGIN_SESSION_TOKEN, funnel.getSessionToken());
intent.putExtra(CreateAccountActivity.LOGIN_REQUEST_SOURCE, loginSource);
startActivityForResult(intent, CreateAccountActivity.ACTION_CREATE_ACCOUNT);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CreateAccountActivity.ACTION_CREATE_ACCOUNT) {
if (wentStraightToCreateAccount) {
logLoginStart();
}
if (resultCode == CreateAccountActivity.RESULT_ACCOUNT_CREATED) {
usernameText.setText(data.getStringExtra(CreateAccountActivity.CREATE_ACCOUNT_RESULT_USERNAME));
passwordText.setText(data.getStringExtra(CreateAccountActivity.CREATE_ACCOUNT_RESULT_PASSWORD));
funnel.logCreateAccountSuccess();
FeedbackUtil.showMessage(this,
R.string.create_account_account_created_toast);
doLogin();
} else {
funnel.logCreateAccountFailure();
}
}
}
private void doLogin() {
final String username = usernameText.getText().toString();
final String password = passwordText.getText().toString();
final String twoFactorCode = twoFactorText.getText().toString();
if (loginClient == null) {
loginClient = new LoginClient();
}
progressDialog.show();
if (!twoFactorCode.isEmpty()) {
loginClient.login(WikipediaApp.getInstance().getWikiSite(), username, password,
twoFactorCode, firstStepToken, getCallback(username, password));
} else {
loginClient.request(WikipediaApp.getInstance().getWikiSite(), username, password,
getCallback(username, password));
}
}
private LoginClient.LoginCallback getCallback(@NonNull final String username,
@NonNull final String password) {
return new LoginClient.LoginCallback() {
@Override
public void success(@NonNull LoginResult result) {
if (!progressDialog.isShowing()) {
// no longer attached to activity!
return;
}
progressDialog.dismiss();
if (result.pass()) {
funnel.logSuccess();
ReadingListSynchronizer.instance().sync();
Bundle extras = getIntent().getExtras();
AccountAuthenticatorResponse response = extras == null
? null
: extras.<AccountAuthenticatorResponse>getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE);
AccountUtil.createAccount(response, username, password);
hideSoftKeyboard(LoginActivity.this);
setResult(RESULT_LOGIN_SUCCESS);
finish();
} else if (result.fail()) {
String message = result.getMessage();
FeedbackUtil.showMessage(LoginActivity.this, message);
funnel.logError(message);
L.w("Login failed with result " + message);
}
}
@Override
public void twoFactorPrompt(@NonNull Throwable caught, @NonNull String token) {
if (!progressDialog.isShowing()) {
// no longer attached to activity!
return;
}
progressDialog.dismiss();
firstStepToken = token;
twoFactorText.setVisibility(View.VISIBLE);
twoFactorText.requestFocus();
FeedbackUtil.showError(LoginActivity.this, caught);
}
@Override
public void error(@NonNull Throwable caught) {
if (!progressDialog.isShowing()) {
// no longer attached to activity!
return;
}
progressDialog.dismiss();
if (caught instanceof LoginClient.LoginFailedException) {
FeedbackUtil.showError(LoginActivity.this, caught);
} else {
showError(caught);
}
}
};
}
@Override
public void onBackPressed() {
hideSoftKeyboard(this);
super.onBackPressed();
}
@Override
public void onStop() {
if (progressDialog.isShowing()) {
progressDialog.dismiss();
}
super.onStop();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("loginShowing", true);
}
private void showError(@NonNull Throwable caught) {
errorView.setError(caught);
errorView.setVisibility(View.VISIBLE);
}
}