Skip to content
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ dependencies {
// Kotlin + coroutines
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.androidx.work.runtime)
implementation(libs.kotlinx.coroutines.rx2)
testImplementation(libs.androidx.work.testing)

//Glide
Expand Down
2 changes: 0 additions & 2 deletions app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ class DeleteHelper @Inject constructor(
media: Media?,
reason: String?
): Single<Boolean>? {

if(context == null && media == null) {
return null
}
Expand Down Expand Up @@ -86,7 +85,6 @@ class DeleteHelper @Inject constructor(
* @return
*/
private fun delete(media: Media, reason: String): Observable<Boolean> {
Timber.d("thread is delete %s", Thread.currentThread().name)
val summary = "Nominating ${media.filename} for deletion."
val calendar = Calendar.getInstance()
val fileDeleteString = """
Expand Down
51 changes: 30 additions & 21 deletions app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,19 @@ package fr.free.nrw.commons.delete

import android.annotation.SuppressLint
import android.content.Context

import fr.free.nrw.commons.utils.DateUtil
import java.util.Locale

import javax.inject.Inject
import javax.inject.Singleton

import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.profile.achievements.FeedbackResponse
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.utils.DateUtil
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Single
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.rx2.rxSingle
import timber.log.Timber
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton

/**
* This class handles the reason for deleting a Media object
Expand All @@ -29,6 +27,8 @@ class ReasonBuilder @Inject constructor(
private val viewUtilWrapper: ViewUtilWrapper
) {

private val defaultFileUsagePageSize = 10

/**
* To process the reason and append the media's upload date and uploaded_by_me string
* @param media
Expand All @@ -39,7 +39,7 @@ class ReasonBuilder @Inject constructor(
if (media == null || reason == null) {
return Single.just("Not known")
}
return fetchArticleNumber(media, reason)
return getAndAppendFileUsage(media, reason)
}

/**
Expand All @@ -54,27 +54,36 @@ class ReasonBuilder @Inject constructor(
}
}

private fun fetchArticleNumber(media: Media, reason: String): Single<String> {
return if (checkAccount()) {
okHttpJsonApiClient
.getAchievements(sessionManager.userName)
.map { feedbackResponse -> appendArticlesUsed(feedbackResponse, media, reason) }
} else {
Single.just("")
private fun getAndAppendFileUsage(media: Media, reason: String): Single<String> {
return rxSingle(context = Dispatchers.IO) {
if (!checkAccount()) return@rxSingle ""

try {
val globalFileUsage = okHttpJsonApiClient.getGlobalFileUsages(
fileName = media.filename,
pageSize = defaultFileUsagePageSize
)
val globalUsages = globalFileUsage?.query?.pages?.sumOf { it.fileUsage.size } ?: 0

appendArticlesUsed(globalUsages, media, reason)
} catch (e: Exception) {
Timber.e(e, "Error fetching file usage")
throw e
}
}
}

/**
* Takes the uploaded_by_me string, the upload date, name of articles using images
* Takes the uploaded_by_me string, the upload date, no. of articles using images
* and appends it to the received reason
* @param feedBack object
* @param fileUsages No. of files/articles using this image
* @param media whose upload data is to be fetched
* @param reason
* @param reason string to be appended
*/
@SuppressLint("StringFormatInvalid")
private fun appendArticlesUsed(feedBack: FeedbackResponse, media: Media, reason: String): String {
private fun appendArticlesUsed(fileUsages: Int, media: Media, reason: String): String {
val reason1Template = context.getString(R.string.uploaded_by_myself)
return reason + String.format(Locale.getDefault(), reason1Template, prettyUploadedDate(media), feedBack.articlesUsingImages)
return reason + String.format(Locale.getDefault(), reason1Template, prettyUploadedDate(media), fileUsages)
.also { Timber.i("New Reason %s", it) }
}

Expand Down
79 changes: 44 additions & 35 deletions app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.CameraPosition
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.CommonsApplication.Companion.instance
import fr.free.nrw.commons.locationpicker.LocationPicker
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
Expand Down Expand Up @@ -102,6 +101,7 @@ import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.language.AppLanguageLookUpTable
import fr.free.nrw.commons.location.LocationServiceManager
import fr.free.nrw.commons.locationpicker.LocationPicker
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
import fr.free.nrw.commons.profile.ProfileActivity
import fr.free.nrw.commons.review.ReviewHelper
Expand All @@ -116,6 +116,7 @@ import fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources
import fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE
import fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction
import fr.free.nrw.commons.utils.PermissionUtils.hasPermission
import fr.free.nrw.commons.utils.ViewUtil
import fr.free.nrw.commons.utils.ViewUtil.showShortToast
import fr.free.nrw.commons.utils.ViewUtilWrapper
import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage.Revision
Expand All @@ -125,6 +126,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import org.apache.commons.lang3.StringUtils
import timber.log.Timber
import java.lang.String.format
import java.util.Date
import java.util.Locale
import java.util.Objects
Expand Down Expand Up @@ -1646,7 +1648,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
getString(R.string.cancel),
{
val reason: String = input.text.toString()
onDeleteClickeddialogtext(reason)
onDeleteClickedDialogText(reason)
},
{},
input
Expand Down Expand Up @@ -1700,26 +1702,48 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
resultSingle
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { _ ->
if (applicationKvStore.getBoolean(
String.format(
NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl
), false
)
) {
applicationKvStore.remove(
String.format(
NOMINATING_FOR_DELETION_MEDIA,
media!!.imageUrl
)
)
callback!!.nominatingForDeletion(index)
}
}
.subscribe(this::handleDeletionResult, this::handleDeletionError);
}

/**
* Disables Progress Bar and Update delete button text.
*/
private fun disableProgressBar() {
activity?.run {
runOnUiThread(Runnable {
binding.progressBarDeletion.visibility = View.GONE
})
} ?: return // Prevent NullPointerException when fragment is not attached to activity
}

private fun handleDeletionResult(success: Boolean) {
if (success) {
binding.nominateDeletion.text = getString(R.string.nominated_for_deletion_btn)
ViewUtil.showLongSnackbar(requireView(), getString(R.string.nominated_for_deletion))
disableProgressBar()
checkAndClearDeletionFlag()
} else {
disableProgressBar()
}
}

private fun handleDeletionError(throwable: Throwable) {
throwable.printStackTrace()
disableProgressBar()
checkAndClearDeletionFlag()
}

private fun checkAndClearDeletionFlag() {
if (applicationKvStore
.getBoolean(format(NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl), false)
) {
applicationKvStore.remove(format(NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl))
callback!!.nominatingForDeletion(index)
}
}

@SuppressLint("CheckResult")
private fun onDeleteClickeddialogtext(reason: String) {
private fun onDeleteClickedDialogText(reason: String) {
applicationKvStore.putBoolean(
String.format(
NOMINATING_FOR_DELETION_MEDIA,
Expand All @@ -1736,22 +1760,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
resultSingletext
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { _ ->
if (applicationKvStore.getBoolean(
String.format(
NOMINATING_FOR_DELETION_MEDIA, media!!.imageUrl
), false
)
) {
applicationKvStore.remove(
String.format(
NOMINATING_FOR_DELETION_MEDIA,
media!!.imageUrl
)
)
callback!!.nominatingForDeletion(index)
}
}
.subscribe(this::handleDeletionResult, this::handleDeletionError);
}

private fun onSeeMoreClicked() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,7 @@ class OkHttpJsonApiClient @Inject constructor(
FeedbackResponse::class.java
)
} catch (e: Exception) {
e.printStackTrace()
return@fromCallable FeedbackResponse(0, 0, 0, FeaturedImages(0, 0), 0, "")
}
}
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@
<string name="deletion_reason_no_longer_want_public">I changed my mind, I don\'t want it to be publicly visible anymore</string>
<string name="deletion_reason_not_interesting">Sorry this picture is not interesting for an encyclopedia</string>

<string name="uploaded_by_myself">Uploaded by myself on %1$s, used in %2$d article(s).</string>
<string name="uploaded_by_myself">Uploaded by myself on %1$s, used in %2$d article(s) at least.</string>

<string name="no_uploads">Welcome to Commons!\n
Upload your first media by tapping on the add button.</string>
Expand Down Expand Up @@ -876,4 +876,5 @@ Upload your first media by tapping on the add button.</string>
<string name="show_in_nearby">Show in Nearby</string>
<string name="image_tag_line_created_and_uploaded_by">Created and uploaded by: %1$s</string>
<string name="image_tag_line_created_by_and_uploaded_by">Created by %1$s and uploaded by %2$s</string>
<string name="nominated_for_deletion_btn">Nominated for Deletion</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import android.content.res.Resources
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.fileusages.GlobalFileUsagesResponse
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.achievements.FeedbackResponse
import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse
import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Observable
import io.reactivex.Single
import kotlinx.coroutines.test.runTest
import media
import org.junit.Before
import org.junit.Test
Expand Down Expand Up @@ -58,25 +59,25 @@ class ReasonBuilderTest {
PowerMockito.`when`(context?.getString(R.string.user_not_logged_in))
.thenReturn("Log-in expired. Please log in again.")

reasonBuilder!!.getReason(mock(Media::class.java), "test")
reasonBuilder!!.getReason(mock(Media::class.java), "test").test().await()
verify(sessionManager, times(1))!!.forceLogin(any(Context::class.java))
}

@Test
fun getReason() {
fun getReason() = runTest {
`when`(sessionManager?.userName).thenReturn("Testuser")
`when`(sessionManager?.doesAccountExist()).thenReturn(true)
`when`(okHttpJsonApiClient!!.getAchievements(anyString()))
.thenReturn(Single.just(mock(FeedbackResponse::class.java)))
`when`(okHttpJsonApiClient!!.getGlobalFileUsages(anyString(), anyInt()))
.thenReturn(mock(GlobalFileUsagesResponse::class.java))
`when`(okHttpJsonApiClient!!.getLeaderboard(anyString(), anyString(), anyString(), anyString(), anyString()))
.thenReturn(Observable.just(mock(LeaderboardResponse::class.java)))
`when`(okHttpJsonApiClient!!.setAvatar(anyString(), anyString()))
.thenReturn(Single.just(mock(UpdateAvatarResponse::class.java)))

val media = media(filename = "test_file", dateUploaded = Date())

reasonBuilder!!.getReason(media, "test")
reasonBuilder!!.getReason(media, "test").test().await()
verify(sessionManager, times(0))!!.forceLogin(any(Context::class.java))
verify(okHttpJsonApiClient, times(1))!!.getAchievements(anyString())
verify(okHttpJsonApiClient, times(1))!!.getGlobalFileUsages(anyString(), anyInt())
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ constraintlayout = "1.1.3"
coordinates2country = "1.8"
dexcount = "4.0.0"
githubTripletPlay = "2.7.2"
kotlinxCoroutinesRx2 = "1.8.0"
osmdroidAndroid = "6.1.17"
testCore = "1.4.0"
coreKtx = "1.9.0"
Expand Down Expand Up @@ -127,6 +128,7 @@ dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref =
facebook-fresco = { module = "com.facebook.fresco:fresco", version.ref = "frescoVersion" }
glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
kotlinx-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "kotlinxCoroutinesRx2" }
photoview = { module = "com.github.chrisbanes:PhotoView", version.ref = "photoviewVersion" }

# RxJava and Reactive Programming
Expand Down