Convert auth package to kotlin (#5966)

* Convert SessionManager to kotlin along with other small fixes

* Convert WikiAccountAuthenticator to kotlin

* Migrate WikiAccountAuthenticatorService to kotlin

* Converted AccountUtil to kotlin

* Convert SignupActivity to kotlin

* Convert LoginActivity to kotlin

* Merge from main
This commit is contained in:
Paul Hawke 2024-11-28 02:09:25 -06:00 committed by GitHub
parent 238023056f
commit 0c969c365b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 752 additions and 964 deletions

View file

@ -17,7 +17,7 @@ class PasteSensitiveTextInputEditTextTest {
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
textView = PasteSensitiveTextInputEditText(context)
textView = PasteSensitiveTextInputEditText(context!!)
}
// this test has no real value, just % for test code coverage

View file

@ -1,44 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import timber.log.Timber;
public class AccountUtil {
public static final String AUTH_TOKEN_TYPE = "CommonsAndroid";
public AccountUtil() {
}
/**
* @return Account|null
*/
@Nullable
public static Account account(Context context) {
try {
Account[] accounts = accountManager(context).getAccountsByType(BuildConfig.ACCOUNT_TYPE);
if (accounts.length > 0) {
return accounts[0];
}
} catch (SecurityException e) {
Timber.e(e);
}
return null;
}
@Nullable
public static String getUserName(Context context) {
Account account = account(context);
return account == null ? null : account.name;
}
private static AccountManager accountManager(Context context) {
return AccountManager.get(context);
}
}

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.auth
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import androidx.annotation.VisibleForTesting
import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE
import timber.log.Timber
const val AUTH_TOKEN_TYPE: String = "CommonsAndroid"
fun getUserName(context: Context): String? {
return account(context)?.name
}
@VisibleForTesting
fun account(context: Context): Account? = try {
val accountManager = AccountManager.get(context)
val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE)
if (accounts.isNotEmpty()) accounts[0] else null
} catch (e: SecurityException) {
Timber.e(e)
null
}

View file

