mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-11-01 23:33:54 +01:00
Merge branch 'main' into bookmark
This commit is contained in:
commit
0c2e8aa5c2
51 changed files with 1798 additions and 2071 deletions
|
|
@ -17,7 +17,7 @@ class PasteSensitiveTextInputEditTextTest {
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
context = ApplicationProvider.getApplicationContext()
|
context = ApplicationProvider.getApplicationContext()
|
||||||
textView = PasteSensitiveTextInputEditText(context)
|
textView = PasteSensitiveTextInputEditText(context!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
// this test has no real value, just % for test code coverage
|
// this test has no real value, just % for test code coverage
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt
Normal file
24
app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
404
app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt
Normal file
404
app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
95
app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt
Normal file
95
app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
75
app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt
Normal file
75
app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -289,7 +289,7 @@ public class ContributionsFragment
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
notification.setOnClickListener(view -> {
|
notification.setOnClickListener(view -> {
|
||||||
NotificationActivity.startYourself(getContext(), "unread");
|
NotificationActivity.Companion.startYourself(getContext(), "unread");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -414,7 +414,7 @@ public class MainActivity extends BaseActivity
|
||||||
return true;
|
return true;
|
||||||
case R.id.notifications:
|
case R.id.notifications:
|
||||||
// Starts notification activity on click to notification icon
|
// Starts notification activity on click to notification icon
|
||||||
NotificationActivity.startYourself(this, "unread");
|
NotificationActivity.Companion.startYourself(this, "unread");
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
import fr.free.nrw.commons.R;
|
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.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.contributions.ContributionDao;
|
import fr.free.nrw.commons.contributions.ContributionDao;
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao;
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao;
|
||||||
|
|
@ -114,11 +113,6 @@ public class CommonsApplicationModule {
|
||||||
return byName;
|
return byName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
public AccountUtil providesAccountUtil(Context context) {
|
|
||||||
return new AccountUtil();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides an instance of CategoryContentProviderClient i.e. the categories
|
* Provides an instance of CategoryContentProviderClient i.e. the categories
|
||||||
* that are there in local storage
|
* that are there in local storage
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ package fr.free.nrw.commons.feedback;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import fr.free.nrw.commons.R;
|
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.feedback.model.Feedback;
|
||||||
import fr.free.nrw.commons.utils.LangCodeUtils;
|
import fr.free.nrw.commons.utils.LangCodeUtils;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
@ -43,7 +43,7 @@ public class FeedbackContentCreator {
|
||||||
|
|
||||||
sectionTitleBuilder = new StringBuilder();
|
sectionTitleBuilder = new StringBuilder();
|
||||||
sectionTitleBuilder.append("Feedback from ");
|
sectionTitleBuilder.append("Feedback from ");
|
||||||
sectionTitleBuilder.append(AccountUtil.getUserName(context));
|
sectionTitleBuilder.append(AccountUtilKt.getUserName(context));
|
||||||
sectionTitleBuilder.append(" for version ");
|
sectionTitleBuilder.append(" for version ");
|
||||||
sectionTitleBuilder.append(feedback.getVersion());
|
sectionTitleBuilder.append(feedback.getVersion());
|
||||||
sectionTitleBuilder.append(" on ");
|
sectionTitleBuilder.append(" on ");
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ import fr.free.nrw.commons.MediaDataExtractor;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.actions.ThanksClient;
|
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.SessionManager;
|
||||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException;
|
||||||
import fr.free.nrw.commons.category.CategoryClient;
|
import fr.free.nrw.commons.category.CategoryClient;
|
||||||
|
|
@ -382,8 +382,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
||||||
enableProgressBar();
|
enableProgressBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (AccountUtil.getUserName(getContext()) != null && media != null
|
if (AccountUtilKt.getUserName(getContext()) != null && media != null
|
||||||
&& AccountUtil.getUserName(getContext()).equals(media.getAuthor())) {
|
&& AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) {
|
||||||
binding.sendThanks.setVisibility(GONE);
|
binding.sendThanks.setVisibility(GONE);
|
||||||
} else {
|
} else {
|
||||||
binding.sendThanks.setVisibility(VISIBLE);
|
binding.sendThanks.setVisibility(VISIBLE);
|
||||||
|
|
@ -485,7 +485,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onDeletionPageExists(Boolean deletionPageExists) {
|
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.nominateDeletion.setVisibility(GONE);
|
||||||
binding.nominatedDeletionBanner.setVisibility(GONE);
|
binding.nominatedDeletionBanner.setVisibility(GONE);
|
||||||
} else if (deletionPageExists) {
|
} else if (deletionPageExists) {
|
||||||
|
|
@ -1079,7 +1079,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
|
||||||
|
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid")
|
||||||
public void onDeleteButtonClicked(){
|
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(),
|
final ArrayAdapter<String> languageAdapter = new ArrayAdapter<>(getActivity(),
|
||||||
R.layout.simple_spinner_dropdown_list, reasonList);
|
R.layout.simple_spinner_dropdown_list, reasonList);
|
||||||
final Spinner spinner = new Spinner(getActivity());
|
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
|
//Reviewer correct me if i have misunderstood something over here
|
||||||
//But how does this if (delete.getVisibility() == View.VISIBLE) {
|
//But how does this if (delete.getVisibility() == View.VISIBLE) {
|
||||||
// enableDeleteButton(true); makes sense ?
|
// enableDeleteButton(true); makes sense ?
|
||||||
else if (AccountUtil.getUserName(getContext()) != null) {
|
else if (AccountUtilKt.getUserName(getContext()) != null) {
|
||||||
final EditText input = new EditText(getActivity());
|
final EditText input = new EditText(getActivity());
|
||||||
input.requestFocus();
|
input.requestFocus();
|
||||||
AlertDialog d = DialogUtil.showAlertDialog(getActivity(),
|
AlertDialog d = DialogUtil.showAlertDialog(getActivity(),
|
||||||
|
|
|
||||||
|
|
@ -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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
79
app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt
Normal file
79
app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
47
app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt
Normal file
47
app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -965,6 +965,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
lastPlaceToCenter.location.getLatitude() - cameraShift,
|
lastPlaceToCenter.location.getLatitude() - cameraShift,
|
||||||
lastPlaceToCenter.getLocation().getLongitude(), 0));
|
lastPlaceToCenter.getLocation().getLongitude(), 0));
|
||||||
}
|
}
|
||||||
|
highlightNearestPlace(place);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2001,7 +2002,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
||||||
*
|
*
|
||||||
* @param nearestPlace nearest place, which has to be highlighted
|
* @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);
|
passInfoToSheet(nearestPlace);
|
||||||
hideBottomSheet();
|
hideBottomSheet();
|
||||||
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
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
|
* @return returns the drawable of marker according to the place information
|
||||||
*/
|
*/
|
||||||
private @DrawableRes int getIconFor(Place place, Boolean isBookmarked) {
|
private @DrawableRes int getIconFor(Place place, Boolean isBookmarked) {
|
||||||
if (nearestPlace != null) {
|
if (nearestPlace != null && place.name.equals(nearestPlace.name)) {
|
||||||
if (place.name.equals(nearestPlace.name)) {
|
// Highlight nearest place only when user clicks on the home nearby banner
|
||||||
// Highlight nearest place only when user clicks on the home nearby banner
|
// highlightNearestPlace(place);
|
||||||
highlightNearestPlace(place);
|
return (isBookmarked ?
|
||||||
return (isBookmarked ?
|
R.drawable.ic_custom_map_marker_purple_bookmarked :
|
||||||
R.drawable.ic_custom_map_marker_purple_bookmarked :
|
R.drawable.ic_custom_map_marker_purple
|
||||||
R.drawable.ic_custom_map_marker_purple);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (place.isMonument()) {
|
if (place.isMonument()) {
|
||||||
return R.drawable.ic_custom_map_marker_monuments;
|
return R.drawable.ic_custom_map_marker_monuments;
|
||||||
} else if (!place.pic.trim().isEmpty()) {
|
}
|
||||||
|
if (!place.pic.trim().isEmpty()) {
|
||||||
return (isBookmarked ?
|
return (isBookmarked ?
|
||||||
R.drawable.ic_custom_map_marker_green_bookmarked :
|
R.drawable.ic_custom_map_marker_green_bookmarked :
|
||||||
R.drawable.ic_custom_map_marker_green);
|
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
|
);
|
||||||
|
}
|
||||||
|
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);
|
return (R.drawable.ic_clear_black_24dp);
|
||||||
}else if (place.name == "") {
|
}
|
||||||
|
if (place.name.isEmpty()) {
|
||||||
return (isBookmarked ?
|
return (isBookmarked ?
|
||||||
R.drawable.ic_custom_map_marker_grey_bookmarked :
|
R.drawable.ic_custom_map_marker_grey_bookmarked :
|
||||||
R.drawable.ic_custom_map_marker_grey);
|
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);
|
|
||||||
}
|
}
|
||||||
|
return (isBookmarked ?
|
||||||
|
R.drawable.ic_custom_map_marker_red_bookmarked :
|
||||||
|
R.drawable.ic_custom_map_marker_red
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ package fr.free.nrw.commons.notification
|
||||||
import fr.free.nrw.commons.notification.models.Notification
|
import fr.free.nrw.commons.notification.models.Notification
|
||||||
import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
|
import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
|
||||||
|
|
||||||
internal class NotificatinAdapter(
|
internal class NotificationAdapter(
|
||||||
onNotificationClicked: (Notification) -> Unit,
|
onNotificationClicked: (Notification) -> Unit,
|
||||||
) : BaseDelegateAdapter<Notification>(
|
) : BaseDelegateAdapter<Notification>(
|
||||||
notificationDelegate(onNotificationClicked),
|
notificationDelegate(onNotificationClicked),
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import fr.free.nrw.commons.Media
|
import fr.free.nrw.commons.Media
|
||||||
import fr.free.nrw.commons.R
|
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.databinding.ActivityReviewBinding
|
||||||
import fr.free.nrw.commons.delete.DeleteHelper
|
import fr.free.nrw.commons.delete.DeleteHelper
|
||||||
import fr.free.nrw.commons.media.MediaDetailFragment
|
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 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()
|
runRandomizer()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ class FailedUploadsFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.isEmpty(userName)) {
|
if (StringUtils.isEmpty(userName)) {
|
||||||
userName = sessionManager.getUserName()
|
userName = sessionManager.userName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ class AbstractTextWatcher(
|
||||||
// No-op
|
// No-op
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TextChange {
|
fun interface TextChange {
|
||||||
fun onTextChanged(value: String)
|
fun onTextChanged(value: String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
android:layout_width="@dimen/dimen_40"
|
android:layout_width="@dimen/dimen_40"
|
||||||
android:layout_height="@dimen/dimen_40"
|
android:layout_height="@dimen/dimen_40"
|
||||||
android:layout_marginLeft="@dimen/standard_gap"
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
android:visibility="gone"></ImageView>
|
android:visibility="gone" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/wikiDataLl"
|
android:id="@+id/wikiDataLl"
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import android.content.Context
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.nhaarman.mockitokotlin2.mock
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
import fr.free.nrw.commons.auth.AccountUtil
|
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper
|
import fr.free.nrw.commons.data.DBOpenHelper
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationComponent
|
import fr.free.nrw.commons.di.CommonsApplicationComponent
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule
|
import fr.free.nrw.commons.di.CommonsApplicationModule
|
||||||
|
|
@ -41,7 +40,6 @@ class TestCommonsApplication : Application() {
|
||||||
class MockCommonsApplicationModule(
|
class MockCommonsApplicationModule(
|
||||||
appContext: Context,
|
appContext: Context,
|
||||||
) : CommonsApplicationModule(appContext) {
|
) : CommonsApplicationModule(appContext) {
|
||||||
val accountUtil: AccountUtil = mock()
|
|
||||||
val defaultSharedPreferences: JsonKvStore = mock()
|
val defaultSharedPreferences: JsonKvStore = mock()
|
||||||
val locationServiceManager: LocationServiceManager = mock()
|
val locationServiceManager: LocationServiceManager = mock()
|
||||||
val mockDbOpenHelper: DBOpenHelper = mock()
|
val mockDbOpenHelper: DBOpenHelper = mock()
|
||||||
|
|
@ -58,8 +56,6 @@ class MockCommonsApplicationModule(
|
||||||
|
|
||||||
override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient
|
override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient
|
||||||
|
|
||||||
override fun providesAccountUtil(context: Context): AccountUtil = accountUtil
|
|
||||||
|
|
||||||
override fun providesDefaultKvStore(
|
override fun providesDefaultKvStore(
|
||||||
context: Context,
|
context: Context,
|
||||||
gson: Gson,
|
gson: Gson,
|
||||||
|
|
|
||||||
|
|
@ -15,25 +15,17 @@ import org.robolectric.annotation.Config
|
||||||
@Config(sdk = [21], application = TestCommonsApplication::class)
|
@Config(sdk = [21], application = TestCommonsApplication::class)
|
||||||
class AccountUtilUnitTest {
|
class AccountUtilUnitTest {
|
||||||
private lateinit var context: FakeContextWrapper
|
private lateinit var context: FakeContextWrapper
|
||||||
private lateinit var accountUtil: AccountUtil
|
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
context = FakeContextWrapper(ApplicationProvider.getApplicationContext())
|
context = FakeContextWrapper(ApplicationProvider.getApplicationContext())
|
||||||
accountUtil = AccountUtil()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun checkNotNull() {
|
|
||||||
Assert.assertNotNull(accountUtil)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testGetUserName() {
|
fun testGetUserName() {
|
||||||
Assert.assertEquals(AccountUtil.getUserName(context), "test@example.com")
|
Assert.assertEquals(getUserName(context), "test@example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -41,13 +33,13 @@ class AccountUtilUnitTest {
|
||||||
fun testGetUserNameWithException() {
|
fun testGetUserNameWithException() {
|
||||||
val context =
|
val context =
|
||||||
FakeContextWrapperWithException(ApplicationProvider.getApplicationContext())
|
FakeContextWrapperWithException(ApplicationProvider.getApplicationContext())
|
||||||
Assert.assertEquals(AccountUtil.getUserName(context), null)
|
Assert.assertEquals(getUserName(context), null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testAccount() {
|
fun testAccount() {
|
||||||
Assert.assertEquals(AccountUtil.account(context)?.name, "test@example.com")
|
Assert.assertEquals(account(context)?.name, "test@example.com")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -55,6 +47,6 @@ class AccountUtilUnitTest {
|
||||||
fun testAccountWithException() {
|
fun testAccountWithException() {
|
||||||
val context =
|
val context =
|
||||||
FakeContextWrapperWithException(ApplicationProvider.getApplicationContext())
|
FakeContextWrapperWithException(ApplicationProvider.getApplicationContext())
|
||||||
Assert.assertEquals(AccountUtil.account(context), null)
|
Assert.assertEquals(account(context), null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -218,17 +218,6 @@ class LoginActivityUnitTests {
|
||||||
method.invoke(activity)
|
method.invoke(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
@Throws(Exception::class)
|
|
||||||
fun testHideProgress() {
|
|
||||||
val method: Method =
|
|
||||||
LoginActivity::class.java.getDeclaredMethod(
|
|
||||||
"hideProgress",
|
|
||||||
)
|
|
||||||
method.isAccessible = true
|
|
||||||
method.invoke(activity)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testOnResume() {
|
fun testOnResume() {
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,13 @@ package fr.free.nrw.commons.auth
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.Mockito.mock
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
|
|
||||||
class WikiAccountAuthenticatorServiceUnitTest {
|
class WikiAccountAuthenticatorServiceUnitTest {
|
||||||
private lateinit var service: WikiAccountAuthenticatorService
|
private val service = WikiAccountAuthenticatorService()
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
MockitoAnnotations.openMocks(this)
|
|
||||||
service = WikiAccountAuthenticatorService()
|
|
||||||
service.onBind(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun checkNotNull() {
|
fun checkNotNull() {
|
||||||
|
|
@ -23,10 +18,9 @@ class WikiAccountAuthenticatorServiceUnitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testOnBindCaseNull() {
|
fun testOnBindCaseNull() {
|
||||||
val field: Field =
|
val field: Field = WikiAccountAuthenticatorService::class.java.getDeclaredField("authenticator")
|
||||||
WikiAccountAuthenticatorService::class.java.getDeclaredField("authenticator")
|
|
||||||
field.isAccessible = true
|
field.isAccessible = true
|
||||||
field.set(service, null)
|
field.set(service, null)
|
||||||
Assert.assertEquals(service.onBind(null), null)
|
Assert.assertEquals(service.onBind(mock()), null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,10 +86,7 @@ class WikiAccountAuthenticatorUnitTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testGetAuthTokenLabelCaseNonNull() {
|
fun testGetAuthTokenLabelCaseNonNull() {
|
||||||
Assert.assertEquals(
|
Assert.assertEquals(authenticator.getAuthTokenLabel(BuildConfig.ACCOUNT_TYPE), AUTH_TOKEN_TYPE)
|
||||||
authenticator.getAuthTokenLabel(BuildConfig.ACCOUNT_TYPE),
|
|
||||||
AccountUtil.AUTH_TOKEN_TYPE,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue