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