@ -1,456 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.AccountAuthenticatorActivity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.TextView;
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.NavUtils;
import androidx.core.content.ContextCompat;
import fr.free.nrw.commons.auth.login.LoginClient;
import fr.free.nrw.commons.auth.login.LoginResult;
import fr.free.nrw.commons.databinding.ActivityLoginBinding;
import fr.free.nrw.commons.utils.ActivityUtils;
import java.util.Locale;
import fr.free.nrw.commons.auth.login.LoginCallback;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Named;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.SystemThemeUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.disposables.CompositeDisposable;
import timber.log.Timber;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.View.VISIBLE;
import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE;
import static fr.free.nrw.commons.CommonsApplication.LOGIN_MESSAGE_INTENT_KEY;
import static fr.free.nrw.commons.CommonsApplication.LOGIN_USERNAME_INTENT_KEY;
public class LoginActivity extends AccountAuthenticatorActivity {
@Inject
SessionManager sessionManager;
@Inject
@Named("default_preferences")
JsonKvStore applicationKvStore;
@Inject
LoginClient loginClient;
@Inject
SystemThemeUtils systemThemeUtils;
private ActivityLoginBinding binding;
ProgressDialog progressDialog;
private AppCompatDelegate delegate;
private LoginTextWatcher textWatcher = new LoginTextWatcher();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
final String saveProgressDailog="ProgressDailog_state";
final String saveErrorMessage ="errorMessage";
final String saveUsername="username";
final String savePassword="password";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ApplicationlessInjection
.getInstance(this.getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
boolean isDarkTheme = systemThemeUtils.isDeviceInNightMode();
setTheme(isDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme);
getDelegate().installViewFactory();
getDelegate().onCreate(savedInstanceState);
binding = ActivityLoginBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
String message = getIntent().getStringExtra(LOGIN_MESSAGE_INTENT_KEY);
String username = getIntent().getStringExtra(LOGIN_USERNAME_INTENT_KEY);
binding.loginUsername.addTextChangedListener(textWatcher);
binding.loginPassword.addTextChangedListener(textWatcher);
binding.loginTwoFactor.addTextChangedListener(textWatcher);
binding.skipLogin.setOnClickListener(view -> skipLogin());
binding.forgotPassword.setOnClickListener(view -> forgotPassword());
binding.aboutPrivacyPolicy.setOnClickListener(view -> onPrivacyPolicyClicked());
binding.signUpButton.setOnClickListener(view -> signUp());
binding.loginButton.setOnClickListener(view -> performLogin());
binding.loginPassword.setOnEditorActionListener(this::onEditorAction);
binding.loginPassword.setOnFocusChangeListener(this::onPasswordFocusChanged);
if (ConfigUtils.isBetaFlavour()) {
binding.loginCredentials.setText(getString(R.string.login_credential));
} else {
binding.loginCredentials.setVisibility(View.GONE);
}
if (message != null) {
showMessage(message, R.color.secondaryDarkColor);
}
if (username != null) {
binding.loginUsername.setText(username);
}
}
/**
* Hides the keyboard if the user's focus is not on the password (hasFocus is false).
* @param view The keyboard
* @param hasFocus Set to true if the keyboard has focus
*/
void onPasswordFocusChanged(View view, boolean hasFocus) {
if (!hasFocus) {
ViewUtil.hideKeyboard(view);
}
}
boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) {
if (binding.loginButton.isEnabled()) {
if (actionId == IME_ACTION_DONE) {
performLogin();
return true;
} else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) {
performLogin();
return true;
}
}
return false;
}
protected void skipLogin() {
new AlertDialog.Builder(this).setTitle(R.string.skip_login_title)
.setMessage(R.string.skip_login_message)
.setCancelable(false)
.setPositiveButton(R.string.yes, (dialog, which) -> {
dialog.cancel();
performSkipLogin();
})
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show();
}
protected void forgotPassword() {
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL));
}
protected void onPrivacyPolicyClicked() {
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL));
}
protected void signUp() {
Intent intent = new Intent(this, SignupActivity.class);
startActivity(intent);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
getDelegate().onPostCreate(savedInstanceState);
}
@Override
protected void onResume() {
super.onResume();
if (sessionManager.getCurrentAccount() != null
&& sessionManager.isUserLoggedIn()) {
applicationKvStore.putBoolean("login_skipped", false);
startMainActivity();
}
if (applicationKvStore.getBoolean("login_skipped", false)) {
performSkipLogin();
}
}
@Override
protected void onDestroy() {
compositeDisposable.clear();
try {
// To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method
if (progressDialog != null && progressDialog.isShowing()) {
progressDialog.dismiss();
}
} catch (Exception e) {
e.printStackTrace();
}
binding.loginUsername.removeTextChangedListener(textWatcher);
binding.loginPassword.removeTextChangedListener(textWatcher);
binding.loginTwoFactor.removeTextChangedListener(textWatcher);
delegate.onDestroy();
if(null!=loginClient) {
loginClient.cancel();
}
binding = null;
super.onDestroy();
}
public void performLogin() {
Timber.d("Login to start!");
final String username = Objects.requireNonNull(binding.loginUsername.getText()).toString();
final String password = Objects.requireNonNull(binding.loginPassword.getText()).toString();
final String twoFactorCode = Objects.requireNonNull(binding.loginTwoFactor.getText()).toString();
showLoggingProgressBar();
loginClient.doLogin(username, password, twoFactorCode, Locale.getDefault().getLanguage(),
new LoginCallback() {
@Override
public void success(@NonNull LoginResult loginResult) {
runOnUiThread(()->{
Timber.d("Login Success");
hideProgress();
onLoginSuccess(loginResult);
});
}
@Override
public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) {
runOnUiThread(()->{
Timber.d("Requesting 2FA prompt");
hideProgress();
askUserForTwoFactorAuth();
});
}
@Override
public void passwordResetPrompt(@Nullable String token) {
runOnUiThread(()->{
Timber.d("Showing password reset prompt");
hideProgress();
showPasswordResetPrompt();
});
}
@Override
public void error(@NonNull Throwable caught) {
runOnUiThread(()->{
Timber.e(caught);
hideProgress();
showMessageAndCancelDialog(caught.getLocalizedMessage());
});
}
});
}
private void hideProgress() {
progressDialog.dismiss();
}
private void showPasswordResetPrompt() {
showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword));
}
/**
* This function is called when user skips the login.
* It redirects the user to Explore Activity.
*/
private void performSkipLogin() {
applicationKvStore.putBoolean("login_skipped", true);
MainActivity.startYourself(this);
finish();
}
private void showLoggingProgressBar() {
progressDialog = new ProgressDialog(this);
progressDialog.setIndeterminate(true);
progressDialog.setTitle(getString(R.string.logging_in_title));
progressDialog.setMessage(getString(R.string.logging_in_message));
progressDialog.setCanceledOnTouchOutside(false);
progressDialog.show();
}
private void onLoginSuccess(LoginResult loginResult) {
compositeDisposable.clear();
sessionManager.setUserLoggedIn(true);
sessionManager.updateAccount(loginResult);
progressDialog.dismiss();
showSuccessAndDismissDialog();
startMainActivity();
}
@Override
protected void onStart() {
super.onStart();
delegate.onStart();
}
@Override
protected void onStop() {
super.onStop();
delegate.onStop();
}
@Override
protected void onPostResume() {
super.onPostResume();
getDelegate().onPostResume();
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
@NonNull
public MenuInflater getMenuInflater() {
return getDelegate().getMenuInflater();
}
public void askUserForTwoFactorAuth() {
progressDialog.dismiss();
binding.twoFactorContainer.setVisibility(VISIBLE);
binding.loginTwoFactor.setVisibility(VISIBLE);
binding.loginTwoFactor.requestFocus();
InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY);
showMessageAndCancelDialog(R.string.login_failed_2fa_needed);
}
public void showMessageAndCancelDialog(@StringRes int resId) {
showMessage(resId, R.color.secondaryDarkColor);
if (progressDialog != null) {
progressDialog.cancel();
}
}
public void showMessageAndCancelDialog(String error) {
showMessage(error, R.color.secondaryDarkColor);
if (progressDialog != null) {
progressDialog.cancel();
}
}
public void showSuccessAndDismissDialog() {
showMessage(R.string.login_success, R.color.primaryDarkColor);
progressDialog.dismiss();
}
public void startMainActivity() {
ActivityUtils.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP);
finish();
}
private void showMessage(@StringRes int resId, @ColorRes int colorResId) {
binding.errorMessage.setText(getString(resId));
binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
binding.errorMessageContainer.setVisibility(VISIBLE);
}
private void showMessage(String message, @ColorRes int colorResId) {
binding.errorMessage.setText(message);
binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId));
binding.errorMessageContainer.setVisibility(VISIBLE);
}
private AppCompatDelegate getDelegate() {
if (delegate == null) {
delegate = AppCompatDelegate.create(this, null);
}
return delegate;
}
private class LoginTextWatcher implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void afterTextChanged(Editable editable) {
boolean enabled = binding.loginUsername.getText().length() != 0 &&
binding.loginPassword.getText().length() != 0 &&
(BuildConfig.DEBUG || binding.loginTwoFactor.getText().length() != 0 ||
binding.loginTwoFactor.getVisibility() != VISIBLE);
binding.loginButton.setEnabled(enabled);
}
}
public static void startYourself(Context context) {
Intent intent = new Intent(context, LoginActivity.class);
context.startActivity(intent);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
// if progressDialog is visible during the configuration change then store state as true else false so that
// we maintain visibility of progressDailog after configuration change
if(progressDialog!=null&&progressDialog.isShowing()) {
outState.putBoolean(saveProgressDailog,true);
} else {
outState.putBoolean(saveProgressDailog,false);
}
outState.putString(saveErrorMessage,binding.errorMessage.getText().toString()); //Save the errorMessage
outState.putString(saveUsername,getUsername()); // Save the username
outState.putString(savePassword,getPassword()); // Save the password
}
private String getUsername() {
return binding.loginUsername.getText().toString();
}
private String getPassword(){
return binding.loginPassword.getText().toString();
}
@Override
protected void onRestoreInstanceState(final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
binding.loginUsername.setText(savedInstanceState.getString(saveUsername));
binding.loginPassword.setText(savedInstanceState.getString(savePassword));
if(savedInstanceState.getBoolean(saveProgressDailog)) {
performLogin();
}
String errorMessage=savedInstanceState.getString(saveErrorMessage);
if(sessionManager.isUserLoggedIn()) {
showMessage(R.string.login_success, R.color.primaryDarkColor);
} else {
showMessage(errorMessage, R.color.secondaryDarkColor);
}
}
}

