Merge branch 'main' into bookmark

This commit is contained in:
Neel Doshi 2024-11-28 15:55:40 +05:30 committed by GitHub
commit 0c2e8aa5c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 1798 additions and 2071 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

@ -289,7 +289,7 @@ public class ContributionsFragment
});
}
notification.setOnClickListener(view -> {
NotificationActivity.startYourself(getContext(), "unread");
NotificationActivity.Companion.startYourself(getContext(), "unread");
});
}

View file

@ -414,7 +414,7 @@ public class MainActivity extends BaseActivity
return true;
case R.id.notifications:
// Starts notification activity on click to notification icon
NotificationActivity.startYourself(this, "unread");
NotificationActivity.Companion.startYourself(this, "unread");
return true;
default:
return super.onOptionsItemSelected(item);

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

@ -1,238 +0,0 @@
package fr.free.nrw.commons.navtab;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.actions.PageEditClient;
import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetBinding;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.feedback.FeedbackContentCreator;
import fr.free.nrw.commons.feedback.model.Feedback;
import fr.free.nrw.commons.feedback.FeedbackDialog;
import fr.free.nrw.commons.kvstore.BasicKvStore;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.logging.CommonsLogSender;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.review.ReviewActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import io.reactivex.Single;
import io.reactivex.SingleSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
public class MoreBottomSheetFragment extends BottomSheetDialogFragment {
@Inject
CommonsLogSender commonsLogSender;
private TextView moreProfile;
@Inject @Named("default_preferences")
JsonKvStore store;
@Inject
@Named("commons-page-edit")
PageEditClient pageEditClient;
private static final String GITHUB_ISSUES_URL = "https://github.com/commons-app/apps-android-commons/issues";
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
final @NonNull FragmentMoreBottomSheetBinding binding =
FragmentMoreBottomSheetBinding.inflate(inflater, container, false);
moreProfile = binding.moreProfile;
if(store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)){
binding.morePeerReview.setVisibility(View.GONE);
}
binding.moreLogout.setOnClickListener(v -> onLogoutClicked());
binding.moreFeedback.setOnClickListener(v -> onFeedbackClicked());
binding.moreAbout.setOnClickListener(v -> onAboutClicked());
binding.moreTutorial.setOnClickListener(v -> onTutorialClicked());
binding.moreSettings.setOnClickListener(v -> onSettingsClicked());
binding.moreProfile.setOnClickListener(v -> onProfileClicked());
binding.morePeerReview.setOnClickListener(v -> onPeerReviewClicked());
binding.moreFeedbackGithub.setOnClickListener(v -> onFeedbackGithubClicked());
setUserName();
return binding.getRoot();
}
private void onFeedbackGithubClicked() {
final Intent intent;
intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(GITHUB_ISSUES_URL));
startActivity(intent);
}
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
ApplicationlessInjection
.getInstance(requireActivity().getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
}
/**
* Set the username and user achievements level (if available) in navigationHeader.
*/
private void setUserName() {
BasicKvStore store = new BasicKvStore(this.getContext(), getUserName());
String level = store.getString("userAchievementsLevel","0");
if (level.equals("0")) {
moreProfile.setText(getUserName() + " (" + getString(R.string.see_your_achievements) + ")");
}
else {
moreProfile.setText(getUserName() + " (" + getString(R.string.level) + " " + level + ")");
}
}
private String getUserName(){
final AccountManager accountManager = AccountManager.get(getActivity());
final Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE);
if (allAccounts.length != 0) {
return allAccounts[0].name;
}
return "";
}
protected void onLogoutClicked() {
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.logout_verification)
.setCancelable(false)
.setPositiveButton(R.string.yes, (dialog, which) -> {
final CommonsApplication app = (CommonsApplication)
requireContext().getApplicationContext();
app.clearApplicationData(requireContext(), new ActivityLogoutListener(requireActivity(), getContext()));
})
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show();
}
protected void onFeedbackClicked() {
showFeedbackDialog();
}
/**
* Creates and shows a dialog asking feedback from users
*/
private void showFeedbackDialog() {
new FeedbackDialog(getContext(), this::uploadFeedback).show();
}
/**
* uploads feedback data on the server
*/
void uploadFeedback(final Feedback feedback) {
final FeedbackContentCreator feedbackContentCreator = new FeedbackContentCreator(getContext(), feedback);
final Single<Boolean> single =
pageEditClient.createNewSection(
"Commons:Mobile_app/Feedback",
feedbackContentCreator.getSectionTitle(),
feedbackContentCreator.getSectionText(),
"New feedback on version " + feedback.getVersion() + " of the app"
)
.flatMapSingle(Single::just)
.firstOrError();
Single.defer((Callable<SingleSource<Boolean>>) () -> single)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(aBoolean -> {
if (aBoolean) {
Toast.makeText(getContext(), getString(R.string.thanks_feedback), Toast.LENGTH_SHORT)
.show();
} else {
Toast.makeText(getContext(), getString(R.string.error_feedback),
Toast.LENGTH_SHORT).show();
}
});
}
/**
* This method shows the alert dialog when a user wants to send feedback about the app.
*/
private void showAlertDialog() {
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.feedback_sharing_data_alert)
.setCancelable(false)
.setPositiveButton(R.string.ok, (dialog, which) -> sendFeedback())
.show();
}
/**
* This method collects the feedback message and starts the activity with implicit intent
* to available email client.
*/
private void sendFeedback() {
final String technicalInfo = commonsLogSender.getExtraInfo();
final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO);
feedbackIntent.setType("message/rfc822");
feedbackIntent.setData(Uri.parse("mailto:"));
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
new String[]{CommonsApplication.FEEDBACK_EMAIL});
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
CommonsApplication.FEEDBACK_EMAIL_SUBJECT);
feedbackIntent.putExtra(Intent.EXTRA_TEXT, String.format(
"\n\n%s\n%s", CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER, technicalInfo));
try {
startActivity(feedbackIntent);
} catch (final ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
}
}
protected void onAboutClicked() {
final Intent intent = new Intent(getActivity(), AboutActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
requireActivity().startActivity(intent);
}
protected void onTutorialClicked() {
WelcomeActivity.startYourself(getActivity());
}
protected void onSettingsClicked() {
final Intent intent = new Intent(getActivity(), SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
requireActivity().startActivity(intent);
}
protected void onProfileClicked() {
ProfileActivity.startYourself(getActivity(), getUserName(), false);
}
protected void onPeerReviewClicked() {
ReviewActivity.Companion.startYourself(getActivity(), getString(R.string.title_activity_review));
}
}

View file

@ -0,0 +1,242 @@
package fr.free.nrw.commons.navtab
import android.accounts.AccountManager
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import fr.free.nrw.commons.AboutActivity
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener
import fr.free.nrw.commons.R
import fr.free.nrw.commons.WelcomeActivity
import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetBinding
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.feedback.FeedbackContentCreator
import fr.free.nrw.commons.feedback.FeedbackDialog
import fr.free.nrw.commons.feedback.model.Feedback
import fr.free.nrw.commons.kvstore.BasicKvStore
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.logging.CommonsLogSender
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.review.ReviewActivity
import fr.free.nrw.commons.settings.SettingsActivity
import io.reactivex.Single
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
import javax.inject.Named
class MoreBottomSheetFragment : BottomSheetDialogFragment() {
@Inject
lateinit var commonsLogSender: CommonsLogSender
@Inject
@field: Named("default_preferences")
lateinit var store: JsonKvStore
@Inject
@field: Named("commons-page-edit")
lateinit var pageEditClient: PageEditClient
companion object {
private const val GITHUB_ISSUES_URL =
"https://github.com/commons-app/apps-android-commons/issues"
}
private var binding: FragmentMoreBottomSheetBinding? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMoreBottomSheetBinding.inflate(inflater, container, false)
if (store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)) {
binding?.morePeerReview?.visibility = View.GONE
}
binding?.apply {
moreLogout.setOnClickListener { onLogoutClicked() }
moreFeedback.setOnClickListener { onFeedbackClicked() }
moreAbout.setOnClickListener { onAboutClicked() }
moreTutorial.setOnClickListener { onTutorialClicked() }
moreSettings.setOnClickListener { onSettingsClicked() }
moreProfile.setOnClickListener { onProfileClicked() }
morePeerReview.setOnClickListener { onPeerReviewClicked() }
moreFeedbackGithub.setOnClickListener { onFeedbackGithubClicked() }
}
setUserName()
return binding?.root
}
private fun onFeedbackGithubClicked() {
val intent = Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(GITHUB_ISSUES_URL)
}
startActivity(intent)
}
override fun onAttach(context: Context) {
super.onAttach(context)
ApplicationlessInjection
.getInstance(requireActivity().applicationContext)
.commonsApplicationComponent
.inject(this)
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
/**
* Set the username and user achievements level (if available) in navigationHeader.
*/
private fun setUserName() {
val store = BasicKvStore(requireContext(), getUserName())
val level = store.getString("userAchievementsLevel", "0")
binding?.moreProfile?.text = if (level == "0") {
"${getUserName()} (${getString(R.string.see_your_achievements)})"
} else {
"${getUserName()} (${getString(R.string.level)} $level)"
}
}
private fun getUserName(): String {
val accountManager = AccountManager.get(requireActivity())
val allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE)
return if (allAccounts.isNotEmpty()) {
allAccounts[0].name
} else {
""
}
}
fun onLogoutClicked() {
AlertDialog.Builder(requireActivity())
.setMessage(R.string.logout_verification)
.setCancelable(false)
.setPositiveButton(R.string.yes) { _, _ ->
val app = requireContext().applicationContext as CommonsApplication
app.clearApplicationData(requireContext(), ActivityLogoutListener(requireActivity(), requireContext()))
}
.setNegativeButton(R.string.no) { dialog, _ -> dialog.cancel() }
.show()
}
fun onFeedbackClicked() {
showFeedbackDialog()
}
/**
* Creates and shows a dialog asking feedback from users
*/
private fun showFeedbackDialog() {
FeedbackDialog(requireContext()) { uploadFeedback(it) }.show()
}
/**
* Uploads feedback data on the server
*/
@SuppressLint("CheckResult")
fun uploadFeedback(feedback: Feedback) {
val feedbackContentCreator = FeedbackContentCreator(requireContext(), feedback)
val single = pageEditClient.createNewSection(
"Commons:Mobile_app/Feedback",
feedbackContentCreator.sectionTitle,
feedbackContentCreator.sectionText,
"New feedback on version ${feedback.version} of the app"
)
.flatMapSingle { Single.just(it) }
.firstOrError()
Single.defer { single }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { success ->
val messageResId = if (success) {
R.string.thanks_feedback
} else {
R.string.error_feedback
}
Toast.makeText(requireContext(), getString(messageResId), Toast.LENGTH_SHORT).show()
}
}
/**
* This method shows the alert dialog when a user wants to send feedback about the app.
*/
private fun showAlertDialog() {
AlertDialog.Builder(requireActivity())
.setMessage(R.string.feedback_sharing_data_alert)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> sendFeedback() }
.show()
}
/**
* This method collects the feedback message and starts the activity with implicit intent
* to the available email client.
*/
@SuppressLint("IntentReset")
private fun sendFeedback() {
val technicalInfo = commonsLogSender.getExtraInfo()
val feedbackIntent = Intent(Intent.ACTION_SENDTO).apply {
type = "message/rfc822"
data = Uri.parse("mailto:")
putExtra(Intent.EXTRA_EMAIL, arrayOf(CommonsApplication.FEEDBACK_EMAIL))
putExtra(Intent.EXTRA_SUBJECT, CommonsApplication.FEEDBACK_EMAIL_SUBJECT)
putExtra(Intent.EXTRA_TEXT, "\n\n${CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER}\n$technicalInfo")
}
try {
startActivity(feedbackIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show()
}
}
fun onAboutClicked() {
val intent = Intent(activity, AboutActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
requireActivity().startActivity(intent)
}
fun onTutorialClicked() {
WelcomeActivity.startYourself(requireActivity())
}
fun onSettingsClicked() {
val intent = Intent(activity, SettingsActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
requireActivity().startActivity(intent)
}
fun onProfileClicked() {
ProfileActivity.startYourself(requireActivity(), getUserName(), false)
}
fun onPeerReviewClicked() {
ReviewActivity.startYourself(requireActivity(), getString(R.string.title_activity_review))
}
}

View file

@ -1,142 +0,0 @@
package fr.free.nrw.commons.navtab;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetLoggedOutBinding;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.logging.CommonsLogSender;
import fr.free.nrw.commons.settings.SettingsActivity;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
public class MoreBottomSheetLoggedOutFragment extends BottomSheetDialogFragment {
private FragmentMoreBottomSheetLoggedOutBinding binding;
@Inject
CommonsLogSender commonsLogSender;
@Inject
@Named("default_preferences")
JsonKvStore applicationKvStore;
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
binding = FragmentMoreBottomSheetLoggedOutBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
binding.moreLogin.setOnClickListener(v -> onLogoutClicked());
binding.moreFeedback.setOnClickListener(v -> onFeedbackClicked());
binding.moreAbout.setOnClickListener(v -> onAboutClicked());
binding.moreSettings.setOnClickListener(v -> onSettingsClicked());
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
@Override
public void onAttach(@NonNull final Context context) {
super.onAttach(context);
ApplicationlessInjection
.getInstance(requireActivity().getApplicationContext())
.getCommonsApplicationComponent()
.inject(this);
}
public void onLogoutClicked() {
applicationKvStore.putBoolean("login_skipped", false);
final Intent intent = new Intent(getContext(), LoginActivity.class);
requireActivity().finish(); //Kill the activity from which you will go to next activity
startActivity(intent);
}
public void onFeedbackClicked() {
showAlertDialog();
}
/**
* This method shows the alert dialog when a user wants to send feedback about the app.
*/
private void showAlertDialog() {
new AlertDialog.Builder(requireActivity())
.setMessage(R.string.feedback_sharing_data_alert)
.setCancelable(false)
.setPositiveButton(R.string.ok, (dialog, which) -> {
sendFeedback();
})
.show();
}
/**
* This method collects the feedback message and starts and activity with implicit intent to
* available email client.
*/
private void sendFeedback() {
final String technicalInfo = commonsLogSender.getExtraInfo();
final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO);
feedbackIntent.setType("message/rfc822");
feedbackIntent.setData(Uri.parse("mailto:"));
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
new String[]{CommonsApplication.FEEDBACK_EMAIL});
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
CommonsApplication.FEEDBACK_EMAIL_SUBJECT);
feedbackIntent.putExtra(Intent.EXTRA_TEXT, String.format(
"\n\n%s\n%s", CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER, technicalInfo));
try {
startActivity(feedbackIntent);
} catch (final ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
}
}
public void onAboutClicked() {
final Intent intent = new Intent(getActivity(), AboutActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
requireActivity().startActivity(intent);
}
public void onSettingsClicked() {
final Intent intent = new Intent(getActivity(), SettingsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
requireActivity().startActivity(intent);
}
private class BaseLogoutListener implements CommonsApplication.LogoutListener {
@Override
public void onLogoutComplete() {
Timber.d("Logout complete callback received.");
final Intent nearbyIntent = new Intent(
getContext(), LoginActivity.class);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(nearbyIntent);
requireActivity().finish();
}
}
}

View file

@ -0,0 +1,151 @@
package fr.free.nrw.commons.navtab
import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import fr.free.nrw.commons.AboutActivity
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetLoggedOutBinding
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.logging.CommonsLogSender
import fr.free.nrw.commons.settings.SettingsActivity
import javax.inject.Inject
import javax.inject.Named
import timber.log.Timber
class MoreBottomSheetLoggedOutFragment : BottomSheetDialogFragment() {
private var binding: FragmentMoreBottomSheetLoggedOutBinding? = null
@Inject
lateinit var commonsLogSender: CommonsLogSender
@Inject
@field: Named("default_preferences")
lateinit var applicationKvStore: JsonKvStore
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentMoreBottomSheetLoggedOutBinding.inflate(
inflater,
container,
false
)
return binding?.root
}
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
binding?.apply {
moreLogin.setOnClickListener { onLogoutClicked() }
moreFeedback.setOnClickListener { onFeedbackClicked() }
moreAbout.setOnClickListener { onAboutClicked() }
moreSettings.setOnClickListener { onSettingsClicked() }
}
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
}
override fun onAttach(context: Context) {
super.onAttach(context)
ApplicationlessInjection
.getInstance(requireActivity().applicationContext)
.commonsApplicationComponent
.inject(this)
}
fun onLogoutClicked() {
applicationKvStore.putBoolean("login_skipped", false)
val intent = Intent(context, LoginActivity::class.java)
requireActivity().finish() // Kill the activity from which you will go to next activity
startActivity(intent)
}
fun onFeedbackClicked() {
showAlertDialog()
}
/**
* This method shows the alert dialog when a user wants to send feedback about the app.
*/
private fun showAlertDialog() {
AlertDialog.Builder(requireActivity())
.setMessage(R.string.feedback_sharing_data_alert)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ -> sendFeedback() }
.show()
}
/**
* This method collects the feedback message and starts an activity with an implicit intent to
* the available email client.
*/
@SuppressLint("IntentReset")
private fun sendFeedback() {
val technicalInfo = commonsLogSender.getExtraInfo()
val feedbackIntent = Intent(Intent.ACTION_SENDTO).apply {
type = "message/rfc822"
data = Uri.parse("mailto:")
putExtra(Intent.EXTRA_EMAIL, arrayOf(CommonsApplication.FEEDBACK_EMAIL))
putExtra(Intent.EXTRA_SUBJECT, CommonsApplication.FEEDBACK_EMAIL_SUBJECT)
putExtra(
Intent.EXTRA_TEXT,
"\n\n${CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER}\n$technicalInfo"
)
}
try {
startActivity(feedbackIntent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show()
}
}
fun onAboutClicked() {
val intent = Intent(activity, AboutActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
requireActivity().startActivity(intent)
}
fun onSettingsClicked() {
val intent = Intent(activity, SettingsActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
requireActivity().startActivity(intent)
}
private inner class BaseLogoutListener : CommonsApplication.LogoutListener {
override fun onLogoutComplete() {
Timber.d("Logout complete callback received.")
val nearbyIntent = Intent(context, LoginActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(nearbyIntent)
requireActivity().finish()
}
}
}

View file

@ -1,95 +0,0 @@
package fr.free.nrw.commons.navtab;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import fr.free.nrw.commons.bookmarks.BookmarkFragment;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.explore.ExploreFragment;
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
import fr.free.nrw.commons.wikidata.model.EnumCode;
import fr.free.nrw.commons.wikidata.model.EnumCodeMap;
import fr.free.nrw.commons.R;
public enum NavTab implements EnumCode {
CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) {
@NonNull
@Override
public Fragment newInstance() {
return ContributionsFragment.newInstance();
}
},
NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return NearbyParentFragment.newInstance();
}
},
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
@NonNull
@Override
public Fragment newInstance() {
return ExploreFragment.newInstance();
}
},
BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) {
@NonNull
@Override
public Fragment newInstance() {
return BookmarkFragment.newInstance();
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return null;
}
};
private static final EnumCodeMap<NavTab> MAP = new EnumCodeMap<>(NavTab.class);
@StringRes
private final int text;
@DrawableRes
private final int icon;
@NonNull
public static NavTab of(int code) {
return MAP.get(code);
}
public static int size() {
return MAP.size();
}
@StringRes
public int text() {
return text;
}
@DrawableRes
public int icon() {
return icon;
}
@NonNull
public abstract Fragment newInstance();
@Override
public int code() {
// This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal();
}
NavTab(@StringRes int text, @DrawableRes int icon) {
this.text = text;
this.icon = icon;
}
}

View file

@ -0,0 +1,79 @@
package fr.free.nrw.commons.navtab
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import fr.free.nrw.commons.bookmarks.BookmarkFragment
import fr.free.nrw.commons.contributions.ContributionsFragment
import fr.free.nrw.commons.explore.ExploreFragment
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment
import fr.free.nrw.commons.wikidata.model.EnumCode
import fr.free.nrw.commons.wikidata.model.EnumCodeMap
import fr.free.nrw.commons.R
enum class NavTab(
@StringRes private val text: Int,
@DrawableRes private val icon: Int
) : EnumCode {
CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) {
override fun newInstance(): Fragment {
return ContributionsFragment.newInstance()
}
},
NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) {
override fun newInstance(): Fragment {
return NearbyParentFragment.newInstance()
}
},
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
override fun newInstance(): Fragment {
return ExploreFragment.newInstance()
}
},
BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) {
override fun newInstance(): Fragment {
return BookmarkFragment.newInstance()
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
override fun newInstance(): Fragment? {
return null
}
};
companion object {
private val MAP: EnumCodeMap<NavTab> = EnumCodeMap(NavTab::class.java)
@JvmStatic
fun of(code: Int): NavTab {
return MAP[code]
}
@JvmStatic
fun size(): Int {
return MAP.size()
}
}
@StringRes
fun text(): Int {
return text
}
@DrawableRes
fun icon(): Int {
return icon
}
abstract fun newInstance(): Fragment?
override fun code(): Int {
// This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal
}
}

