Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,21 @@ class ContributionsPresenter @Inject internal constructor(
contribution.state = Contribution.STATE_QUEUED
saveContribution(contribution)
} else {
// For nearby uploads, if image already exists but Wikidata linking failed,
// we should retry just the Wikidata operation instead of deleting
if (contribution.wikidataPlace != null) {
Timber.d("Nearby upload: Image exists on Commons, retrying Wikidata P18 linking")
contribution.state = Contribution.STATE_QUEUED
saveContribution(contribution)
} else {
Timber.e("Contribution already exists")
compositeDisposable!!.add(
contributionsRepository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe()
)
}
}
})
}
Expand Down
16 changes: 14 additions & 2 deletions app/src/main/java/fr/free/nrw/commons/media/MediaClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,25 @@ class MediaClient
if (pages.isEmpty()) {
Single.just(emptyList())
} else {
// If any pageId is invalid (0 or negative), avoid fetching entities like M0
// and convert using a blank entity to prevent wbgetentities no-such-entity errors.
if (pages.any { it.pageId() <= 0 }) {
Single.just(
pages.mapNotNull { page ->
page.imageInfo()?.let { imageInfo ->
mediaConverter.convert(page, Entities.Entity(), imageInfo)
}
},
)
} else {
getEntities(pages.map { "$PAGE_ID_PREFIX${it.pageId()}" })
.map {
pages
.zip(it.entities().values)
.mapNotNull { (page, entity) ->
page.imageInfo()?.let {
mediaConverter.convert(page, entity, it)
page.imageInfo()?.let { imageInfo ->
mediaConverter.convert(page, entity, imageInfo)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import android.content.Intent
import android.content.pm.ServiceInfo
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.multidex.BuildConfig
Expand Down Expand Up @@ -340,6 +343,38 @@ class UploadWorker(
)

try {
// Check if this is a retry for a nearby upload that already succeeded on Commons
// but failed on Wikidata - identified by having retries > 0, wikidataPlace != null,
// and non-empty filename (meaning it was previously uploaded)
if (contribution.retries > 0 && contribution.wikidataPlace != null &&
!contribution.media.filename.isNullOrEmpty()) {

Timber.d("Nearby upload retry detected - filename already exists: ${contribution.media.filename}")

// Create a minimal UploadResult directly from the saved filename
val uploadResult = UploadResult(
result = "Success",
filekey = "",
offset = 0,
filename = contribution.media.filename!!
)

// Skip the Commons upload and proceed directly to Wikidata operation
Timber.d("Skipping Commons upload, proceeding directly to Wikidata P18 linking")
try {
Timber.d("Retry: Directly attempting Wikidata P18 linking with existing filename")
makeWikiDataEdit(uploadResult, contribution)
Timber.d("Retry: Wikidata P18 linking succeeded!")
showSuccessNotification(contribution)
return
} catch (exception: Exception) {
Timber.e(exception, "Retry: Wikidata P18 linking failed")
contribution.state = Contribution.STATE_FAILED
contributionDao.saveSynchronous(contribution)
showFailedNotification(contribution)
throw exception // Let WorkManager handle retry
}
}
// Upload the file to stash
val stashUploadResult =
uploadClient
Expand Down Expand Up @@ -390,11 +425,17 @@ class UploadWorker(
Timber.d(
"WikiDataEdit required, making wikidata edit",
)

// Save the filename for retry in case Wikidata operation fails
Timber.d("Saving Commons filename to media: ${uploadResult.filename}")
contribution.media.filename = uploadResult.filename
contributionDao.saveSynchronous(contribution)

makeWikiDataEdit(uploadResult, contribution)
}
showSuccessNotification(contribution)
if (appContext.contentResolver.persistedUriPermissions.any {
it.uri == contribution.contentUri }) {
it.uri == contribution.contentUri }) {
appContext.contentResolver.releasePersistableUriPermission(
contribution.contentUri!!, Intent.FLAG_GRANT_READ_URI_PERMISSION
)
Expand Down Expand Up @@ -467,6 +508,9 @@ class UploadWorker(
contributionDao.saveSynchronous(contribution)
}

// For testing - set to true to enable 10-second delay before Wikidata operations
private val ENABLE_TESTING_DELAY = false

/**
* Make the WikiData Edit, if applicable
*/
Expand All @@ -479,6 +523,20 @@ class UploadWorker(
if (!contribution.hasInvalidLocation()) {
var revisionID: Long? = null
val p18WasSkipped = !wikiDataPlace.imageValue.isNullOrBlank()

// Testing mechanism: Add 10-second delay before Wikidata operations to simulate network issues
if (ENABLE_TESTING_DELAY) {
Timber.d("TESTING: Adding 10-second delay before Wikidata operation")
Handler(Looper.getMainLooper()).post {
Toast.makeText(applicationContext, "Starting Wikidata operation - testing delay", Toast.LENGTH_LONG).show()
}
Thread.sleep(10000) // 10 seconds delay for testing

Handler(Looper.getMainLooper()).post {
Toast.makeText(applicationContext, "Proceeding with Wikidata after delay", Toast.LENGTH_LONG).show()
}
}

try {
if (!p18WasSkipped) {
// Only set P18 if the place does not already have a picture
Expand All @@ -504,7 +562,19 @@ class UploadWorker(
// Always show success notification, whether P18 was set or skipped
showSuccessNotification(contribution)
} catch (exception: Exception) {
Timber.e(exception)
Timber.e(exception, "Wikidata P18 linking failed")

// Mark contribution as failed so it can be retried
contribution.state = Contribution.STATE_FAILED
contributionDao.saveSynchronous(contribution)

// Show failed notification
showFailedNotification(contribution)

// Re-throw to allow WorkManager to retry
// If it's a network error, WorkManager will automatically retry
// Network errors: UnknownHostException, SocketException, SocketTimeoutException, etc.
throw exception
}

withContext(Dispatchers.Main) {
Expand Down Expand Up @@ -548,8 +618,16 @@ class UploadWorker(
private fun saveIntoUploadedStatus(contribution: Contribution) {
contribution.contentUri?.let {
val imageSha1 = contribution.imageSHA1.toString()
val modifiedSha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(contribution.localUri?.path))
val modifiedSha1 = try {
fileUtilsWrapper.getSHA1(
fileUtilsWrapper.getFileInputStream(contribution.localUri?.path),
)
} catch (e: Exception) {
Timber.w(e, "UploadedStatus: modified file missing/unreadable; falling back to original SHA1")
imageSha1
}
CoroutineScope(Dispatchers.IO).launch {
try {
uploadedStatusDao.insertUploaded(
UploadedStatus(
imageSha1,
Expand All @@ -558,6 +636,9 @@ class UploadWorker(
true,
),
)
} catch (e: Exception) {
Timber.w(e, "UploadedStatus: insert failed; continuing")
}
}
}
}
Expand Down
Loading