View file

@ -0,0 +1,404 @@
package fr.free.nrw.commons.auth
import android.accounts.AccountAuthenticatorActivity
import android.app.ProgressDialog
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.KeyEvent
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.StringRes
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.app.NavUtils
import androidx.core.content.ContextCompat
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.login.LoginCallback
import fr.free.nrw.commons.auth.login.LoginClient
import fr.free.nrw.commons.auth.login.LoginResult
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.ActivityLoginBinding
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.utils.AbstractTextWatcher
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
import fr.free.nrw.commons.utils.SystemThemeUtils
import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
import javax.inject.Named
class LoginActivity : AccountAuthenticatorActivity() {
@Inject
lateinit var sessionManager: SessionManager
@Inject
@field:Named("default_preferences")
lateinit var applicationKvStore: JsonKvStore
@Inject
lateinit var loginClient: LoginClient
@Inject
lateinit var systemThemeUtils: SystemThemeUtils
private var binding: ActivityLoginBinding? = null
private var progressDialog: ProgressDialog? = null
private val textWatcher = AbstractTextWatcher(::onTextChanged)
private val compositeDisposable = CompositeDisposable()
private val delegate: AppCompatDelegate by lazy {
AppCompatDelegate.create(this, null)
}
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ApplicationlessInjection
.getInstance(this.applicationContext)
.commonsApplicationComponent
.inject(this)
val isDarkTheme = systemThemeUtils.isDeviceInNightMode()
setTheme(if (isDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme)
delegate.installViewFactory()
delegate.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
with(binding!!) {
setContentView(root)
loginUsername.addTextChangedListener(textWatcher)
loginPassword.addTextChangedListener(textWatcher)
loginTwoFactor.addTextChangedListener(textWatcher)
skipLogin.setOnClickListener { skipLogin() }
forgotPassword.setOnClickListener { forgotPassword() }
aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() }
signUpButton.setOnClickListener { signUp() }
loginButton.setOnClickListener { performLogin() }
loginPassword.setOnEditorActionListener(::onEditorAction)
loginPassword.onFocusChangeListener =
View.OnFocusChangeListener(::onPasswordFocusChanged)
if (isBetaFlavour) {
loginCredentials.text = getString(R.string.login_credential)
} else {
loginCredentials.visibility = View.GONE
}
intent.getStringExtra(CommonsApplication.LOGIN_MESSAGE_INTENT_KEY)?.let {
showMessage(it, R.color.secondaryDarkColor)
}
intent.getStringExtra(CommonsApplication.LOGIN_USERNAME_INTENT_KEY)?.let {
loginUsername.setText(it)
}
}
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
delegate.onPostCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
if (sessionManager.currentAccount != null && sessionManager.isUserLoggedIn) {
applicationKvStore.putBoolean("login_skipped", false)
startMainActivity()
}
if (applicationKvStore.getBoolean("login_skipped", false)) {
performSkipLogin()
}
}
override fun onDestroy() {
compositeDisposable.clear()
try {
// To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method
if (progressDialog?.isShowing == true) {
progressDialog!!.dismiss()
}
} catch (e: Exception) {
e.printStackTrace()
}
with(binding!!) {
loginUsername.removeTextChangedListener(textWatcher)
loginPassword.removeTextChangedListener(textWatcher)
loginTwoFactor.removeTextChangedListener(textWatcher)
}
delegate.onDestroy()
loginClient?.cancel()
binding = null
super.onDestroy()
}
override fun onStart() {
super.onStart()
delegate.onStart()
}
override fun onStop() {
super.onStop()
delegate.onStop()
}
override fun onPostResume() {
super.onPostResume()
delegate.onPostResume()
}
override fun setContentView(view: View, params: ViewGroup.LayoutParams) {
delegate.setContentView(view, params)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
NavUtils.navigateUpFromSameTask(this)
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onSaveInstanceState(outState: Bundle) {
// if progressDialog is visible during the configuration change then store state as true else false so that
// we maintain visibility of progressDailog after configuration change
if (progressDialog != null && progressDialog!!.isShowing) {
outState.putBoolean(saveProgressDailog, true)
} else {
outState.putBoolean(saveProgressDailog, false)
}
outState.putString(
saveErrorMessage,
binding!!.errorMessage.text.toString()
) //Save the errorMessage
outState.putString(
saveUsername,
binding!!.loginUsername.text.toString()
) // Save the username
outState.putString(
savePassword,
binding!!.loginPassword.text.toString()
) // Save the password
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
binding!!.loginUsername.setText(savedInstanceState.getString(saveUsername))
binding!!.loginPassword.setText(savedInstanceState.getString(savePassword))
if (savedInstanceState.getBoolean(saveProgressDailog)) {
performLogin()
}
val errorMessage = savedInstanceState.getString(saveErrorMessage)
if (sessionManager.isUserLoggedIn) {
showMessage(R.string.login_success, R.color.primaryDarkColor)
} else {
showMessage(errorMessage, R.color.secondaryDarkColor)
}
}
/**
* Hides the keyboard if the user's focus is not on the password (hasFocus is false).
* @param view The keyboard
* @param hasFocus Set to true if the keyboard has focus
*/
private fun onPasswordFocusChanged(view: View, hasFocus: Boolean) {
if (!hasFocus) {
hideKeyboard(view)
}
}
private fun onEditorAction(textView: TextView, actionId: Int, keyEvent: KeyEvent?) =
if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) {
performLogin()
true
} else false
private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) =
actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
private fun skipLogin() {
AlertDialog.Builder(this)
.setTitle(R.string.skip_login_title)
.setMessage(R.string.skip_login_message)
.setCancelable(false)
.setPositiveButton(R.string.yes) { dialog: DialogInterface, which: Int ->
dialog.cancel()
performSkipLogin()
}
.setNegativeButton(R.string.no) { dialog: DialogInterface, which: Int ->
dialog.cancel()
}
.show()
}
private fun forgotPassword() =
Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL))
private fun onPrivacyPolicyClicked() =
Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL))
private fun signUp() =
startActivity(Intent(this, SignupActivity::class.java))
@VisibleForTesting
fun performLogin() {
Timber.d("Login to start!")
val username = binding!!.loginUsername.text.toString()
val password = binding!!.loginPassword.text.toString()
val twoFactorCode = binding!!.loginTwoFactor.text.toString()
showLoggingProgressBar()
loginClient.doLogin(username,
password,
twoFactorCode,
Locale.getDefault().language,
object : LoginCallback {
override fun success(loginResult: LoginResult) = runOnUiThread {
Timber.d("Login Success")
progressDialog!!.dismiss()
onLoginSuccess(loginResult)
}
override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread {
Timber.d("Requesting 2FA prompt")
progressDialog!!.dismiss()
askUserForTwoFactorAuth()
}
override fun passwordResetPrompt(token: String?) = runOnUiThread {
Timber.d("Showing password reset prompt")
progressDialog!!.dismiss()
showPasswordResetPrompt()
}
override fun error(caught: Throwable) = runOnUiThread {
Timber.e(caught)
progressDialog!!.dismiss()
showMessageAndCancelDialog(caught.localizedMessage ?: "")
}
}
)
}
private fun showPasswordResetPrompt() =
showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword))
/**
* This function is called when user skips the login.
* It redirects the user to Explore Activity.
*/
private fun performSkipLogin() {
applicationKvStore.putBoolean("login_skipped", true)
MainActivity.startYourself(this)
finish()
}
private fun showLoggingProgressBar() {
progressDialog = ProgressDialog(this).apply {
isIndeterminate = true
setTitle(getString(R.string.logging_in_title))
setMessage(getString(R.string.logging_in_message))
setCanceledOnTouchOutside(false)
}
progressDialog!!.show()
}
private fun onLoginSuccess(loginResult: LoginResult) {
compositeDisposable.clear()
sessionManager.setUserLoggedIn(true)
sessionManager.updateAccount(loginResult)
progressDialog!!.dismiss()
showSuccessAndDismissDialog()
startMainActivity()
}
override fun getMenuInflater(): MenuInflater =
delegate.menuInflater
@VisibleForTesting
fun askUserForTwoFactorAuth() {
progressDialog!!.dismiss()
with(binding!!) {
twoFactorContainer.visibility = View.VISIBLE
loginTwoFactor.visibility = View.VISIBLE
loginTwoFactor.requestFocus()
}
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)
showMessageAndCancelDialog(R.string.login_failed_2fa_needed)
}
@VisibleForTesting
fun showMessageAndCancelDialog(@StringRes resId: Int) {
showMessage(resId, R.color.secondaryDarkColor)
progressDialog?.cancel()
}
@VisibleForTesting
fun showMessageAndCancelDialog(error: String) {
showMessage(error, R.color.secondaryDarkColor)
progressDialog?.cancel()
}
@VisibleForTesting
fun showSuccessAndDismissDialog() {
showMessage(R.string.login_success, R.color.primaryDarkColor)
progressDialog!!.dismiss()
}
@VisibleForTesting
fun startMainActivity() {
startActivityWithFlags(this, MainActivity::class.java, Intent.FLAG_ACTIVITY_SINGLE_TOP)
finish()
}
private fun showMessage(@StringRes resId: Int, @ColorRes colorResId: Int) = with(binding!!) {
errorMessage.text = getString(resId)
errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId))
errorMessageContainer.visibility = View.VISIBLE
}
private fun showMessage(message: String?, @ColorRes colorResId: Int) = with(binding!!) {
errorMessage.text = message
errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId))
errorMessageContainer.visibility = View.VISIBLE
}
private fun onTextChanged(text: String) {
val enabled =
binding!!.loginUsername.text!!.length != 0 && binding!!.loginPassword.text!!.length != 0 &&
(BuildConfig.DEBUG || binding!!.loginTwoFactor.text!!.length != 0 || binding!!.loginTwoFactor.visibility != View.VISIBLE)
binding!!.loginButton.isEnabled = enabled
}
companion object {
fun startYourself(context: Context) =
context.startActivity(Intent(context, LoginActivity::class.java))
const val saveProgressDailog: String = "ProgressDailog_state"
const val saveErrorMessage: String = "errorMessage"
const val saveUsername: String = "username"
const val savePassword: String = "password"
}
}

