diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 57a369bad..ee4176cae 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -1,15 +1,9 @@ package fr.free.nrw.commons.auth; -import android.accounts.Account; import android.accounts.AccountAuthenticatorActivity; -import android.accounts.AccountAuthenticatorResponse; -import android.accounts.AccountManager; -import android.app.Activity; import android.app.ProgressDialog; -import android.content.ContentResolver; import android.content.Intent; import android.content.SharedPreferences; -import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.NavUtils; import android.text.Editable; @@ -21,18 +15,11 @@ import android.view.inputmethod.EditorInfo; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; + import android.widget.Toast; - -import java.io.IOException; - -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.EventLog; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.WelcomeActivity; -import fr.free.nrw.commons.contributions.ContributionsActivity; -import fr.free.nrw.commons.contributions.ContributionsContentProvider; -import fr.free.nrw.commons.modifications.ModificationsContentProvider; import timber.log.Timber; @@ -40,155 +27,49 @@ public class LoginActivity extends AccountAuthenticatorActivity { public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username"; - private CommonsApplication app; - private SharedPreferences prefs = null; - Button loginButton; - Button signupButton; - EditText usernameEdit; + private Button loginButton; + private EditText usernameEdit; EditText passwordEdit; EditText twoFactorEdit; - ProgressDialog dialog; - - private class LoginTask extends AsyncTask { - - Activity context; - String username; - String password; - String twoFactorCode = ""; - - @Override - protected void onPostExecute(String result) { - super.onPostExecute(result); - Timber.d("Login done!"); - - EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT) - .param("username", username) - .param("result", result) - .log(); - - if (result.equals("PASS")) { - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } - Toast successToast = Toast.makeText(context, R.string.login_success, Toast.LENGTH_SHORT); - successToast.show(); - Account account = new Account(username, WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); - boolean accountCreated = AccountManager.get(context).addAccountExplicitly(account, password, null); - - Bundle extras = context.getIntent().getExtras(); - - if (extras != null) { - Timber.d("Bundle of extras: %s", extras); - if (accountCreated) { // Pass the new account back to the account manager - AccountAuthenticatorResponse response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); - Bundle authResult = new Bundle(); - authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username); - authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); - - if (response != null) { - response.onResult(authResult); - } - } - } - // FIXME: If the user turns it off, it shouldn't be auto turned back on - ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default! - ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! - - Intent intent = new Intent(context, ContributionsActivity.class); - startActivity(intent); - finish(); - - } else { - int response; - // Match known failure message codes and provide messages - if(result.equals("NetworkFailure")) { - // Matches NetworkFailure which is created by the doInBackground method - response = R.string.login_failed_network; - } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { - // Matches nosuchuser, nosuchusershort, noname - response = R.string.login_failed_username; - emptySensitiveEditFields(); - } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { - // Matches wrongpassword, wrongpasswordempty - response = R.string.login_failed_password; - emptySensitiveEditFields(); - } else if (result.toLowerCase().contains("throttle".toLowerCase())) { - // Matches unknown throttle error codes - response = R.string.login_failed_throttled; - } else if (result.toLowerCase().contains("userblocked".toLowerCase())) { - // Matches login-userblocked - response = R.string.login_failed_blocked; - } else if (result.equals("2FA")){ - twoFactorEdit.setVisibility(View.VISIBLE); - response = R.string.login_failed_2fa_needed; - } else { - // Occurs with unhandled login failure codes - Timber.d("Login failed with reason: %s", result); - response = R.string.login_failed_generic; - } - Toast.makeText(getApplicationContext(), response, Toast.LENGTH_LONG).show(); - dialog.cancel(); - } - } - - private void emptySensitiveEditFields() { - passwordEdit.setText(""); - twoFactorEdit.setText(""); - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - dialog = new ProgressDialog(context); - dialog.setIndeterminate(true); - dialog.setTitle(getString(R.string.logging_in_title)); - dialog.setMessage(getString(R.string.logging_in_message)); - dialog.setCanceledOnTouchOutside(false); - dialog.show(); - } - - LoginTask(Activity context) { - this.context = context; - } - - @Override - protected String doInBackground(String... params) { - username = params[0]; - password = params[1]; - if(params.length > 2) { - twoFactorCode = params[2]; - } - - try { - if(twoFactorCode.isEmpty()) { - return app.getApi().login(username, password); - } else { - return app.getApi().login(username, password, twoFactorCode); - } - } catch (IOException e) { - // Do something better! - return "NetworkFailure"; - } - } - } + ProgressDialog progressDialog; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - app = (CommonsApplication) this.getApplicationContext(); + setContentView(R.layout.activity_login); + final LoginActivity that = this; + loginButton = (Button) findViewById(R.id.loginButton); - signupButton = (Button) findViewById(R.id.signupButton); + Button signupButton = (Button) findViewById(R.id.signupButton); usernameEdit = (EditText) findViewById(R.id.loginUsername); passwordEdit = (EditText) findViewById(R.id.loginPassword); twoFactorEdit = (EditText) findViewById(R.id.loginTwoFactor); - final LoginActivity that = this; prefs = getSharedPreferences("fr.free.nrw.commons", MODE_PRIVATE); - TextWatcher loginEnabler = new TextWatcher() { + TextWatcher loginEnabler = newLoginTextWatcher(); + usernameEdit.addTextChangedListener(loginEnabler); + passwordEdit.addTextChangedListener(loginEnabler); + twoFactorEdit.addTextChangedListener(loginEnabler); + passwordEdit.setOnEditorActionListener( newLoginInputActionListener() ); + + loginButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + that.performLogin(); + } + }); + signupButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { that.signUp(v); } + }); + } + + private TextWatcher newLoginTextWatcher() { + return new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { } @@ -199,8 +80,8 @@ public class LoginActivity extends AccountAuthenticatorActivity { public void afterTextChanged(Editable editable) { if( usernameEdit.getText().length() != 0 && - passwordEdit.getText().length() != 0 && - ( twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE ) + passwordEdit.getText().length() != 0 && + ( twoFactorEdit.getText().length() != 0 || twoFactorEdit.getVisibility() != View.VISIBLE ) ) { loginButton.setEnabled(true); } else { @@ -208,11 +89,10 @@ public class LoginActivity extends AccountAuthenticatorActivity { } } }; + } - usernameEdit.addTextChangedListener(loginEnabler); - passwordEdit.addTextChangedListener(loginEnabler); - twoFactorEdit.addTextChangedListener(loginEnabler); - passwordEdit.setOnEditorActionListener(new TextView.OnEditorActionListener() { + private TextView.OnEditorActionListener newLoginInputActionListener() { + return new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { if (loginButton.isEnabled()) { @@ -226,36 +106,28 @@ public class LoginActivity extends AccountAuthenticatorActivity { } return false; } - }); - - loginButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - that.performLogin(); - } - }); - + }; } - @Override protected void onResume() { super.onResume(); - if (prefs.getBoolean("firstrun", true)) { - // Do first run stuff here then set 'firstrun' as false - Intent welcomeIntent = new Intent(this, WelcomeActivity.class); - startActivity(welcomeIntent); + this.startWelcomeIntent(); prefs.edit().putBoolean("firstrun", false).apply(); } } + private void startWelcomeIntent() { + Intent welcomeIntent = new Intent(this, WelcomeActivity.class); + startActivity(welcomeIntent); + } @Override protected void onDestroy() { try { // To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method - if (dialog != null && dialog.isShowing()) { - dialog.dismiss(); + if (progressDialog != null && progressDialog.isShowing()) { + progressDialog.dismiss(); } } catch (Exception e) { e.printStackTrace(); @@ -264,21 +136,27 @@ public class LoginActivity extends AccountAuthenticatorActivity { } private void performLogin() { - String username = usernameEdit.getText().toString(); - // Because Mediawiki is upercase-first-char-then-case-sensitive :) - String canonicalUsername = Utils.capitalize(username.substring(0,1)) + username.substring(1); - - String password = passwordEdit.getText().toString(); - - String twoFactorCode = twoFactorEdit.getText().toString(); - Timber.d("Login to start!"); - LoginTask task = new LoginTask(this); - if(twoFactorCode.isEmpty()) { - task.execute(canonicalUsername, password); - } else { - task.execute(canonicalUsername, password, twoFactorCode); - } + LoginTask task = getLoginTask(); + task.execute(); + } + + private LoginTask getLoginTask() { + return new LoginTask( + this, + canonicializeUsername( usernameEdit.getText().toString() ), + passwordEdit.getText().toString(), + twoFactorEdit.getText().toString() + ); + } + + /** + * Because Mediawiki is upercase-first-char-then-case-sensitive :) + * @param username String + * @return String canonicial username + */ + private String canonicializeUsername( String username ) { + return Utils.capitalize(username.substring(0,1)) + username.substring(1); } @Override @@ -291,9 +169,38 @@ public class LoginActivity extends AccountAuthenticatorActivity { return super.onOptionsItemSelected(item); } - //Called when Sign Up button is clicked + /** + * Called when Sign Up button is clicked. + * @param view View + */ public void signUp(View view) { Intent intent = new Intent(this, SignupActivity.class); startActivity(intent); } + + public void askUserForTwoFactorAuth() { + twoFactorEdit.setVisibility(View.VISIBLE); + showUserToastAndCancelDialog( R.string.login_failed_2fa_needed ); + } + + public void showUserToastAndCancelDialog( int resId ) { + showUserToast( resId ); + progressDialog.cancel(); + } + + private void showUserToast( int resId ) { + Toast.makeText(getApplicationContext(), resId, Toast.LENGTH_LONG).show(); + } + + public void showSuccessToastAndDismissDialog() { + Toast successToast = Toast.makeText(this, R.string.login_success, Toast.LENGTH_SHORT); + successToast.show(); + progressDialog.dismiss(); + } + + public void emptySensitiveEditFields() { + passwordEdit.setText(""); + twoFactorEdit.setText(""); + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java new file mode 100644 index 000000000..63e77878a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginTask.java @@ -0,0 +1,145 @@ +package fr.free.nrw.commons.auth; + +import android.accounts.Account; +import android.accounts.AccountAuthenticatorResponse; +import android.accounts.AccountManager; +import android.app.ProgressDialog; +import android.content.ContentResolver; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import fr.free.nrw.commons.CommonsApplication; +import fr.free.nrw.commons.EventLog; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.contributions.ContributionsActivity; +import fr.free.nrw.commons.contributions.ContributionsContentProvider; +import fr.free.nrw.commons.modifications.ModificationsContentProvider; +import timber.log.Timber; + +import java.io.IOException; + +class LoginTask extends AsyncTask { + + private LoginActivity loginActivity; + private String username; + private String password; + private String twoFactorCode = ""; + private CommonsApplication app; + + public LoginTask(LoginActivity loginActivity, String username, String password, String twoFactorCode) { + this.loginActivity = loginActivity; + this.username = username; + this.password = password; + this.twoFactorCode = twoFactorCode; + app = (CommonsApplication) loginActivity.getApplicationContext(); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + loginActivity.progressDialog = new ProgressDialog(loginActivity); + loginActivity.progressDialog.setIndeterminate(true); + loginActivity.progressDialog.setTitle(loginActivity.getString(R.string.logging_in_title)); + loginActivity.progressDialog.setMessage(loginActivity.getString(R.string.logging_in_message)); + loginActivity.progressDialog.setCanceledOnTouchOutside(false); + loginActivity.progressDialog.show(); + } + + @Override + protected String doInBackground(String... params) { + try { + if (twoFactorCode.isEmpty()) { + return app.getApi().login(username, password); + } else { + return app.getApi().login(username, password, twoFactorCode); + } + } catch (IOException e) { + // Do something better! + return "NetworkFailure"; + } + } + + @Override + protected void onPostExecute(String result) { + super.onPostExecute(result); + Timber.d("Login done!"); + + EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT) + .param("username", username) + .param("result", result) + .log(); + + if (result.equals("PASS")) { + handlePassResult(); + } else { + handleOtherResults( result ); + } + } + + private void handlePassResult() { + loginActivity.showSuccessToastAndDismissDialog(); + + Account account = new Account(username, WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); + + + boolean accountCreated = AccountManager.get(loginActivity).addAccount(account, password, null); + + + boolean accountCreated = AccountManager.get(loginActivity).addAccountExplicitly(account, password, null); + + Bundle extras = loginActivity.getIntent().getExtras(); + + if (extras != null) { + Timber.d("Bundle of extras: %s", extras); + if (accountCreated) { // Pass the new account back to the account manager + AccountAuthenticatorResponse response = extras.getParcelable(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE); + Bundle authResult = new Bundle(); + authResult.putString(AccountManager.KEY_ACCOUNT_NAME, username); + authResult.putString(AccountManager.KEY_ACCOUNT_TYPE, WikiAccountAuthenticator.COMMONS_ACCOUNT_TYPE); + + if (response != null) { + response.onResult(authResult); + } + } + } + + // FIXME: If the user turns it off, it shouldn't be auto turned back on + ContentResolver.setSyncAutomatically(account, ContributionsContentProvider.AUTHORITY, true); // Enable sync by default! + ContentResolver.setSyncAutomatically(account, ModificationsContentProvider.AUTHORITY, true); // Enable sync by default! + + Intent intent = new Intent(loginActivity, ContributionsActivity.class); + loginActivity.startActivity(intent); + loginActivity.finish(); + } + + /** + * Match known failure message codes and provide messages + * @param result String + */ + private void handleOtherResults( String result ) { + if (result.equals("NetworkFailure")) { + // Matches NetworkFailure which is created by the doInBackground method + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_network ); + } else if (result.toLowerCase().contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { + // Matches nosuchuser, nosuchusershort, noname + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_username ); + loginActivity.emptySensitiveEditFields(); + } else if (result.toLowerCase().contains("wrongpassword".toLowerCase())) { + // Matches wrongpassword, wrongpasswordempty + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_password ); + loginActivity.emptySensitiveEditFields(); + } else if (result.toLowerCase().contains("throttle".toLowerCase())) { + // Matches unknown throttle error codes + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_throttled ); + } else if (result.toLowerCase().contains("userblocked".toLowerCase())) { + // Matches login-userblocked + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_blocked ); + } else if (result.equals("2FA")) { + loginActivity.askUserForTwoFactorAuth(); + } else { + // Occurs with unhandled login failure codes + Timber.d("Login failed with reason: %s", result); + loginActivity.showUserToastAndCancelDialog( R.string.login_failed_generic ); + } + } +} diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index c81f78a33..b426f4553 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -93,7 +93,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:onClick="signUp" android:text="@string/signup" />