Merge branch 'main' into fix/sparql-exclude-closed-locations

This commit is contained in:
Nicolas Raoul 2025-04-11 12:19:19 +09:00 committed by GitHub
commit 11b74953b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 213 additions and 122 deletions

View file

@ -65,6 +65,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
private val delegate: AppCompatDelegate by lazy {
AppCompatDelegate.create(this, null)
}
private var lastLoginResult: LoginResult? = null
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -271,6 +272,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
showLoggingProgressBar()
loginClient.doLogin(username,
password,
lastLoginResult,
twoFactorCode,
Locale.getDefault().language,
object : LoginCallback {
@ -280,9 +282,17 @@ class LoginActivity : AccountAuthenticatorActivity() {
onLoginSuccess(loginResult)
}
override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread {
override fun twoFactorPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread {
Timber.d("Requesting 2FA prompt")
progressDialog!!.dismiss()
lastLoginResult = loginResult
askUserForTwoFactorAuth()
}
override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) {
Timber.d("Requesting email auth prompt")
progressDialog!!.dismiss()
lastLoginResult = loginResult
askUserForTwoFactorAuth()
}
@ -341,12 +351,13 @@ class LoginActivity : AccountAuthenticatorActivity() {
progressDialog!!.dismiss()
with(binding!!) {
twoFactorContainer.visibility = View.VISIBLE
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
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)
showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed))
}
@VisibleForTesting

View file