View file

@ -1,148 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.Context;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.auth.login.LoginResult;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import io.reactivex.Completable;
import io.reactivex.Observable;
/**
* Manage the current logged in user session.
*/
@Singleton
public class SessionManager {
private final Context context;
private Account currentAccount; // Unlike a savings account... ;-)
private JsonKvStore defaultKvStore;
@Inject
public SessionManager(Context context,
@Named("default_preferences") JsonKvStore defaultKvStore) {
this.context = context;
this.currentAccount = null;
this.defaultKvStore = defaultKvStore;
}
private boolean createAccount(@NonNull String userName, @NonNull String password) {
Account account = getCurrentAccount();
if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) {
removeAccount();
account = new Account(userName, BuildConfig.ACCOUNT_TYPE);
return accountManager().addAccountExplicitly(account, password, null);
}
return true;
}
private void removeAccount() {
Account account = getCurrentAccount();
if (account != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
accountManager().removeAccountExplicitly(account);
} else {
//noinspection deprecation
accountManager().removeAccount(account, null, null);
}
}
}
public void updateAccount(LoginResult result) {
boolean accountCreated = createAccount(result.getUserName(), result.getPassword());
if (accountCreated) {
setPassword(result.getPassword());
}
}
private void setPassword(@NonNull String password) {
Account account = getCurrentAccount();
if (account != null) {
accountManager().setPassword(account, password);
}
}
/**
* @return Account|null
*/
@Nullable
public Account getCurrentAccount() {
if (currentAccount == null) {
AccountManager accountManager = AccountManager.get(context);
Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE);
if (allAccounts.length != 0) {
currentAccount = allAccounts[0];
}
}
return currentAccount;
}
public boolean doesAccountExist() {
return getCurrentAccount() != null;
}
@Nullable
public String getUserName() {
Account account = getCurrentAccount();
return account == null ? null : account.name;
}
@Nullable
public String getPassword() {
Account account = getCurrentAccount();
return account == null ? null : accountManager().getPassword(account);
}
private AccountManager accountManager() {
return AccountManager.get(context);
}
public boolean isUserLoggedIn() {
return defaultKvStore.getBoolean("isUserLoggedIn", false);
}
void setUserLoggedIn(boolean isLoggedIn) {
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn);
}
public void forceLogin(Context context) {
if (context != null) {
LoginActivity.startYourself(context);
}
}
/**
* Returns a Completable that clears existing accounts from account manager
*/
public Completable logout() {
return Completable.fromObservable(
Observable.empty()
.doOnComplete(
() -> {
removeAccount();
currentAccount = null;
}
)
);
}
/**
* Return a corresponding boolean preference
*
* @param key
* @return
*/
public boolean getPreference(String key) {
return defaultKvStore.getBoolean(key);
}
}