View file

@ -1,38 +0,0 @@
package fr.free.nrw.commons.navtab;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
public class NavTabFragmentPagerAdapter extends FragmentPagerAdapter {
private Fragment currentFragment;
public NavTabFragmentPagerAdapter(FragmentManager mgr) {
super(mgr);
}
@Nullable
public Fragment getCurrentFragment() {
return currentFragment;
}
@Override
public Fragment getItem(int pos) {
return NavTab.of(pos).newInstance();
}
@Override
public int getCount() {
return NavTab.size();
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
currentFragment = ((Fragment) object);
super.setPrimaryItem(container, position, object);
}
}

View file

@ -0,0 +1,36 @@
package fr.free.nrw.commons.navtab
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
class NavTabFragmentPagerAdapter(
mgr: FragmentManager
) : FragmentPagerAdapter(mgr, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
private var currentFragment: Fragment? = null
fun getCurrentFragment(): Fragment? {
return currentFragment
}
override fun getItem(pos: Int): Fragment {
return NavTab.of(pos).newInstance()!!
}
override fun getCount(): Int {
return NavTab.size()
}
override fun setPrimaryItem(
container: ViewGroup,
position: Int,
`object`: Any
) {
currentFragment = `object` as Fragment
super.setPrimaryItem(container, position, `object`)
}
}

