Precise error message password change (#5544)

* Precise Error Message and Action When Password is Changed

* Make message persistent

* code cleanup and fixes

* removed unnecessary string resource

* removed unnecessary string resource

* fix

* fix

* Upload Client to kotlin

* Remove Redundant Code

* Remove Redundant Code

* code cleanup and Improvements

* Improved Java doc

* Improved Java doc

* Improved Javadoc
This commit is contained in:
Shashank Kumar 2024-03-21 23:21:53 +05:30 committed by GitHub
parent 152e824aa6
commit 2a6ab66c11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 171 additions and 54 deletions

View file

@ -9,9 +9,11 @@ import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_COMMENT;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.os.Build;
@ -22,6 +24,7 @@ import androidx.multidex.MultiDexApplication;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.core.ImagePipelineConfig;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
@ -33,6 +36,7 @@ import fr.free.nrw.commons.contributions.ContributionDao;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.language.AppLanguageLookUpTable;
import fr.free.nrw.commons.logging.FileLoggingTree;
import fr.free.nrw.commons.logging.LogUtils;
import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher;
@ -57,7 +61,6 @@ import org.acra.annotation.AcraCore;
import org.acra.annotation.AcraDialog;
import org.acra.annotation.AcraMailSender;
import org.acra.data.StringFormat;
import fr.free.nrw.commons.language.AppLanguageLookUpTable;
import timber.log.Timber;
@AcraCore(
@ -82,6 +85,9 @@ import timber.log.Timber;
public class CommonsApplication extends MultiDexApplication {
public static final String loginMessageIntentKey = "loginMessage";
public static final String loginUsernameIntentKey = "loginUsername";
public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
@Inject
SessionManager sessionManager;
@ -339,4 +345,96 @@ public class CommonsApplication extends MultiDexApplication {
void onLogoutComplete();
}
/**
* This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity
* with relevant intent parameters. It does not perform the actual logout operation.
*/
public static class BaseLogoutListener implements CommonsApplication.LogoutListener {
Context ctx;
String loginMessage, userName;
/**
* Constructor for BaseLogoutListener.
*
* @param ctx Application context
*/
public BaseLogoutListener(final Context ctx) {
this.ctx = ctx;
}
/**
* Constructor for BaseLogoutListener
*
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
* @param loginMessage Message to be displayed on the login page
* @param loginUsername Username to be pre-filled on the login page
*/
public BaseLogoutListener(final Context ctx, final String loginMessage,
final String loginUsername) {
this.ctx = ctx;
this.loginMessage = loginMessage;
this.userName = loginUsername;
}
@Override
public void onLogoutComplete() {
Timber.d("Logout complete callback received.");
final Intent loginIntent = new Intent(ctx, LoginActivity.class);
loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (loginMessage != null) {
loginIntent.putExtra(loginMessageIntentKey, loginMessage);
}
if (userName != null) {
loginIntent.putExtra(loginUsernameIntentKey, userName);
}
ctx.startActivity(loginIntent);
}
}
/**
* This class is an extension of BaseLogoutListener, providing additional functionality or customization
* for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen.
*/
public static class ActivityLogoutListener extends BaseLogoutListener {
Activity activity;
/**
* Constructor for ActivityLogoutListener.
*
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
*/
public ActivityLogoutListener(final Activity activity, final Context ctx) {
super(ctx);
this.activity = activity;
}
/**
* Constructor for ActivityLogoutListener with additional parameters for the login screen.
*
* @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity.
* @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process.
* @param loginMessage Message to be displayed on the login page after logout.
* @param loginUsername Username to be pre-filled on the login page after logout.
*/
public ActivityLogoutListener(final Activity activity, final Context ctx,
final String loginMessage, final String loginUsername) {
super(activity, loginMessage, loginUsername);
this.activity = activity;
}
@Override
public void onLogoutComplete() {
super.onLogoutComplete();
activity.finish();
}
}
}

View file

@ -24,7 +24,6 @@ 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;
@ -51,6 +50,8 @@ 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.loginMessageIntentKey;
import static fr.free.nrw.commons.CommonsApplication.loginUsernameIntentKey;
public class LoginActivity extends AccountAuthenticatorActivity {
@ -93,6 +94,9 @@ public class LoginActivity extends AccountAuthenticatorActivity {
binding = ActivityLoginBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
String message = getIntent().getStringExtra(loginMessageIntentKey);
String username = getIntent().getStringExtra(loginUsernameIntentKey);
binding.loginUsername.addTextChangedListener(textWatcher);
binding.loginPassword.addTextChangedListener(textWatcher);
binding.loginTwoFactor.addTextChangedListener(textWatcher);
@ -111,6 +115,12 @@ public class LoginActivity extends AccountAuthenticatorActivity {
} 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).

View file

@ -23,6 +23,7 @@ class CsrfTokenClient(
private var retries = 0
private var csrfTokenCall: Call<MwQueryResponse?>? = null
@Throws(Throwable::class)
fun getTokenBlocking(): String {
var token = ""
@ -56,7 +57,7 @@ class CsrfTokenClient(
}
if (token.isEmpty() || token == ANON_TOKEN) {
throw IOException("Invalid token, or login failure.")
throw IOException(INVALID_TOKEN_ERROR_MESSAGE)
}
return token
}
@ -159,5 +160,6 @@ class CsrfTokenClient(
private const val ANON_TOKEN = "+\\"
private const val MAX_RETRIES = 1
private const val MAX_RETRIES_OF_LOGIN_BLOCKING = 2
const val INVALID_TOKEN_ERROR_MESSAGE = "Invalid token, or login failure."
}
}

View file

@ -19,10 +19,10 @@ 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.auth.LoginActivity;
import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetBinding;
import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.feedback.FeedbackContentCreator;
@ -41,7 +41,6 @@ import io.reactivex.schedulers.Schedulers;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
public class MoreBottomSheetFragment extends BottomSheetDialogFragment {
@ -122,7 +121,7 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment {
.setPositiveButton(R.string.yes, (dialog, which) -> {
final CommonsApplication app = (CommonsApplication)
requireContext().getApplicationContext();
app.clearApplicationData(requireContext(), new BaseLogoutListener());
app.clearApplicationData(requireContext(), new ActivityLogoutListener(requireActivity(), getContext()));
})
.setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
.show();
@ -221,19 +220,5 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment {
protected void onPeerReviewClicked() {
ReviewActivity.startYourself(getActivity(), getString(R.string.title_activity_review));
}
private class BaseLogoutListener implements CommonsApplication.LogoutListener {
@Override
public void onLogoutComplete() {
Timber.d("Logout complete callback received.");
final Intent nearbyIntent = new Intent(
getContext(), LoginActivity.class);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(nearbyIntent);
requireActivity().finish();
}
}
}

View file

@ -70,10 +70,10 @@ import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding3.appcompat.RxSearchView;
import fr.free.nrw.commons.BaseMarker;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.CommonsApplication.BaseLogoutListener;
import fr.free.nrw.commons.MapController.NearbyPlacesInfo;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.contributions.MainActivity;
@ -1372,8 +1372,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
.setMessage(R.string.login_alert_message)
.setPositiveButton(R.string.login, (dialog, which) -> {
// logout of the app
BaseLogoutListener logoutListener = new BaseLogoutListener();
CommonsApplication app = (CommonsApplication) getActivity().getApplication();
BaseLogoutListener logoutListener = new BaseLogoutListener(getActivity()); CommonsApplication app = (CommonsApplication) getActivity().getApplication();
app.clearApplicationData(getContext(), logoutListener);
})
.show();
@ -1419,18 +1418,6 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
* onLogoutComplete is called after shared preferences and data stored in local database are
* cleared.
*/
private class BaseLogoutListener implements CommonsApplication.LogoutListener {
@Override
public void onLogoutComplete() {
Timber.d("Logout complete callback received.");
final Intent nearbyIntent = new Intent(getActivity(), LoginActivity.class);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(nearbyIntent);
getActivity().finish();
}
}
@Override
public void setFABPlusAction(final View.OnClickListener onClickListener) {

View file

@ -2,7 +2,8 @@ package fr.free.nrw.commons.upload
data class StashUploadResult(
val state: StashUploadState,
val fileKey: String?
val fileKey: String?,
val errorMessage : String?
)
enum class StashUploadState {

View file

@ -54,7 +54,7 @@ class UploadClient @Inject constructor(
): Observable<StashUploadResult> {
if (contribution.isCompleted()) {
return Observable.just(
StashUploadResult(StashUploadState.SUCCESS, contribution.fileKey)
StashUploadResult(StashUploadState.SUCCESS, contribution.fileKey,null)
)
}
@ -76,12 +76,13 @@ class UploadClient @Inject constructor(
val index = AtomicInteger()
val failures = AtomicBoolean()
val errorMessage = AtomicReference<String>()
compositeDisposable.add(
Observable.fromIterable(fileChunks).forEach { chunkFile: File ->
if (canProcess(contribution, failures)) {
processChunk(
filename, contribution, notificationUpdater, chunkFile,
failures, chunkInfo, index, mediaType!!, file!!, fileChunks.size
failures, chunkInfo, index, errorMessage, mediaType!!, file!!, fileChunks.size
)
}
}
@ -90,24 +91,25 @@ class UploadClient @Inject constructor(
return when {
contribution.isPaused() -> {
Timber.d("Upload stash paused %s", contribution.pageId)
Observable.just(StashUploadResult(StashUploadState.PAUSED, null))
Observable.just(StashUploadResult(StashUploadState.PAUSED, null, null))
}
failures.get() -> {
Timber.d("Upload stash contains failures %s", contribution.pageId)
Observable.just(StashUploadResult(StashUploadState.FAILED, null))
Observable.just(StashUploadResult(StashUploadState.FAILED, null, errorMessage.get()))
}
chunkInfo.get() != null -> {
Timber.d("Upload stash success %s", contribution.pageId)
Observable.just(
StashUploadResult(
StashUploadState.SUCCESS,
chunkInfo.get()!!.uploadResult!!.filekey
chunkInfo.get()!!.uploadResult!!.filekey,
"success"
)
)
}
else -> {
Timber.d("Upload stash failed %s", contribution.pageId)
Observable.just(StashUploadResult(StashUploadState.FAILED, null))
Observable.just(StashUploadResult(StashUploadState.FAILED, null,null))
}
}
}
@ -116,7 +118,7 @@ class UploadClient @Inject constructor(
filename: String, contribution: Contribution,
notificationUpdater: NotificationUpdateProgressListener, chunkFile: File,
failures: AtomicBoolean, chunkInfo: AtomicReference<ChunkInfo?>, index: AtomicInteger,
mediaType: MediaType, file: File, totalChunks: Int
errorMessage : AtomicReference<String>, mediaType: MediaType, file: File, totalChunks: Int
) {
if (shouldSkip(chunkInfo, index)) {
index.incrementAndGet()
@ -150,6 +152,7 @@ class UploadClient @Inject constructor(
notificationUpdater.onChunkUploaded(contribution, chunkInfo.get())
}, { throwable: Throwable? ->
Timber.e(throwable, "Received error in chunk upload")
errorMessage.set(throwable?.message)
failures.set(true)
}
)

View file

@ -10,16 +10,17 @@ import android.graphics.BitmapFactory
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.multidex.BuildConfig
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import androidx.multidex.BuildConfig
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import dagger.android.ContributesAndroidInjector
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.contributions.ChunkInfo
import fr.free.nrw.commons.contributions.Contribution
import fr.free.nrw.commons.contributions.ContributionDao
@ -29,8 +30,8 @@ import fr.free.nrw.commons.customselector.database.UploadedStatusDao
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.StashUploadResult
import fr.free.nrw.commons.upload.FileUtilsWrapper
import fr.free.nrw.commons.upload.StashUploadResult
import fr.free.nrw.commons.upload.StashUploadState
import fr.free.nrw.commons.upload.UploadClient
import fr.free.nrw.commons.upload.UploadResult
@ -46,13 +47,14 @@ import timber.log.Timber
import java.util.*
import java.util.regex.Pattern
import javax.inject.Inject
import kotlin.collections.ArrayList
class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
CoroutineWorker(appContext, workerParams) {
private var notificationManager: NotificationManagerCompat? = null
@Inject
lateinit var wikidataEditService: WikidataEditService
@ -79,6 +81,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
private val PROCESSING_UPLOADS_NOTIFICATION_ID = 101
//Attributes of the current-upload notification
private var currentNotificationID: Int = -1// lateinit is not allowed with primitives
private lateinit var currentNotificationTag: String
@ -295,7 +298,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
* Upload the contribution
* @param contribution
*/
@SuppressLint("StringFormatInvalid")
@SuppressLint("StringFormatInvalid", "CheckResult")
private suspend fun uploadContribution(contribution: Contribution) {
if (contribution.localUri == null || contribution.localUri.path == null) {
Timber.e("""upload: ${contribution.media.filename} failed, file path is null""")
@ -338,7 +341,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
val stashUploadResult = uploadClient.uploadFileToStash(
filename!!, contribution, notificationProgressUpdater
).onErrorReturn{
return@onErrorReturn StashUploadResult(StashUploadState.FAILED,fileKey = null)
return@onErrorReturn StashUploadResult(StashUploadState.FAILED,fileKey = null,errorMessage = it.message)
}.blockingSingle()
when (stashUploadResult.state) {
@ -402,10 +405,21 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
}
else -> {
Timber.e("""upload file to stash failed with status: ${stashUploadResult.state}""")
showFailedNotification(contribution)
showInvalidLoginNotification(contribution)
contribution.state = Contribution.STATE_FAILED
contribution.chunkInfo = null
contributionDao.saveSynchronous(contribution)
if (stashUploadResult.errorMessage.equals(CsrfTokenClient.INVALID_TOKEN_ERROR_MESSAGE)) {
Timber.e("Invalid Login, logging out")
val username = sessionManager.userName
var logoutListener = CommonsApplication.BaseLogoutListener(
appContext,
appContext.getString(R.string.invalid_login_message),
username
)
CommonsApplication.getInstance()
.clearApplicationData(appContext, logoutListener)
}
}
}
}catch (exception: Exception){
@ -566,6 +580,23 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
curentNotification.build()
)
}
@SuppressLint("StringFormatInvalid")
private fun showInvalidLoginNotification(contribution: Contribution) {
val displayTitle = contribution.media.displayTitle
curentNotification.setContentTitle(
appContext.getString(
R.string.upload_failed_notification_title,
displayTitle
)
)
.setContentText(appContext.getString(R.string.invalid_login_message))
.setProgress(0, 0, false)
.setOngoing(false)
notificationManager?.notify(
currentNotificationTag, currentNotificationID,
curentNotification.build()
)
}
/**
* Notify that the current upload is paused
@ -605,5 +636,4 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
}
};
}
}

View file

@ -792,6 +792,7 @@ Upload your first media by tapping on the add button.</string>
<string name="edit_location">Edit Location</string>
<string name="send_thanks_to_author">Thank the author</string>
<string name="error_sending_thanks">Error sending thanks to author.</string>
<string name="invalid_login_message">Your login has expired, Please login again.</string>
<plurals name="custom_picker_images_selected_title_appendix">
<item quantity="one">%d image selected</item>
<item quantity="other">%d images selected</item>