View file

@ -0,0 +1,95 @@
package fr.free.nrw.commons.auth
import android.accounts.Account
import android.accounts.AccountManager
import android.content.Context
import android.os.Build
import android.text.TextUtils
import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE
import fr.free.nrw.commons.auth.login.LoginResult
import fr.free.nrw.commons.kvstore.JsonKvStore
import io.reactivex.Completable
import io.reactivex.Observable
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Singleton
/**
* Manage the current logged in user session.
*/
@Singleton
class SessionManager @Inject constructor(
private val context: Context,
@param:Named("default_preferences") private val defaultKvStore: JsonKvStore
) {
private val accountManager: AccountManager get() = AccountManager.get(context)
private var _currentAccount: Account? = null // Unlike a savings account... ;-)
val currentAccount: Account? get() {
if (_currentAccount == null) {
val allAccounts = AccountManager.get(context).getAccountsByType(ACCOUNT_TYPE)
if (allAccounts.isNotEmpty()) {
_currentAccount = allAccounts[0]
}
}
return _currentAccount
}
val userName: String?
get() = currentAccount?.name
var password: String?
get() = currentAccount?.let { accountManager.getPassword(it) }
private set(value) {
currentAccount?.let { accountManager.setPassword(it, value) }
}
val isUserLoggedIn: Boolean
get() = defaultKvStore.getBoolean("isUserLoggedIn", false)
fun updateAccount(result: LoginResult) {
if (createAccount(result.userName!!, result.password!!)) {
password = result.password
}
}
fun doesAccountExist(): Boolean =
currentAccount != null
fun setUserLoggedIn(isLoggedIn: Boolean) =
defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn)
fun forceLogin(context: Context?) =
context?.let { LoginActivity.startYourself(it) }
fun getPreference(key: String?): Boolean =
defaultKvStore.getBoolean(key)
fun logout(): Completable = Completable.fromObservable(
Observable.empty<Any>()
.doOnComplete {
removeAccount()
_currentAccount = null
}
)
private fun createAccount(userName: String, password: String): Boolean {
var account = currentAccount
if (account == null || TextUtils.isEmpty(account.name) || account.name != userName) {
removeAccount()
account = Account(userName, ACCOUNT_TYPE)
return accountManager.addAccountExplicitly(account, password, null)
}
return true
}
private fun removeAccount() {
currentAccount?.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
accountManager.removeAccountExplicitly(it)
} else {
accountManager.removeAccount(it, null, null)
}
}
}
}

View file

@ -1,82 +0,0 @@
package fr.free.nrw.commons.auth;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.theme.BaseActivity;
import timber.log.Timber;
public class SignupActivity extends BaseActivity {
private WebView webView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Timber.d("Signup Activity started");
webView = new WebView(this);
setContentView(webView);
webView.setWebViewClient(new MyWebViewClient());
WebSettings webSettings = webView.getSettings();
/*Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can
trust Wikimedia's site... right?*/
webSettings.setJavaScriptEnabled(true);
webView.loadUrl(BuildConfig.SIGNUP_LANDING_URL);
}
private class MyWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (url.equals(BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL)) {
//Signup success, so clear cookies, notify user, and load LoginActivity again
Timber.d("Overriding URL %s", url);
Toast toast = Toast.makeText(SignupActivity.this,
R.string.account_created, Toast.LENGTH_LONG);
toast.show();
// terminate on task completion.
finish();
return true;
} else {
//If user clicks any other links in the webview
Timber.d("Not overriding URL, URL is: %s", url);
return false;
}
}
}
@Override
public void onBackPressed() {
if (webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
}
}
/**
* Known bug in androidx.appcompat library version 1.1.0 being tracked here
* https://issuetracker.google.com/issues/141132133
* App tries to put light/dark theme to webview and crashes in the process
* This code tries to prevent applying the theme when sdk is between api 21 to 25
* @param overrideConfiguration
*/
@Override
public void applyOverrideConfiguration(final Configuration overrideConfiguration) {
if (Build.VERSION.SDK_INT <= 25 &&
(getResources().getConfiguration().uiMode == getApplicationContext().getResources().getConfiguration().uiMode)) {
return;
}
super.applyOverrideConfiguration(overrideConfiguration);
}
}