View file

@ -1,41 +0,0 @@
package fr.free.nrw.commons.navtab;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Menu;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import fr.free.nrw.commons.contributions.MainActivity;
public class NavTabLayout extends BottomNavigationView {
public NavTabLayout(Context context) {
super(context);
setTabViews();
}
public NavTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setTabViews();
}
public NavTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setTabViews();
}
private void setTabViews() {
if (((MainActivity) getContext()).applicationKvStore.getBoolean("login_skipped") == true) {
for (int i = 0; i < NavTabLoggedOut.size(); i++) {
NavTabLoggedOut navTab = NavTabLoggedOut.of(i);
getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
}
} else {
for (int i = 0; i < NavTab.size(); i++) {
NavTab navTab = NavTab.of(i);
getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
}
}
}
}

View file

@ -0,0 +1,47 @@
package fr.free.nrw.commons.navtab
import android.content.Context
import android.util.AttributeSet
import android.view.Menu
import com.google.android.material.bottomnavigation.BottomNavigationView
import fr.free.nrw.commons.contributions.MainActivity
class NavTabLayout : BottomNavigationView {
constructor(context: Context) : super(context) {
setTabViews()
}
constructor(
context: Context,
attrs: AttributeSet?
) : super(context, attrs) {
setTabViews()
}
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr) {
setTabViews()
}
private fun setTabViews() {
val isLoginSkipped = (context as MainActivity)
.applicationKvStore.getBoolean("login_skipped")
if (isLoginSkipped) {
for (i in 0 until NavTabLoggedOut.size()) {
val navTab = NavTabLoggedOut.of(i)
menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon())
}
} else {
for (i in 0 until NavTab.size()) {
val navTab = NavTab.of(i)
menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon())
}
}
}
}

View file

@ -1,79 +0,0 @@
package fr.free.nrw.commons.navtab;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.fragment.app.Fragment;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.BookmarkFragment;
import fr.free.nrw.commons.explore.ExploreFragment;
import fr.free.nrw.commons.wikidata.model.EnumCode;
import fr.free.nrw.commons.wikidata.model.EnumCodeMap;
public enum NavTabLoggedOut implements EnumCode {
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
@NonNull
@Override
public Fragment newInstance() {
return ExploreFragment.newInstance();
}
},
BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) {
@NonNull
@Override
public Fragment newInstance() {
return BookmarkFragment.newInstance();
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return null;
}
};
private static final EnumCodeMap<NavTabLoggedOut> MAP = new EnumCodeMap<>(
NavTabLoggedOut.class);
@StringRes
private final int text;
@DrawableRes
private final int icon;
@NonNull
public static NavTabLoggedOut of(int code) {
return MAP.get(code);
}
public static int size() {
return MAP.size();
}
@StringRes
public int text() {
return text;
}
@DrawableRes
public int icon() {
return icon;
}
@NonNull
public abstract Fragment newInstance();
@Override
public int code() {
// This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal();
}
NavTabLoggedOut(@StringRes int text, @DrawableRes int icon) {
this.text = text;
this.icon = icon;
}
}