@ -32,7 +32,7 @@ class CsrfTokenClient(
try {
if (retry > 0) {
// Log in explicitly
loginClient.loginBlocking(userName, password, "")
loginClient.loginBlocking(userName, password)
}
// Get CSRFToken response off the main thread.
@ -92,6 +92,8 @@ class CsrfTokenClient(
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
override fun twoFactorPrompt() = cb.twoFactorPrompt()
override fun emailAuthPrompt() = cb.emailAuthPrompt()
},
)
@ -165,10 +167,17 @@ class CsrfTokenClient(
}
override fun twoFactorPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
) = callback.twoFactorPrompt()
override fun emailAuthPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
) = callback.emailAuthPrompt()
// Should not happen here, but call the callback just in case.
override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password."))
@ -190,6 +199,8 @@ class CsrfTokenClient(
fun failure(caught: Throwable?)
fun twoFactorPrompt()
fun emailAuthPrompt()
}
companion object {

View file

@ -4,6 +4,13 @@ interface LoginCallback {
fun success(loginResult: LoginResult)
fun twoFactorPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
)
fun emailAuthPrompt(
loginResult: LoginResult,
caught: Throwable,
token: String?,
)

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.auth.login
import android.text.TextUtils
import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
@ -51,6 +52,7 @@ class LoginClient(
password,
null,
null,
null,
response.body()!!.query()!!.loginToken(),
userLanguage,
cb,
@ -75,6 +77,7 @@ class LoginClient(
password: String,
retypedPassword: String?,
twoFactorCode: String?,
emailAuthCode: String?,
loginToken: String?,
userLanguage: String,
cb: LoginCallback,
@ -82,7 +85,7 @@ class LoginClient(
this.userLanguage = userLanguage
loginCall =
if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else {
loginInterface.postLogIn(
@ -90,6 +93,7 @@ class LoginClient(
password,
retypedPassword,
twoFactorCode,
emailAuthCode,
loginToken,
userLanguage,
true,
@ -112,10 +116,18 @@ class LoginClient(
when (loginResult) {
is OAuthResult ->
cb.twoFactorPrompt(
loginResult,
LoginFailedException(loginResult.message),
loginToken,
)
is EmailAuthResult ->
cb.emailAuthPrompt(
loginResult,
LoginFailedException(loginResult.message),
loginToken
)
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
is LoginResult.Result ->
@ -147,6 +159,7 @@ class LoginClient(
fun doLogin(
username: String,
password: String,
lastLoginResult: LoginResult?,
twoFactorCode: String,
userLanguage: String,
loginCallback: LoginCallback,
@ -159,7 +172,10 @@ class LoginClient(
) = if (response.isSuccessful) {
val loginToken = response.body()?.query()?.loginToken()
loginToken?.let {
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
login(username, password, null,
if (lastLoginResult is OAuthResult) twoFactorCode else null,
if (lastLoginResult is EmailAuthResult) twoFactorCode else null,
it, userLanguage, loginCallback)
} ?: run {
loginCallback.error(IOException("Failed to retrieve login token"))
}
@ -181,7 +197,8 @@ class LoginClient(
fun loginBlocking(
userName: String,
password: String,
twoFactorCode: String?,
twoFactorCode: String? = null,
emailAuthCode: String? = null
) {
val tokenResponse = getLoginToken().execute()
if (tokenResponse
@ -195,7 +212,7 @@ class LoginClient(
val loginToken = tokenResponse.body()?.query()?.loginToken()
val tempLoginCall =
if (twoFactorCode.isNullOrEmpty()) {
if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else {
loginInterface.postLogIn(
@ -203,6 +220,7 @@ class LoginClient(
password,
null,
twoFactorCode,
emailAuthCode,
loginToken,
userLanguage,
true,
@ -214,7 +232,7 @@ class LoginClient(
val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.")
if ("UI" == loginResult.status) {
if (loginResult is OAuthResult) {
if (loginResult is OAuthResult || loginResult is EmailAuthResult) {
// TODO: Find a better way to boil up the warning about 2FA
throw LoginFailedException(loginResult.message)
}

View file

@ -35,7 +35,8 @@ interface LoginInterface {
@Field("password") pass: String?,
@Field("retype") retypedPass: String?,
@Field("OATHToken") twoFactorCode: String?,
@Field("logintoken") token: String?,
@Field("token") emailAuthToken: String?,
@Field("logintoken") loginToken: String?,
@Field("uselang") userLanguage: String?,
@Field("logincontinue") loginContinue: Boolean,
): Call<LoginResponse?>

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.auth.login
import com.google.gson.annotations.SerializedName
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
import fr.free.nrw.commons.auth.login.LoginResult.Result
import fr.free.nrw.commons.wikidata.mwapi.MwServiceError
@ -27,11 +28,13 @@ internal class ClientLogin {
fun toLoginResult(password: String): LoginResult {
var userMessage = message
if ("UI" == status) {
if (requests != null) {
for (req in requests) {
if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) {
requests?.forEach { request ->
request.id()?.let {
if (it.endsWith("TOTPAuthenticationRequest")) {
return OAuthResult(status, userName, password, message)
} else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) {
} else if (it.endsWith("EmailAuthAuthenticationRequest")) {
return EmailAuthResult(status, userName, password, message)
} else if (it.endsWith("PasswordAuthenticationRequest")) {
return ResetPasswordResult(status, userName, password, message)
}
}
@ -49,7 +52,7 @@ internal class Request {
private val required: String? = null
private val provider: String? = null
private val account: String? = null
private val fields: Map<String, RequestField>? = null
internal val fields: Map<String, RequestField>? = null
fun id(): String? = id
}
@ -57,5 +60,5 @@ internal class Request {
internal class RequestField {
private val type: String? = null
private val label: String? = null
private val help: String? = null
internal val help: String? = null
}

View file

@ -24,6 +24,13 @@ sealed class LoginResult(
message: String?,
) : LoginResult(status, userName, password, message)
class EmailAuthResult(
status: String,
userName: String?,
password: String?,
message: String?,
) : LoginResult(status, userName, password, message)
class ResetPasswordResult(
status: String,
userName: String?,

View file

@ -775,7 +775,11 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
* @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be removed.
*/
private void removeMarker(BaseMarker nearbyBaseMarker) {
Place place = nearbyBaseMarker.getPlace();
if (nearbyBaseMarker == null || nearbyBaseMarker.getPlace().getName() == null) {
return;
}
String target = nearbyBaseMarker.getPlace().getName();
List<Overlay> overlays = binding.mapView.getOverlays();
ItemizedOverlayWithFocus item;
@ -784,8 +788,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment
item = (ItemizedOverlayWithFocus) overlays.get(i);
OverlayItem overlayItem = item.getItem(0);
if (place.location.getLatitude() == overlayItem.getPoint().getLatitude()
&& place.location.getLongitude() == overlayItem.getPoint().getLongitude()) {
if (overlayItem.getTitle().equals(target)) {
binding.mapView.getOverlays().remove(i);
binding.mapView.invalidate();
break;

View file

@ -14,6 +14,7 @@ import android.os.Bundle
import android.provider.Settings
import android.view.View
import android.widget.CheckBox
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
@ -122,7 +123,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
/**
* Set the value of the showPermissionDialog variable.
*
* @param showPermissionsDialog `true` to indicate to show
* @property isShowPermissionsDialog `true` to indicate to show
* Permissions Dialog if permissions are missing, `false` otherwise.
*/
/**
@ -166,6 +167,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
private var _binding: ActivityUploadBinding? = null
private val binding: ActivityUploadBinding get() = _binding!!
private lateinit var onBackPressedCallback: OnBackPressedCallback
@SuppressLint("CheckResult")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -173,6 +176,23 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
_binding = ActivityUploadBinding.inflate(layoutInflater)
setContentView(binding.root)
// Overrides the back button to make sure the user is prepared to lose their progress
onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
showAlertDialog(
this@UploadActivity,
getString(R.string.back_button_warning),
getString(R.string.back_button_warning_desc),
getString(R.string.back_button_continue),
getString(R.string.back_button_warning),
null
) {
finish()
}
}
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
/*
If Configuration of device is changed then get the new fragments
created by the system and populate the fragments ArrayList
@ -187,7 +207,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
}
init()
binding.rlContainerTitle.setOnClickListener { v: View? -> onRlContainerTitleClicked() }
binding.rlContainerTitle.setOnClickListener { _: View? -> onRlContainerTitleClicked() }
nearbyPopupAnswers = mutableMapOf()
//getting the current dpi of the device and if it is less than 320dp i.e. overlapping
//threshold, thumbnails automatically minimizes
@ -201,7 +221,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
}
locationManager!!.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER)
locationManager!!.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER)
store = BasicKvStore(this, storeNameForCurrentUploadImagesSize).apply {
store = BasicKvStore(this, STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE).apply {
clearAll()
}
checkStoragePermissions()
@ -241,7 +261,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
override fun onPageSelected(position: Int) {
currentSelectedPosition = position
if (position >= uploadableFiles!!.size) {
if (position >= uploadableFiles.size) {
binding.cvContainerTopCard.visibility = View.GONE
} else {
thumbnailsAdapter!!.notifyDataSetChanged()
@ -274,7 +294,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.filter { result: Boolean? -> result!! }
.subscribe { result: Boolean? ->
.subscribe { _: Boolean? ->
showAlertDialog(
this,
getString(R.string.block_notification_title),
@ -284,7 +304,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
})
}
fun checkStoragePermissions() {
private fun checkStoragePermissions() {
// Check if all required permissions are granted
val hasAllPermissions = hasPermission(this, PERMISSIONS_STORAGE)
val hasPartialAccess = hasPartialAccess(this)
@ -355,7 +375,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
showLongToast(this, messageResourceId)
}
override fun getUploadableFiles(): List<UploadableFile>? {
override fun getUploadableFiles(): List<UploadableFile> {
return uploadableFiles
}
@ -367,6 +387,14 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
override fun onUploadMediaDeleted(index: Int) {
fragments!!.removeAt(index) //Remove the corresponding fragment
uploadableFiles.removeAt(index) //Remove the files from the list
val isMediaDetailFragment = fragments!!.getOrNull(currentSelectedPosition)?.let {
it is UploadMediaDetailFragment
} ?: false
if(!isMediaDetailFragment) {
// Should hide the top card current fragment is not the media detail fragment
showHideTopCard(false)
}
thumbnailsAdapter!!.notifyItemRemoved(index) //Notify the thumbnails adapter
uploadImagesAdapter!!.notifyDataSetChanged() //Notify the ViewPager
}
@ -375,8 +403,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
binding.tvTopCardTitle.text = resources
.getQuantityString(
R.plurals.upload_count_title,
uploadableFiles!!.size,
uploadableFiles!!.size
uploadableFiles.size,
uploadableFiles.size
)
}
@ -444,15 +472,16 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
receiveInternalSharedItems()
}
if (uploadableFiles == null || uploadableFiles!!.isEmpty()) {
if (uploadableFiles.isEmpty()) {
handleNullMedia()
} else {
//Show thumbnails
if (uploadableFiles!!.size > 1) {
if (!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")) { //If there is only file, no need to show the image thumbnails
if (uploadableFiles.size > 1) {
if (!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")) {
// If there is only file, no need to show the image thumbnails
showAlertDialogForCategories()
}
if (uploadableFiles!!.size > 3 &&
if (uploadableFiles.size > 3 &&
!defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload")
) {
showAlertForBattery()
@ -464,8 +493,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
binding.tvTopCardTitle.text = resources
.getQuantityString(
R.plurals.upload_count_title,
uploadableFiles!!.size,
uploadableFiles!!.size
uploadableFiles.size,
uploadableFiles.size
)
@ -474,7 +503,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
}
for (uploadableFile in uploadableFiles!!) {
for (uploadableFile in uploadableFiles) {
val uploadMediaDetailFragment = UploadMediaDetailFragment()
if (!uploadIsOfAPlace) {
@ -497,8 +526,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
object : UploadMediaDetailFragmentCallback {
override fun deletePictureAtIndex(index: Int) {
store!!.putInt(
keyForCurrentUploadImagesSize,
(store!!.getInt(keyForCurrentUploadImagesSize) - 1)
KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE,
(store!!.getInt(KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE) - 1)
)
presenter!!.deletePictureAtIndex(index)
}
@ -576,11 +605,11 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
fragments!!.add(mediaLicenseFragment!!)
} else {
for (i in 1 until fragments!!.size) {
fragments!![i]!!.callback = object : UploadBaseFragment.Callback {
fragments!![i].callback = object : UploadBaseFragment.Callback {
override fun onNextButtonClicked(index: Int) {
if (index < fragments!!.size - 1) {
binding.vpUpload.setCurrentItem(index + 1, false)
fragments!![index + 1]!!.onBecameVisible()
fragments!![index + 1].onBecameVisible()
(binding.rvThumbnails.layoutManager as LinearLayoutManager)
.scrollToPositionWithOffset(
if ((index > 0)) index - 1 else 0,
@ -594,7 +623,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
override fun onPreviousButtonClicked(index: Int) {
if (index != 0) {
binding.vpUpload.setCurrentItem(index - 1, true)
fragments!![index - 1]!!.onBecameVisible()
fragments!![index - 1].onBecameVisible()
(binding.rvThumbnails.layoutManager as LinearLayoutManager)
.scrollToPositionWithOffset(
if ((index > 3)) index - 2 else 0,
@ -632,11 +661,12 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
binding.vpUpload.offscreenPageLimit = fragments!!.size
}
// Saving size of uploadableFiles
store!!.putInt(keyForCurrentUploadImagesSize, uploadableFiles!!.size)
store!!.putInt(KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE, uploadableFiles.size)
}
/**
* Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail.
* Changes current image when one image upload is cancelled, to highlight next image in the top
* thumbnail.
* Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511)
*
* @param index Index of image to be removed
@ -771,7 +801,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
override fun onNextButtonClicked(index: Int) {
if (index < fragments!!.size - 1) {
binding.vpUpload.setCurrentItem(index + 1, false)
fragments!![index + 1]!!.onBecameVisible()
fragments!![index + 1].onBecameVisible()
(binding.rvThumbnails.layoutManager as LinearLayoutManager)
.scrollToPositionWithOffset(if ((index > 0)) index - 1 else 0, 0)
if (index < fragments!!.size - 4) {
@ -786,10 +816,10 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
override fun onPreviousButtonClicked(index: Int) {
if (index != 0) {
binding.vpUpload.setCurrentItem(index - 1, true)
fragments!![index - 1]!!.onBecameVisible()
fragments!![index - 1].onBecameVisible()
(binding.rvThumbnails.layoutManager as LinearLayoutManager)
.scrollToPositionWithOffset(if ((index > 3)) index - 2 else 0, 0)
if ((index != 1) && ((index - 1) < uploadableFiles!!.size)) {
if ((index != 1) && ((index - 1) < uploadableFiles.size)) {
// Shows the top card if it was hidden because of the last image being deleted and
// now the user has hit previous button to go back to the media details
showHideTopCard(true)
@ -797,7 +827,10 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
}
}
override fun onThumbnailDeleted(position: Int) = presenter!!.deletePictureAtIndex(position)
override fun onThumbnailDeleted(position: Int) {
presenter!!.deletePictureAtIndex(position)
thumbnailsAdapter?.notifyDataSetChanged()
}
/**
* The adapter used to show image upload intermediate fragments
@ -824,11 +857,11 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
}
fun onRlContainerTitleClicked() {
private fun onRlContainerTitleClicked() {
binding.rvThumbnails.visibility =
if (isTitleExpanded) View.GONE else View.VISIBLE
isTitleExpanded = !isTitleExpanded
binding.ibToggleTopCard.rotation = binding.ibToggleTopCard.rotation + 180
binding.ibToggleTopCard.rotation += 180
}
override fun onDestroy() {
@ -845,21 +878,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
if (uploadCategoriesFragment != null) {
uploadCategoriesFragment!!.callback = null
}
}
/**
* Overrides the back button to make sure the user is prepared to lose their progress
*/
@SuppressLint("MissingSuperCall")
override fun onBackPressed() {
showAlertDialog(
this,
getString(R.string.back_button_warning),
getString(R.string.back_button_warning_desc),
getString(R.string.back_button_continue),
getString(R.string.back_button_warning),
null
) { finish() }
onBackPressedCallback.remove()
}
/**
@ -879,7 +898,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
.setView(view)
.setTitle(getString(R.string.multiple_files_depiction_header))
.setMessage(getString(R.string.multiple_files_depiction))
.setPositiveButton("OK") { dialog: DialogInterface?, which: Int ->
.setPositiveButton("OK") { _: DialogInterface?, _: Int ->
if (checkBox.isChecked) {
// Save the user's choice to not show the dialog again
defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true)
@ -958,7 +977,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
Also, location information is discarded if the difference between
current location and location recorded just before capturing the image
is greater than 100 meters */
if (isLocationTagUnchecked || locationDifference > 100 || !defaultKvStore.getBoolean("inAppCameraLocationPref")
if (isLocationTagUnchecked || locationDifference > 100
|| !defaultKvStore.getBoolean("inAppCameraLocationPref")
|| !isInAppCameraUpload
) {
currLocation = null
@ -979,8 +999,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
@JvmField
var nearbyPopupAnswers: MutableMap<Place, Boolean>? = null
const val keyForCurrentUploadImagesSize: String = "CurrentUploadImagesSize"
const val storeNameForCurrentUploadImagesSize: String = "CurrentUploadImageQualities"
const val KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE: String = "CurrentUploadImagesSize"
const val STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE: String = "CurrentUploadImageQualities"
/**
* Sets the flag indicating whether the upload is of a specific place.

View file

@ -146,11 +146,7 @@ class UploadPresenter @Inject internal constructor(
override fun deletePictureAtIndex(index: Int) {
val uploadableFiles = view.getUploadableFiles()
if (index == uploadableFiles!!.size - 1) {
// If the next fragment to be shown is not one of the MediaDetailsFragment
// lets hide the top card so that it doesn't appear on the other fragments
view.showHideTopCard(false)
}
uploadableFiles?.let {
view.setImageCancelled(true)
repository.deletePicture(uploadableFiles[index].getFilePath())
if (uploadableFiles.size == 1) {
@ -175,6 +171,7 @@ class UploadPresenter @Inject internal constructor(
//In case lets update the number of uploadable media
view.updateTopCardTitle()
}
}
override fun onAttachView(view: UploadContract.View) {
this.view = view

View file

@ -532,7 +532,7 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
basicKvStore!!.putBoolean(keyForShowingAlertDialog, false)
if (isInternetConnectionEstablished(requireActivity())) {
val sizeOfUploads = basicKvStore!!.getInt(
UploadActivity.keyForCurrentUploadImagesSize
UploadActivity.KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE
)
for (i in indexOfFragment until sizeOfUploads) {
presenter.getImageQuality(

View file

@ -310,7 +310,7 @@ class UploadMediaPresenter @Inject constructor(
private fun storeImageQuality(
imageResult: Int, uploadItemIndex: Int, activity: Activity, uploadItem: UploadItem
) {
val store = BasicKvStore(activity, UploadActivity.storeNameForCurrentUploadImagesSize)
val store = BasicKvStore(activity, UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE)
val value = store.getString(UPLOAD_QUALITIES_KEY, null)
try {
val jsonObject = value.asJsonObject().apply {
@ -339,8 +339,10 @@ class UploadMediaPresenter @Inject constructor(
*/
override fun checkImageQuality(uploadItem: UploadItem, index: Int) {
if ((uploadItem.imageQuality != IMAGE_OK) && (uploadItem.imageQuality != IMAGE_KEEP)) {
val value = basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) }
val value = basicKvStoreFactory?.let { it(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) }
?.getString(UPLOAD_QUALITIES_KEY, null)
try {
val imageQuality = value.asJsonObject()["UploadItem$index"] as Int
view.showProgress(false)
@ -363,8 +365,9 @@ class UploadMediaPresenter @Inject constructor(
* @param index Index of the UploadItem which was deleted
*/
override fun updateImageQualitiesJSON(size: Int, index: Int) {
val value = basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) }
val value = basicKvStoreFactory?.let { it(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) }
?.getString(UPLOAD_QUALITIES_KEY, null)
try {
val jsonObject = value.asJsonObject().apply {
for (i in index until (size - 1)) {
@ -372,7 +375,8 @@ class UploadMediaPresenter @Inject constructor(
}
remove("UploadItem" + (size - 1))
}
basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) }
basicKvStoreFactory?.let { it(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) }
?.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString())
} catch (e: Exception) {
Timber.e(e)

View file

@ -146,6 +146,7 @@
android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginRight="@dimen/standard_gap"
android:layout_marginBottom="@dimen/standard_gap"
android:hint="@string/_2fa_code"
android:visibility="gone"
app:passwordToggleEnabled="false"
tools:visibility="visible">
@ -154,9 +155,7 @@
android:id="@+id/login_two_factor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/_2fa_code"
android:imeOptions="flagNoExtractUi"
android:inputType="number"
android:visibility="gone"
tools:visibility="visible" />

View file

@ -168,6 +168,7 @@
<string name="login_failed_throttled">الكثير من المحاولات غير الناجحة. الرجاء المحاولة مرة أخرى في بضع دقائق.</string>
<string name="login_failed_blocked">عذراً، لقد تم منع هذا المستخدم على كومنز</string>
<string name="login_failed_2fa_needed">يجب توفير رمز التحقق المزدوج.</string>
<string name="login_failed_email_auth_needed">تم إرسال رمز التحقق إلى بريدك الإلكتروني. يُرجى إدخال الرمز لتسجيل الدخول.</string>
<string name="login_failed_generic">فشل تسجيل الدخول</string>
<string name="share_upload_button">ارفع</string>
<string name="multiple_share_base_title">اسم هذه المجموعة</string>
@ -273,6 +274,7 @@
<string name="become_a_tester_title">انضم لمختبري اصدارات Beta (بيتا)</string>
<string name="become_a_tester_description">يمكنك الاشتراك في القناة التجريبية على جوجل بلاي والحصول على إمكانية الوصول المبكر إلى الميزات الجديدة وإصلاحات الأخطاء</string>
<string name="_2fa_code">رمز التحقق المزدوج 2FA</string>
<string name="email_auth_code">رمز التحقق من البريد الإلكتروني</string>
<string name="logout_verification">أترغب فعلا في الخروج؟</string>
<string name="mediaimage_failed">صورة الوسائط فشلت</string>
<string name="no_subcategory_found">لم يتم العثور على تصنيفات فرعية.</string>

View file

@ -119,6 +119,7 @@
<string name="login_failed_throttled">For mange mislykkede forsøg. Prøv igen om et par minutter.</string>
<string name="login_failed_blocked">Beklager, denne bruger er blevet blokeret på Commons</string>
<string name="login_failed_2fa_needed">Du skal angive din tofaktorgodkendelseskode.</string>
<string name="login_failed_email_auth_needed">En login-bekræftelseskode er blevet sendt til din e-mailadresse. Angiv koden for at logge ind.</string>
<string name="login_failed_generic">Login mislykkedes</string>
<string name="share_upload_button">Upload</string>
<string name="multiple_share_base_title">Navngiv dette sæt</string>
@ -224,6 +225,7 @@
<string name="become_a_tester_title">Bliv betatester</string>
<string name="become_a_tester_description">Registrer dig på vores betakanal på Google Play og få tidlig adgang til nye funktioner og fejlrettelser</string>
<string name="_2fa_code">2FA-kode</string>
<string name="email_auth_code">E-mail-bekræftelseskode</string>
<string name="logout_verification">Ønsker du at logge ud?</string>
<string name="mediaimage_failed">Mediebillede mislykkedes</string>
<string name="no_subcategory_found">Ingen underkategorier fundet</string>

View file

@ -119,7 +119,7 @@
<string name="login_failed_network">ログインできません - ネットワークのエラーです</string>
<string name="login_failed_throttled">失敗した回数が多すぎます。数分後にもう一度お試しください。</string>
<string name="login_failed_blocked">申し訳ありませんが、この利用者はコモンズでブロックされています。</string>
<string name="login_failed_2fa_needed">2段階認証コードを入力してください。</string>
<string name="login_failed_2fa_needed">二要素認証コードを入力してください。</string>
<string name="login_failed_generic">ログイン失敗</string>
<string name="share_upload_button">アップロード</string>
<string name="multiple_share_base_title">このセットに名前をつけてください</string>
@ -221,7 +221,7 @@
<string name="media_detail_coordinates_empty">情報なし</string>
<string name="become_a_tester_title">ベータ版テスターになる</string>
<string name="become_a_tester_description">Google Playのベータ版チャンネルにオプトインして、新機能やバグ修正プログラムに早期にアクセス</string>
<string name="_2fa_code">2段階認証コード</string>
<string name="_2fa_code">2FAコード</string>
<string name="logout_verification">ログアウトしてもよろしいですか?</string>
<string name="mediaimage_failed">メディアイメージが失敗しました</string>
<string name="no_subcategory_found">下位カテゴリは見つかりませんでした</string>

View file

@ -179,7 +179,7 @@
<string name="statistics_thanks">Spasî Hate Wergirtin</string>
<string name="statistics_featured">Wêneyên Bijartî</string>
<string name="statistics_wikidata_edits">Wêneyên bi riya \"Cihên Nêz\"</string>
<string name="level" fuzzy="true">Derece</string>
<string name="level">Derece %d</string>
<string name="images_uploaded">Wêneyên Barkirî</string>
<string name="review_thanks_yes_button_text">Wêneyê din</string>
<string name="review_thanks_no_button_text">Belê, çima na</string>

View file

@ -99,6 +99,7 @@
<string name="login_failed_throttled">Ze dacks ouni Succès probéiert. Probéiert wgl. an e puer Minutten nach eng Kéier.</string>
<string name="login_failed_blocked">Pardon, dëse Benotzer ass op Commons gespaart</string>
<string name="login_failed_2fa_needed">Dir musst de Code vun Ärer Zwee-Facteur-Authentifizéierung uginn.</string>
<string name="login_failed_email_auth_needed">Dir hutt ee Login-Verifikatiounscode per E-Mail geschéckt kritt. Gitt wgl. de Code an, fir Iech anzeloggen.</string>
<string name="login_failed_generic">Aloggen huet net funktionéiert</string>
<string name="share_upload_button">Eroplueden</string>
<string name="multiple_share_base_title">Gitt dëser Biller een Numm</string>
@ -192,6 +193,7 @@
<string name="become_a_tester_title">Beta-Tester ginn</string>
<string name="become_a_tester_description">Schreift Iech op GooglePlay a fir eise Beta-Kanal a kritt fréi Zougang zu neie Funktiounen a Verbesserunge vu Feeler</string>
<string name="_2fa_code">2FA-Code</string>
<string name="email_auth_code">E-Mail-Verifikatiounscode</string>
<string name="logout_verification">Wëllt dir Iech wierklech ausloggen?</string>
<string name="no_subcategory_found">Keng Ënnerkategorie fonnt</string>
<string name="welcome_image_mount_zao">Bierg Zao</string>

View file

@ -113,6 +113,7 @@
<string name="login_failed_throttled">Направени се премногу неуспешни обиди. Обидете се пак за некоја минута.</string>
<string name="login_failed_blocked">За жал, корисникот е блокиран на Ризницата</string>
<string name="login_failed_2fa_needed">Мора да го укажете вашиот код за двочинителска заверка.</string>
<string name="login_failed_email_auth_needed">На вашата е-поштенска адреса ви испративме потврден код за најава. Внесете го кодот за да се најавите.</string>
<string name="login_failed_generic">Најавата не успеа</string>
<string name="share_upload_button">Подигни</string>
<string name="multiple_share_base_title">Дајте му име на овој комплет</string>
@ -218,6 +219,7 @@
<string name="become_a_tester_title">Станете бета-испробувач</string>
<string name="become_a_tester_description">Пријавете се на нашиот бета-канал на Google Play и добивајте ран пристап до нови можности и исправки на грешки</string>
<string name="_2fa_code">З-код</string>
<string name="email_auth_code">Испрати го потврдниот код на е-пошта</string>
<string name="logout_verification">Дали навистина сакате да се одјавите?</string>
<string name="mediaimage_failed">Сликата не успеа</string>
<string name="no_subcategory_found">Не пронајдов поткатегории</string>

View file

@ -111,6 +111,7 @@
<string name="login_failed_throttled">Tròpi tentativ falì. Për piasì, ch\'a preuva torna da-sì chèiche minute.</string>
<string name="login_failed_blocked">An dëspias, s\'utent-sì a l\'é stàit blocà ansima a Commons</string>
<string name="login_failed_2fa_needed">A dev fornì sò còdes d\'autentificassion a doi fator.</string>
<string name="login_failed_email_auth_needed">Un còdes ëd verìfica ëd conession a l\'é stàit mandà a soa adrëssa ëd pòsta eletrònica. Për piasì, ch\'a anserissa col còdes për intré ant ël sistema.</string>
<string name="login_failed_generic">Falì a rintré ant ël sistema</string>
<string name="share_upload_button">Carié</string>
<string name="multiple_share_base_title">Deje un nòm a s\'ansem</string>
@ -216,6 +217,7 @@
<string name="become_a_tester_title">Dventé në sperimentador Beta</string>
<string name="become_a_tester_description">Anscriv-se a nòstr canal beta su Google Play a oten-e n\'acess antissipà a le neuve fonsionalità e coression ëd givo</string>
<string name="_2fa_code">Còdes 2FA</string>
<string name="email_auth_code">Mandé un còdes ëd verifica</string>
<string name="logout_verification">Veul-lo për da bon seurte dal sistema?</string>
<string name="mediaimage_failed">Faliment ëd la plancia dël mojen</string>
<string name="no_subcategory_found">Gnun-e sot-categorìe trovà</string>

View file

@ -47,7 +47,14 @@
<item quantity="one">%d upload</item>
<item quantity="other">%d پورته کول</item>
</plurals>
<string name="share_license_summary" fuzzy="true">دا انځور به د %1$s په منښتليک سمبال وي.</string>
<plurals name="share_license_summary">
<item quantity="one">دا انځور به د منښتليک %1$s لاندې وي</item>
<item quantity="other">دا انځورونه به د %1$s منښتليک لاندې وي</item>
</plurals>
<plurals name="upload_count_title">
<item quantity="one">%1$d راپورته کول</item>
<item quantity="other">%1$d راپورته کېدنې</item>
</plurals>
<string name="navigation_item_explore">سپړنه</string>
<string name="preference_category_appearance">ښکارېدنه</string>
<string name="preference_category_general">ټولګړی</string>

View file

@ -696,7 +696,7 @@
<string name="place_state_wlm">WLM</string>
<string name="wlm_upload_info">Essa imagem será enviada ao concurso Wiki Loves Monuments</string>
<string name="display_monuments">Monumentos de exibição</string>
<string name="wlm_month_message">Estamos no mês no Wiki Loves Monuments!</string>
<string name="wlm_month_message">Chegou o mês do Wiki Loves Monuments!</string>
<string name="learn_more">SABER MAIS</string>
<string name="wlm_campaign_title">Wiki Loves Monuments</string>
<string name="wlm_campaign_description">O Wiki Loves Monuments é um concurso internacional organizado pela Wikimedia sobre fotografias de monumentos</string>

View file

@ -136,6 +136,7 @@
<string name="login_failed_throttled">失敗次數過多。請於幾分鐘後重試。</string>
<string name="login_failed_blocked">很抱歉,該使用者已被維基共享資源封禁</string>
<string name="login_failed_2fa_needed">必須提供您的雙重驗證代碼。</string>
<string name="login_failed_email_auth_needed">登入驗證碼已發送到您的電子郵件地址。請提供驗證碼以登入。</string>
<string name="login_failed_generic">登入失敗</string>
<string name="share_upload_button">上傳</string>
<string name="multiple_share_base_title">給這個集合命名</string>
@ -241,6 +242,7 @@
<string name="become_a_tester_title">成為 Beta 測試員</string>
<string name="become_a_tester_description">選擇加入我們在 Google Play 上的測試版頻道並儘早訪問新功能和錯誤修復</string>
<string name="_2fa_code">2FA 代碼</string>
<string name="email_auth_code">電子郵件驗證碼</string>
<string name="logout_verification">您確定要登出嗎?</string>
<string name="mediaimage_failed">媒體圖片失敗</string>
<string name="no_subcategory_found">找不到子分類</string>

View file

@ -111,6 +111,7 @@
<string name="login_failed_throttled">Too many unsuccessful attempts. Please try again in a few minutes.</string>
<string name="login_failed_blocked">Sorry, this user has been blocked on Commons</string>
<string name="login_failed_2fa_needed">You must provide your two factor authentication code.</string>
<string name="login_failed_email_auth_needed">A login verification code has been sent to your email address. Please provide the code to log in.</string>
<string name="login_failed_generic">Log-in failed</string>
<string name="share_upload_button">Upload</string>
<string name="multiple_share_base_title">Name this set</string>
@ -218,6 +219,7 @@
<string name="become_a_tester_description">Opt-in to our beta channel on Google Play and get early access to new features and bug fixes</string>
<string name="beta_opt_in_link">https://play.google.com/apps/testing/fr.free.nrw.commons</string>
<string name="_2fa_code">2FA Code</string>
<string name="email_auth_code">Email verification code</string>
<string name="logout_verification">Do you really want to logout?</string>
<string name="mediaimage_failed">Media Image Failed</string>
<string name="no_subcategory_found">No subcategories found</string>

View file

@ -262,15 +262,4 @@ class UploadActivityUnitTests {
method.isAccessible = true
method.invoke(activity)
}
@Test
@Throws(Exception::class)
fun testOnBackPressed() {
val method: Method =
UploadActivity::class.java.getDeclaredMethod(
"onBackPressed",
)
method.isAccessible = true
method.invoke(activity)
}
}