View file

@ -0,0 +1,75 @@
package fr.free.nrw.commons.auth
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.R
import fr.free.nrw.commons.theme.BaseActivity
import timber.log.Timber
class SignupActivity : BaseActivity() {
private var webView: WebView? = null
@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Timber.d("Signup Activity started")
webView = WebView(this)
with(webView!!) {
setContentView(this)
webViewClient = MyWebViewClient()
// Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can
// trust Wikimedia's site... right?
settings.javaScriptEnabled = true
loadUrl(BuildConfig.SIGNUP_LANDING_URL)
}
}
override fun onBackPressed() {
if (webView!!.canGoBack()) {
webView!!.goBack()
} else {
super.onBackPressed()
}
}
/**
* Known bug in androidx.appcompat library version 1.1.0 being tracked here
* https://issuetracker.google.com/issues/141132133
* App tries to put light/dark theme to webview and crashes in the process
* This code tries to prevent applying the theme when sdk is between api 21 to 25
*/
override fun applyOverrideConfiguration(overrideConfiguration: Configuration) {
if (Build.VERSION.SDK_INT <= 25 &&
(resources.configuration.uiMode == applicationContext.resources.configuration.uiMode)
) return
super.applyOverrideConfiguration(overrideConfiguration)
}
private inner class MyWebViewClient : WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean =
if (url == BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL) {
//Signup success, so clear cookies, notify user, and load LoginActivity again
Timber.d("Overriding URL %s", url)
Toast.makeText(
this@SignupActivity, R.string.account_created, Toast.LENGTH_LONG
).show()
// terminate on task completion.
finish()
true
} else {
//If user clicks any other links in the webview
Timber.d("Not overriding URL, URL is: %s", url)
false
}
}
}

View file

@ -1,141 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.BuildConfig;
import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE;
/**
* Handles WikiMedia commons account Authentication
*/
public class WikiAccountAuthenticator extends AbstractAccountAuthenticator {
private static final String[] SYNC_AUTHORITIES = {BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY};
@NonNull
private final Context context;
public WikiAccountAuthenticator(@NonNull Context context) {
super(context);
this.context = context;
}
/**
* Provides Bundle with edited Account Properties
*/
@Override
public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
Bundle bundle = new Bundle();
bundle.putString("test", "editProperties");
return bundle;
}
@Override
public Bundle addAccount(@NonNull AccountAuthenticatorResponse response,
@NonNull String accountType, @Nullable String authTokenType,
@Nullable String[] requiredFeatures, @Nullable Bundle options)
throws NetworkErrorException {
// account type not supported returns bundle without loginActivity Intent, it just contains "test" key
if (!supportedAccountType(accountType)) {
Bundle bundle = new Bundle();
bundle.putString("test", "addAccount");
return bundle;
}
return addAccount(response);
}
@Override
public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @Nullable Bundle options)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putString("test", "confirmCredentials");
return bundle;
}
@Override
public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @NonNull String authTokenType,
@Nullable Bundle options)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putString("test", "getAuthToken");
return bundle;
}
@Nullable
@Override
public String getAuthTokenLabel(@NonNull String authTokenType) {
return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null;
}
@Nullable
@Override
public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @Nullable String authTokenType,
@Nullable Bundle options)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putString("test", "updateCredentials");
return bundle;
}
@Nullable
@Override
public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response,
@NonNull Account account, @NonNull String[] features)
throws NetworkErrorException {
Bundle bundle = new Bundle();
bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
return bundle;
}
private boolean supportedAccountType(@Nullable String type) {
return BuildConfig.ACCOUNT_TYPE.equals(type);
}
/**
* Provides a bundle containing a Parcel
* the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type)
*/
private Bundle addAccount(AccountAuthenticatorResponse response) {
Intent intent = new Intent(context, LoginActivity.class);
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response);
Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;
}
@Override
public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response,
Account account) throws NetworkErrorException {
Bundle result = super.getAccountRemovalAllowed(response, account);
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
&& !result.containsKey(AccountManager.KEY_INTENT)) {
boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT);
if (allowed) {
for (String auth : SYNC_AUTHORITIES) {
ContentResolver.cancelSync(account, auth);
}
}
}
return result;
}
}

View file

