diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 9305e82eab..93413213d2 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -9,9 +9,11 @@ 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 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.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.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 @@ 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; @@ -137,12 +143,12 @@ public AppLanguageLookUpTable getLanguageLookUpTable() { ContributionDao contributionDao; /** - * In-memory list of contributions whose uploads have been paused by the user + * In-memory list of contributions whose uploads have been paused by the user */ public static Map pauseUploads = new HashMap<>(); /** - * In-memory list of uploads that have been cancelled by the user + * In-memory list of uploads that have been cancelled by the user */ public static HashSet cancelledUploads = new HashSet<>(); @@ -339,4 +345,96 @@ public interface LogoutListener { 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(); + } + } } + diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 7bb60fbd06..1aa62d7b5c 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -24,7 +24,6 @@ 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; @@ -54,6 +53,8 @@ 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 { @@ -97,6 +98,9 @@ public void onCreate(Bundle savedInstanceState) { 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); @@ -115,6 +119,12 @@ public void onCreate(Bundle savedInstanceState) { } 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). diff --git a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt index 423d9fb34b..f4141c170d 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt @@ -23,6 +23,7 @@ class CsrfTokenClient( private var retries = 0 private var csrfTokenCall: Call? = 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." } } diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java index 0b044565fa..608a1b11f8 100644 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java @@ -19,10 +19,10 @@ 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 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 @@ protected void onLogoutClicked() { .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 @@ protected void onProfileClicked() { 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(); - } - } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index a9a47baba9..6e0654a3ed 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -72,10 +72,10 @@ import com.mapbox.mapboxsdk.geometry.LatLng; import com.mapbox.mapboxsdk.geometry.LatLngBounds; 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; @@ -1408,8 +1408,7 @@ public void displayLoginSkippedWarning() { .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(); @@ -1455,18 +1454,6 @@ public boolean backButtonClicked() { * 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) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/StashUploadResult.kt b/app/src/main/java/fr/free/nrw/commons/upload/StashUploadResult.kt index 8d2e4f2d07..68a28bdbb8 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/StashUploadResult.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/StashUploadResult.kt @@ -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 { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.kt index e082501d8d..db60621067 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadClient.kt @@ -54,7 +54,7 @@ class UploadClient @Inject constructor( ): Observable { 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() 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, index: AtomicInteger, - mediaType: MediaType, file: File, totalChunks: Int + errorMessage : AtomicReference, 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) } ) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 864e3149ad..296ffe6a18 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -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) : } }; } - -} \ No newline at end of file +} diff --git a/app/src/main/res/values-yue-hant/error.xml b/app/src/main/res/values-yue-hant/error.xml deleted file mode 100644 index 68579e4a05..0000000000 --- a/app/src/main/res/values-yue-hant/error.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - 同享壞咗 - 哎呀。出咗錯! - 多謝你! - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d0ab4e42a..608c5dcdf3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -792,6 +792,7 @@ Upload your first media by tapping on the add button. Edit Location Thank the author Error sending thanks to author. + Your login has expired, Please login again. %d image selected %d images selected