View file

@ -0,0 +1,65 @@
package fr.free.nrw.commons.navtab
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.fragment.app.Fragment
import fr.free.nrw.commons.R
import fr.free.nrw.commons.bookmarks.BookmarkFragment
import fr.free.nrw.commons.explore.ExploreFragment
import fr.free.nrw.commons.wikidata.model.EnumCode
import fr.free.nrw.commons.wikidata.model.EnumCodeMap
enum class NavTabLoggedOut(
@StringRes private val text: Int,
@DrawableRes private val icon: Int
) : EnumCode {
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
override fun newInstance(): Fragment {
return ExploreFragment.newInstance()
}
},
BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) {
override fun newInstance(): Fragment {
return BookmarkFragment.newInstance()
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
override fun newInstance(): Fragment? {
return null
}
};
companion object {
private val MAP: EnumCodeMap<NavTabLoggedOut> = EnumCodeMap(NavTabLoggedOut::class.java)
@JvmStatic
fun of(code: Int): NavTabLoggedOut {
return MAP[code]
}
@JvmStatic
fun size(): Int {
return MAP.size()
}
}
@StringRes
fun text(): Int {
return text
}
@DrawableRes
fun icon(): Int {
return icon
}
abstract fun newInstance(): Fragment?
override fun code(): Int {
// This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal
}
}

View file