@ -0,0 +1,108 @@
package fr.free.nrw.commons.auth
import android.accounts.AbstractAccountAuthenticator
import android.accounts.Account
import android.accounts.AccountAuthenticatorResponse
import android.accounts.AccountManager
import android.accounts.NetworkErrorException
import android.content.ContentResolver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.os.bundleOf
import fr.free.nrw.commons.BuildConfig
private val SYNC_AUTHORITIES = arrayOf(
BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY
)
/**
* Handles WikiMedia commons account Authentication
*/
class WikiAccountAuthenticator(
private val context: Context
) : AbstractAccountAuthenticator(context) {
/**
* Provides Bundle with edited Account Properties
*/
override fun editProperties(
response: AccountAuthenticatorResponse,
accountType: String
) = bundleOf("test" to "editProperties")
// account type not supported returns bundle without loginActivity Intent, it just contains "test" key
@Throws(NetworkErrorException::class)
override fun addAccount(
response: AccountAuthenticatorResponse,
accountType: String,
authTokenType: String?,
requiredFeatures: Array<String>?,
options: Bundle?
) = if (BuildConfig.ACCOUNT_TYPE == accountType) {
addAccount(response)
} else {
bundleOf("test" to "addAccount")
}
@Throws(NetworkErrorException::class)
override fun confirmCredentials(
response: AccountAuthenticatorResponse, account: Account, options: Bundle?
) = bundleOf("test" to "confirmCredentials")
@Throws(NetworkErrorException::class)
override fun getAuthToken(
response: AccountAuthenticatorResponse,
account: Account,
authTokenType: String,
options: Bundle?
) = bundleOf("test" to "getAuthToken")
override fun getAuthTokenLabel(authTokenType: String) =
if (BuildConfig.ACCOUNT_TYPE == authTokenType) AUTH_TOKEN_TYPE else null
@Throws(NetworkErrorException::class)
override fun updateCredentials(
response: AccountAuthenticatorResponse,
account: Account,
authTokenType: String?,
options: Bundle?
) = bundleOf("test" to "updateCredentials")
@Throws(NetworkErrorException::class)
override fun hasFeatures(
response: AccountAuthenticatorResponse,
account: Account, features: Array<String>
) = bundleOf(AccountManager.KEY_BOOLEAN_RESULT to false)
/**
* Provides a bundle containing a Parcel
* the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type)
*/
private fun addAccount(response: AccountAuthenticatorResponse): Bundle {
val intent = Intent(context, LoginActivity::class.java)
.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
return bundleOf(AccountManager.KEY_INTENT to intent)
}
@Throws(NetworkErrorException::class)
override fun getAccountRemovalAllowed(
response: AccountAuthenticatorResponse?,
account: Account?
): Bundle {
val result = super.getAccountRemovalAllowed(response, account)
if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT)
&& !result.containsKey(AccountManager.KEY_INTENT)
) {
val allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT)
if (allowed) {
for (auth in SYNC_AUTHORITIES) {
ContentResolver.cancelSync(account, auth)
}
}
}
return result
}
}

View file

@ -1,31 +0,0 @@
package fr.free.nrw.commons.auth;
import android.accounts.AbstractAccountAuthenticator;
import android.content.Intent;
import android.os.IBinder;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.di.CommonsDaggerService;
/**
* Handles the Auth service of the App, see AndroidManifests for details
* (Uses Dagger 2 as injector)
*/
public class WikiAccountAuthenticatorService extends CommonsDaggerService {
@Nullable
private AbstractAccountAuthenticator authenticator;
@Override
public void onCreate() {
super.onCreate();
authenticator = new WikiAccountAuthenticator(this);
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return authenticator == null ? null : authenticator.getIBinder();
}
}

View file

@ -0,0 +1,22 @@
package fr.free.nrw.commons.auth
import android.accounts.AbstractAccountAuthenticator
import android.content.Intent
import android.os.IBinder
import fr.free.nrw.commons.di.CommonsDaggerService
/**
* Handles the Auth service of the App, see AndroidManifests for details
* (Uses Dagger 2 as injector)
*/
class WikiAccountAuthenticatorService : CommonsDaggerService() {
private var authenticator: AbstractAccountAuthenticator? = null
override fun onCreate() {
super.onCreate()
authenticator = WikiAccountAuthenticator(this)
}
override fun onBind(intent: Intent): IBinder? =
authenticator?.iBinder
}

View file