@ -965,6 +965,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
lastPlaceToCenter.location.getLatitude() - cameraShift,
lastPlaceToCenter.getLocation().getLongitude(), 0));
}
highlightNearestPlace(place);
}
@ -2001,7 +2002,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
*
* @param nearestPlace nearest place, which has to be highlighted
*/
private void highlightNearestPlace(Place nearestPlace) {
private void highlightNearestPlace(final Place nearestPlace) {
binding.bottomSheetDetails.icon.setVisibility(View.VISIBLE);
passInfoToSheet(nearestPlace);
hideBottomSheet();
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
@ -2015,32 +2017,37 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
* @return returns the drawable of marker according to the place information
*/
private @DrawableRes int getIconFor(Place place, Boolean isBookmarked) {
if (nearestPlace != null) {
if (place.name.equals(nearestPlace.name)) {
// Highlight nearest place only when user clicks on the home nearby banner
highlightNearestPlace(place);
return (isBookmarked ?
R.drawable.ic_custom_map_marker_purple_bookmarked :
R.drawable.ic_custom_map_marker_purple);
}
if (nearestPlace != null && place.name.equals(nearestPlace.name)) {
// Highlight nearest place only when user clicks on the home nearby banner
// highlightNearestPlace(place);
return (isBookmarked ?
R.drawable.ic_custom_map_marker_purple_bookmarked :
R.drawable.ic_custom_map_marker_purple
);
}
if (place.isMonument()) {
return R.drawable.ic_custom_map_marker_monuments;
} else if (!place.pic.trim().isEmpty()) {
}
if (!place.pic.trim().isEmpty()) {
return (isBookmarked ?
R.drawable.ic_custom_map_marker_green_bookmarked :
R.drawable.ic_custom_map_marker_green);
} else if (!place.exists) { // Means that the topic of the Wikidata item does not exist in the real world anymore, for instance it is a past event, or a place that was destroyed
R.drawable.ic_custom_map_marker_green
);
}
if (!place.exists) { // Means that the topic of the Wikidata item does not exist in the real world anymore, for instance it is a past event, or a place that was destroyed
return (R.drawable.ic_clear_black_24dp);
}else if (place.name == "") {
}
if (place.name.isEmpty()) {
return (isBookmarked ?
R.drawable.ic_custom_map_marker_grey_bookmarked :
R.drawable.ic_custom_map_marker_grey);
} else {
return (isBookmarked ?
R.drawable.ic_custom_map_marker_red_bookmarked :
R.drawable.ic_custom_map_marker_red);
R.drawable.ic_custom_map_marker_grey
);
}
return (isBookmarked ?
R.drawable.ic_custom_map_marker_red_bookmarked :
R.drawable.ic_custom_map_marker_red
);
}
/**

View file

@ -1,288 +0,0 @@
package fr.free.nrw.commons.notification;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import androidx.recyclerview.widget.DividerItemDecoration;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.google.android.material.snackbar.Snackbar;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.databinding.ActivityNotificationBinding;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
import fr.free.nrw.commons.notification.models.Notification;
import fr.free.nrw.commons.notification.models.NotificationType;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import kotlin.Unit;
import timber.log.Timber;
/**
* Created by root on 18.12.2017.
*/
public class NotificationActivity extends BaseActivity {
private ActivityNotificationBinding binding;
@Inject
NotificationController controller;
@Inject
SessionManager sessionManager;
private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment";
private NotificationWorkerFragment mNotificationWorkerFragment;
private NotificatinAdapter adapter;
private List<Notification> notificationList;
MenuItem notificationMenuItem;
/**
* Boolean isRead is true if this notification activity is for read section of notification.
*/
private boolean isRead;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
isRead = getIntent().getStringExtra("title").equals("read");
binding = ActivityNotificationBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
mNotificationWorkerFragment = (NotificationWorkerFragment) getFragmentManager()
.findFragmentByTag(TAG_NOTIFICATION_WORKER_FRAGMENT);
initListView();
setPageTitle();
setSupportActionBar(binding.toolbar.toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
/**
* If this is unread section of the notifications, removeNotification method
* Marks the notification as read,
* Removes the notification from unread,
* Displays the Snackbar.
*
* Otherwise returns (read section).
*
* @param notification
*/
@SuppressLint("CheckResult")
public void removeNotification(Notification notification) {
if (isRead) {
return;
}
Disposable disposable = Observable.defer((Callable<ObservableSource<Boolean>>)
() -> controller.markAsRead(notification))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
if (result) {
notificationList.remove(notification);
setItems(notificationList);
adapter.notifyDataSetChanged();
ViewUtil.showLongSnackbar(binding.container,getString(R.string.notification_mark_read));
if (notificationList.size() == 0) {
setEmptyView();
binding.container.setVisibility(View.GONE);
binding.noNotificationBackground.setVisibility(View.VISIBLE);
}
} else {
adapter.notifyDataSetChanged();
setItems(notificationList);
ViewUtil.showLongToast(this,getString(R.string.some_error));
}
}, throwable -> {
if (throwable instanceof InvalidLoginTokenException) {
final String username = sessionManager.getUserName();
final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener(
this,
getString(R.string.invalid_login_message),
username
);
CommonsApplication.getInstance().clearApplicationData(
this, logoutListener);
} else {
Timber.e(throwable, "Error occurred while loading notifications");
throwable.printStackTrace();
ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications);
binding.progressBar.setVisibility(View.GONE);
ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications);
}
binding.progressBar.setVisibility(View.GONE);
});
getCompositeDisposable().add(disposable);
}
private void initListView() {
binding.listView.setLayoutManager(new LinearLayoutManager(this));
DividerItemDecoration itemDecor = new DividerItemDecoration(binding.listView.getContext(), DividerItemDecoration.VERTICAL);
binding.listView.addItemDecoration(itemDecor);
if (isRead) {
refresh(true);
} else {
refresh(false);
}
adapter = new NotificatinAdapter(item -> {
Timber.d("Notification clicked %s", item.getLink());
if (item.getNotificationType() == NotificationType.EMAIL){
ViewUtil.showLongSnackbar(binding.container,getString(R.string.check_your_email_inbox));
} else {
handleUrl(item.getLink());
}
removeNotification(item);
return Unit.INSTANCE;
});
binding.listView.setAdapter(adapter);
}
private void refresh(boolean archived) {
if (!NetworkUtils.isInternetConnectionEstablished(this)) {
binding.progressBar.setVisibility(View.GONE);
Snackbar.make(binding.container, R.string.no_internet, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.retry, view -> refresh(archived)).show();
} else {
addNotifications(archived);
}
binding.progressBar.setVisibility(View.VISIBLE);
binding.noNotificationBackground.setVisibility(View.GONE);
binding.container.setVisibility(View.VISIBLE);
}
@SuppressLint("CheckResult")
private void addNotifications(boolean archived) {
Timber.d("Add notifications");
if (mNotificationWorkerFragment == null) {
binding.progressBar.setVisibility(View.VISIBLE);
getCompositeDisposable().add(controller.getNotifications(archived)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(notificationList -> {
Collections.reverse(notificationList);
Timber.d("Number of notifications is %d", notificationList.size());
this.notificationList = notificationList;
if (notificationList.size()==0){
setEmptyView();
binding.container.setVisibility(View.GONE);
binding.noNotificationBackground.setVisibility(View.VISIBLE);
} else {
setItems(notificationList);
}
binding.progressBar.setVisibility(View.GONE);
}, throwable -> {
Timber.e(throwable, "Error occurred while loading notifications ");
ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications);
binding.progressBar.setVisibility(View.GONE);
}));
} else {
notificationList = mNotificationWorkerFragment.getNotificationList();
setItems(notificationList);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_notifications, menu);
notificationMenuItem = menu.findItem(R.id.archived);
setMenuItemTitle();
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.archived:
if (item.getTitle().equals(getString(R.string.menu_option_read))) {
NotificationActivity.startYourself(NotificationActivity.this, "read");
}else if (item.getTitle().equals(getString(R.string.menu_option_unread))) {
onBackPressed();
}
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void handleUrl(String url) {
if (url == null || url.equals("")) {
return;
}
Utils.handleWebUrl(this, Uri.parse(url));
}
private void setItems(List<Notification> notificationList) {
if (notificationList == null || notificationList.isEmpty()) {
ViewUtil.showShortSnackbar(binding.container, R.string.no_notifications);
/*progressBar.setVisibility(View.GONE);
recyclerView.setVisibility(View.GONE);*/
binding.container.setVisibility(View.GONE);
setEmptyView();
binding.noNotificationBackground.setVisibility(View.VISIBLE);
return;
}
binding.container.setVisibility(View.VISIBLE);
binding.noNotificationBackground.setVisibility(View.GONE);
adapter.setItems(notificationList);
}
public static void startYourself(Context context, String title) {
Intent intent = new Intent(context, NotificationActivity.class);
intent.putExtra("title", title);
context.startActivity(intent);
}
private void setPageTitle() {
if (getSupportActionBar() != null) {
if (isRead) {
getSupportActionBar().setTitle(R.string.read_notifications);
} else {
getSupportActionBar().setTitle(R.string.notifications);
}
}
}
private void setEmptyView() {
if (isRead) {
binding.noNotificationText.setText(R.string.no_read_notification);
}else {
binding.noNotificationText.setText(R.string.no_notification);
}
}
private void setMenuItemTitle() {
if (isRead) {
notificationMenuItem.setTitle(R.string.menu_option_unread);
}else {
notificationMenuItem.setTitle(R.string.menu_option_read);
}
}
}

View file

@ -0,0 +1,247 @@
package fr.free.nrw.commons.notification
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.databinding.ActivityNotificationBinding
import fr.free.nrw.commons.notification.models.Notification
import fr.free.nrw.commons.notification.models.NotificationType
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.NetworkUtils
import fr.free.nrw.commons.utils.ViewUtil
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import javax.inject.Inject
/**
* Created by root on 18.12.2017.
*/
class NotificationActivity : BaseActivity() {
private lateinit var binding: ActivityNotificationBinding
@Inject
lateinit var controller: NotificationController
@Inject
lateinit var sessionManager: SessionManager
private val tagNotificationWorkerFragment = "NotificationWorkerFragment"
private var mNotificationWorkerFragment: NotificationWorkerFragment? = null
private lateinit var adapter: NotificationAdapter
private var notificationList: MutableList<Notification> = mutableListOf()
private var notificationMenuItem: MenuItem? = null
/**
* Boolean isRead is true if this notification activity is for read section of notification.
*/
private var isRead: Boolean = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isRead = intent.getStringExtra("title") == "read"
binding = ActivityNotificationBinding.inflate(layoutInflater)
setContentView(binding.root)
mNotificationWorkerFragment = supportFragmentManager.findFragmentByTag(
tagNotificationWorkerFragment
) as? NotificationWorkerFragment
initListView()
setPageTitle()
setSupportActionBar(binding.toolbar.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
@SuppressLint("CheckResult", "NotifyDataSetChanged")
fun removeNotification(notification: Notification) {
if (isRead) return
val disposable = Observable.defer { controller.markAsRead(notification) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ result ->
if (result) {
notificationList.remove(notification)
setItems(notificationList)
adapter.notifyDataSetChanged()
ViewUtil.showLongSnackbar(binding.container, getString(R.string.notification_mark_read))
if (notificationList.isEmpty()) {
setEmptyView()
binding.container.visibility = View.GONE
binding.noNotificationBackground.visibility = View.VISIBLE
}
} else {
adapter.notifyDataSetChanged()
setItems(notificationList)
ViewUtil.showLongToast(this, getString(R.string.some_error))
}
}, { throwable ->
if (throwable is InvalidLoginTokenException) {
val username = sessionManager.userName
val logoutListener = CommonsApplication.BaseLogoutListener(
this,
getString(R.string.invalid_login_message),
username
)
CommonsApplication.instance.clearApplicationData(this, logoutListener)
} else {
Timber.e(throwable, "Error occurred while loading notifications")
ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications)
}
binding.progressBar.visibility = View.GONE
})
compositeDisposable.add(disposable)
}
private fun initListView() {
binding.listView.layoutManager = LinearLayoutManager(this)
val itemDecor = DividerItemDecoration(binding.listView.context, DividerItemDecoration.VERTICAL)
binding.listView.addItemDecoration(itemDecor)
refresh(isRead)
adapter = NotificationAdapter { item ->
Timber.d("Notification clicked %s", item.link)
if (item.notificationType == NotificationType.EMAIL) {
ViewUtil.showLongSnackbar(binding.container, getString(R.string.check_your_email_inbox))
} else {
handleUrl(item.link)
}
removeNotification(item)
}
binding.listView.adapter = adapter
}
private fun refresh(archived: Boolean) {
if (!NetworkUtils.isInternetConnectionEstablished(this)) {
binding.progressBar.visibility = View.GONE
Snackbar.make(binding.container, R.string.no_internet, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.retry) { refresh(archived) }
.show()
} else {
addNotifications(archived)
}
binding.progressBar.visibility = View.VISIBLE
binding.noNotificationBackground.visibility = View.GONE
binding.container.visibility = View.VISIBLE
}
@SuppressLint("CheckResult")
private fun addNotifications(archived: Boolean) {
Timber.d("Add notifications")
if (mNotificationWorkerFragment == null) {
binding.progressBar.visibility = View.VISIBLE
compositeDisposable.add(controller.getNotifications(archived)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ notificationList ->
notificationList.reversed()
Timber.d("Number of notifications is %d", notificationList.size)
this.notificationList = notificationList.toMutableList()
if (notificationList.isEmpty()) {
setEmptyView()
binding.container.visibility = View.GONE
binding.noNotificationBackground.visibility = View.VISIBLE
} else {
setItems(notificationList)
}
binding.progressBar.visibility = View.GONE
}, { throwable ->
Timber.e(throwable, "Error occurred while loading notifications")
ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications)
binding.progressBar.visibility = View.GONE
}))
} else {
notificationList = mNotificationWorkerFragment?.notificationList?.toMutableList() ?: mutableListOf()
setItems(notificationList)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_notifications, menu)
notificationMenuItem = menu.findItem(R.id.archived)
setMenuItemTitle()
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.archived -> {
if (item.title == getString(R.string.menu_option_read)) {
startYourself(this, "read")
} else if (item.title == getString(R.string.menu_option_unread)) {
onBackPressed()
}
true
}
else -> super.onOptionsItemSelected(item)
}
}
private fun handleUrl(url: String?) {
if (url.isNullOrEmpty()) return
Utils.handleWebUrl(this, Uri.parse(url))
}
private fun setItems(notificationList: List<Notification>?) {
if (notificationList.isNullOrEmpty()) {
ViewUtil.showShortSnackbar(binding.container, R.string.no_notifications)
binding.container.visibility = View.GONE
setEmptyView()
binding.noNotificationBackground.visibility = View.VISIBLE
return
}
binding.container.visibility = View.VISIBLE
binding.noNotificationBackground.visibility = View.GONE
adapter.items = notificationList
}
private fun setPageTitle() {
supportActionBar?.title = if (isRead) {
getString(R.string.read_notifications)
} else {
getString(R.string.notifications)
}
}
private fun setEmptyView() {
binding.noNotificationText.text = if (isRead) {
getString(R.string.no_read_notification)
} else {
getString(R.string.no_notification)
}
}
private fun setMenuItemTitle() {
notificationMenuItem?.title = if (isRead) {
getString(R.string.menu_option_unread)
} else {
getString(R.string.menu_option_read)
}
}
companion object {
fun startYourself(context: Context, title: String) {
val intent = Intent(context, NotificationActivity::class.java)
intent.putExtra("title", title)
context.startActivity(intent)
}
}
}

View file