@ -14,7 +14,6 @@ import dagger.Module;
import dagger.Provides;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao;
@ -114,11 +113,6 @@ public class CommonsApplicationModule {
return byName;
}
@Provides
public AccountUtil providesAccountUtil(Context context) {
return new AccountUtil();
}
/**
* Provides an instance of CategoryContentProviderClient i.e. the categories
* that are there in local storage

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.feedback;
import android.content.Context;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.AccountUtilKt;
import fr.free.nrw.commons.feedback.model.Feedback;
import fr.free.nrw.commons.utils.LangCodeUtils;
import java.util.Locale;
@ -43,7 +43,7 @@ public class FeedbackContentCreator {
sectionTitleBuilder = new StringBuilder();
sectionTitleBuilder.append("Feedback from ");
sectionTitleBuilder.append(AccountUtil.getUserName(context));
sectionTitleBuilder.append(AccountUtilKt.getUserName(context));
sectionTitleBuilder.append(" for version ");
sectionTitleBuilder.append(feedback.getVersion());
sectionTitleBuilder.append(" on ");

View file

@ -53,7 +53,7 @@ import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.actions.ThanksClient;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.AccountUtilKt;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
import fr.free.nrw.commons.category.CategoryClient;
@ -382,8 +382,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
enableProgressBar();
}
if (AccountUtil.getUserName(getContext()) != null && media != null
&& AccountUtil.getUserName(getContext()).equals(media.getAuthor())) {
if (AccountUtilKt.getUserName(getContext()) != null && media != null
&& AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) {
binding.sendThanks.setVisibility(GONE);
} else {
binding.sendThanks.setVisibility(VISIBLE);
@ -485,7 +485,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
}
private void onDeletionPageExists(Boolean deletionPageExists) {
if (AccountUtil.getUserName(getContext()) == null && !AccountUtil.getUserName(getContext()).equals(media.getAuthor())) {
if (AccountUtilKt.getUserName(getContext()) == null && !AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) {
binding.nominateDeletion.setVisibility(GONE);
binding.nominatedDeletionBanner.setVisibility(GONE);
} else if (deletionPageExists) {
@ -1079,7 +1079,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
@SuppressLint("StringFormatInvalid")
public void onDeleteButtonClicked(){
if (AccountUtil.getUserName(getContext()) != null && AccountUtil.getUserName(getContext()).equals(media.getAuthor())) {
if (AccountUtilKt.getUserName(getContext()) != null && AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) {
final ArrayAdapter<String> languageAdapter = new ArrayAdapter<>(getActivity(),
R.layout.simple_spinner_dropdown_list, reasonList);
final Spinner spinner = new Spinner(getActivity());
@ -1105,7 +1105,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
//Reviewer correct me if i have misunderstood something over here
//But how does this if (delete.getVisibility() == View.VISIBLE) {
// enableDeleteButton(true); makes sense ?
else if (AccountUtil.getUserName(getContext()) != null) {
else if (AccountUtilKt.getUserName(getContext()) != null) {
final EditText input = new EditText(getActivity());
input.requestFocus();
AlertDialog d = DialogUtil.showAlertDialog(getActivity(),

View file

@ -96,7 +96,7 @@ class NotificationActivity : BaseActivity() {
}
}, { throwable ->
if (throwable is InvalidLoginTokenException) {
val username = sessionManager.getUserName()
val username = sessionManager.userName
val logoutListener = CommonsApplication.BaseLogoutListener(
this,
getString(R.string.invalid_login_message),

View file

@ -11,7 +11,7 @@ import android.view.MotionEvent
import android.view.View
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.AccountUtil
import fr.free.nrw.commons.auth.getUserName
import fr.free.nrw.commons.databinding.ActivityReviewBinding
import fr.free.nrw.commons.delete.DeleteHelper
import fr.free.nrw.commons.media.MediaDetailFragment
@ -183,7 +183,7 @@ class ReviewActivity : BaseActivity() {
}
//If The Media User and Current Session Username is same then Skip the Image
if (media.user == AccountUtil.getUserName(applicationContext)) {
if (media.user == getUserName(applicationContext)) {
runRandomizer()
return
}

View file

@ -63,7 +63,7 @@ class FailedUploadsFragment :
}
if (StringUtils.isEmpty(userName)) {
userName = sessionManager.getUserName()
userName = sessionManager.userName
}
}

View file

@ -19,7 +19,7 @@ class AbstractTextWatcher(
// No-op
}
interface TextChange {
fun interface TextChange {
fun onTextChanged(value: String)
}
}

View file

@ -6,7 +6,6 @@ import android.content.Context
import androidx.collection.LruCache
import com.google.gson.Gson
import com.nhaarman.mockitokotlin2.mock
import fr.free.nrw.commons.auth.AccountUtil
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.CommonsApplicationComponent
import fr.free.nrw.commons.di.CommonsApplicationModule
@ -41,7 +40,6 @@ class TestCommonsApplication : Application() {
class MockCommonsApplicationModule(
appContext: Context,
) : CommonsApplicationModule(appContext) {
val accountUtil: AccountUtil = mock()
val defaultSharedPreferences: JsonKvStore = mock()
val locationServiceManager: LocationServiceManager = mock()
val mockDbOpenHelper: DBOpenHelper = mock()
@ -58,8 +56,6 @@ class MockCommonsApplicationModule(
override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient
override fun providesAccountUtil(context: Context): AccountUtil = accountUtil
override fun providesDefaultKvStore(
context: Context,
gson: Gson,

View file

@ -15,25 +15,17 @@ import org.robolectric.annotation.Config
@Config(sdk = [21], application = TestCommonsApplication::class)
class AccountUtilUnitTest {
private lateinit var context: FakeContextWrapper
private lateinit var accountUtil: AccountUtil
@Before
@Throws(Exception::class)
fun setUp() {
context = FakeContextWrapper(ApplicationProvider.getApplicationContext())
accountUtil = AccountUtil()
}
@Test
@Throws(Exception::class)
fun checkNotNull() {
Assert.assertNotNull(accountUtil)
}
@Test
@Throws(Exception::class)
fun testGetUserName() {
Assert.assertEquals(AccountUtil.getUserName(context), "test@example.com")
Assert.assertEquals(getUserName(context), "test@example.com")
}
@Test
@ -41,13 +33,13 @@ class AccountUtilUnitTest {
fun testGetUserNameWithException() {
val context =
FakeContextWrapperWithException(ApplicationProvider.getApplicationContext())
Assert.assertEquals(AccountUtil.getUserName(context), null)
Assert.assertEquals(getUserName(context), null)
}
@Test
@Throws(Exception::class)
fun testAccount() {
Assert.assertEquals(AccountUtil.account(context)?.name, "test@example.com")
Assert.assertEquals(account(context)?.name, "test@example.com")
}
@Test
@ -55,6 +47,6 @@ class AccountUtilUnitTest {
fun testAccountWithException() {
val context =
FakeContextWrapperWithException(ApplicationProvider.getApplicationContext())
Assert.assertEquals(AccountUtil.account(context), null)
Assert.assertEquals(account(context), null)
}
}

View file

@ -218,17 +218,6 @@ class LoginActivityUnitTests {
method.invoke(activity)
}
@Test
@Throws(Exception::class)
fun testHideProgress() {
val method: Method =
LoginActivity::class.java.getDeclaredMethod(
"hideProgress",
)
method.isAccessible = true
method.invoke(activity)
}
@Test
@Throws(Exception::class)
fun testOnResume() {

View file

@ -3,18 +3,13 @@ package fr.free.nrw.commons.auth
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import java.lang.reflect.Field
class WikiAccountAuthenticatorServiceUnitTest {
private lateinit var service: WikiAccountAuthenticatorService
@Before
fun setUp() {
MockitoAnnotations.openMocks(this)
service = WikiAccountAuthenticatorService()
service.onBind(null)
}
private val service = WikiAccountAuthenticatorService()
@Test
fun checkNotNull() {
@ -23,10 +18,9 @@ class WikiAccountAuthenticatorServiceUnitTest {
@Test
fun testOnBindCaseNull() {
val field: Field =
WikiAccountAuthenticatorService::class.java.getDeclaredField("authenticator")
val field: Field = WikiAccountAuthenticatorService::class.java.getDeclaredField("authenticator")
field.isAccessible = true
field.set(service, null)
Assert.assertEquals(service.onBind(null), null)
Assert.assertEquals(service.onBind(mock()), null)
}
}

View file

@ -86,10 +86,7 @@ class WikiAccountAuthenticatorUnitTest {
@Test
fun testGetAuthTokenLabelCaseNonNull() {
Assert.assertEquals(
authenticator.getAuthTokenLabel(BuildConfig.ACCOUNT_TYPE),
AccountUtil.AUTH_TOKEN_TYPE,
)
Assert.assertEquals(authenticator.getAuthTokenLabel(BuildConfig.ACCOUNT_TYPE), AUTH_TOKEN_TYPE)
}
@Test