@ -3,7 +3,7 @@ package fr.free.nrw.commons.notification
import fr.free.nrw.commons.notification.models.Notification
import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
internal class NotificatinAdapter(
internal class NotificationAdapter(
onNotificationClicked: (Notification) -> Unit,
) : BaseDelegateAdapter<Notification>(
notificationDelegate(onNotificationClicked),

View file

@ -1,33 +0,0 @@
package fr.free.nrw.commons.notification;
import fr.free.nrw.commons.notification.models.Notification;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
import io.reactivex.Observable;
import io.reactivex.Single;
/**
* Created by root on 19.12.2017.
*/
@Singleton
public class NotificationController {
private NotificationClient notificationClient;
@Inject
public NotificationController(NotificationClient notificationClient) {
this.notificationClient = notificationClient;
}
public Single<List<Notification>> getNotifications(boolean archived) {
return notificationClient.getNotifications(archived);
}
Observable<Boolean> markAsRead(Notification notification) {
return notificationClient.markNotificationAsRead(notification.getNotificationId());
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.notification
import fr.free.nrw.commons.notification.models.Notification
import javax.inject.Inject
import javax.inject.Singleton
import io.reactivex.Observable
import io.reactivex.Single
/**
* Created by root on 19.12.2017.
*/
@Singleton
class NotificationController @Inject constructor(
private val notificationClient: NotificationClient
) {
fun getNotifications(archived: Boolean): Single<List<Notification>> {
return notificationClient.getNotifications(archived)
}
fun markAsRead(notification: Notification): Observable<Boolean> {
return notificationClient.markNotificationAsRead(notification.notificationId)
}
}

View file

@ -1,73 +0,0 @@
package fr.free.nrw.commons.notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.core.app.NotificationCompat;
import javax.inject.Inject;
import javax.inject.Singleton;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import static androidx.core.app.NotificationCompat.DEFAULT_ALL;
import static androidx.core.app.NotificationCompat.PRIORITY_HIGH;
/**
* Helper class that can be used to build a generic notification
* Going forward all notifications should be built using this helper class
*/
@Singleton
public class NotificationHelper {
public static final int NOTIFICATION_DELETE = 1;
public static final int NOTIFICATION_EDIT_CATEGORY = 2;
public static final int NOTIFICATION_EDIT_COORDINATES = 3;
public static final int NOTIFICATION_EDIT_DESCRIPTION = 4;
public static final int NOTIFICATION_EDIT_DEPICTIONS = 5;
private final NotificationManager notificationManager;
private final NotificationCompat.Builder notificationBuilder;
@Inject
public NotificationHelper(final Context context) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationBuilder = new NotificationCompat
.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)
.setOnlyAlertOnce(true);
}
/**
* Public interface to build and show a notification in the notification bar
* @param context passed context
* @param notificationTitle title of the notification
* @param notificationMessage message to be displayed in the notification
* @param notificationId the notificationID
* @param intent the intent to be fired when the notification is clicked
*/
public void showNotification(
final Context context,
final String notificationTitle,
final String notificationMessage,
final int notificationId,
final Intent intent
) {
notificationBuilder.setDefaults(DEFAULT_ALL)
.setContentTitle(notificationTitle)
.setStyle(new NotificationCompat.BigTextStyle()
.bigText(notificationMessage))
.setSmallIcon(R.drawable.ic_launcher)
.setProgress(0, 0, false)
.setOngoing(false)
.setPriority(PRIORITY_HIGH);
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
flags |= PendingIntent.FLAG_IMMUTABLE; // This flag was introduced in API 23
}
final PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, flags);
notificationBuilder.setContentIntent(pendingIntent);
notificationManager.notify(notificationId, notificationBuilder.build());
}
}

View file

@ -0,0 +1,73 @@
package fr.free.nrw.commons.notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import javax.inject.Inject
import javax.inject.Singleton
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.R
import androidx.core.app.NotificationCompat.DEFAULT_ALL
import androidx.core.app.NotificationCompat.PRIORITY_HIGH
/**
* Helper class that can be used to build a generic notification
* Going forward all notifications should be built using this helper class
*/
@Singleton
class NotificationHelper @Inject constructor(
context: Context
) {
companion object {
const val NOTIFICATION_DELETE = 1
const val NOTIFICATION_EDIT_CATEGORY = 2
const val NOTIFICATION_EDIT_COORDINATES = 3
const val NOTIFICATION_EDIT_DESCRIPTION = 4
const val NOTIFICATION_EDIT_DEPICTIONS = 5
}
private val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val notificationBuilder: NotificationCompat.Builder = NotificationCompat
.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL)
.setOnlyAlertOnce(true)
/**
* Public interface to build and show a notification in the notification bar
* @param context passed context
* @param notificationTitle title of the notification
* @param notificationMessage message to be displayed in the notification
* @param notificationId the notificationID
* @param intent the intent to be fired when the notification is clicked
*/
fun showNotification(
context: Context,
notificationTitle: String,
notificationMessage: String,
notificationId: Int,
intent: Intent
) {
notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL)
.setContentTitle(notificationTitle)
.setStyle(NotificationCompat.BigTextStyle().bigText(notificationMessage))
.setSmallIcon(R.drawable.ic_launcher)
.setProgress(0, 0, false)
.setOngoing(false)
.setPriority(NotificationCompat.PRIORITY_HIGH)
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
val pendingIntent = PendingIntent.getActivity(context, 1, intent, flags)
notificationBuilder.setContentIntent(pendingIntent)
notificationManager.notify(notificationId, notificationBuilder.build())
}
}

View file

@ -1,31 +0,0 @@
package fr.free.nrw.commons.notification;
import android.app.Fragment;
import android.os.Bundle;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.notification.models.Notification;
import java.util.List;
/**
* Created by knightshade on 25/2/18.
*/
public class NotificationWorkerFragment extends Fragment {
private List<Notification> notificationList;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
public void setNotificationList(List<Notification> notificationList){
this.notificationList = notificationList;
}
public List<Notification> getNotificationList(){
return notificationList;
}
}

View file

@ -0,0 +1,20 @@
package fr.free.nrw.commons.notification
import android.app.Fragment
import android.os.Bundle
import fr.free.nrw.commons.notification.models.Notification
/**
* Created by knightshade on 25/2/18.
*/
class NotificationWorkerFragment : Fragment() {
var notificationList: List<Notification>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
retainInstance = true
}
}

View file

@ -1,28 +0,0 @@
package fr.free.nrw.commons.notification.models;
public enum NotificationType {
THANK_YOU_EDIT("thank-you-edit"),
EDIT_USER_TALK("edit-user-talk"),
MENTION("mention"),
EMAIL("email"),
WELCOME("welcome"),
UNKNOWN("unknown");
private String type;
NotificationType(String type) {
this.type = type;
}
public String getType() {
return type;
}
public static NotificationType handledValueOf(String name) {
for (NotificationType e : values()) {
if (e.getType().equals(name)) {
return e;
}
}
return UNKNOWN;
}
}

View file

@ -0,0 +1,33 @@
package fr.free.nrw.commons.notification.models
enum class NotificationType(private val type: String) {
THANK_YOU_EDIT("thank-you-edit"),
EDIT_USER_TALK("edit-user-talk"),
MENTION("mention"),
EMAIL("email"),
WELCOME("welcome"),
UNKNOWN("unknown");
// Getter for the type property
fun getType(): String {
return type
}
companion object {
// Returns the corresponding NotificationType for a given name or UNKNOWN
// if no match is found
fun handledValueOf(name: String): NotificationType {
for (e in values()) {
if (e.type == name) {
return e
}
}
return UNKNOWN
}
}
}

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

@ -32,7 +32,7 @@
android:layout_width="@dimen/dimen_40"
android:layout_height="@dimen/dimen_40"
android:layout_marginLeft="@dimen/standard_gap"
android:visibility="gone"></ImageView>
android:visibility="gone" />
<LinearLayout
android:id="@+id/wikiDataLl"

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