Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
555fe59
Initial plan
Copilot Mar 28, 2026
2884292
Migrate image loading dependencies from Fresco + Glide to Coil
Copilot Mar 28, 2026
3336054
Replace Fresco views with standard ImageView/PhotoView in XML layouts
Copilot Mar 28, 2026
bde504c
Replace Fresco with Coil image loader in CommonsApplication
Copilot Mar 28, 2026
b9dcb7c
Migrate SimpleDraweeView to ImageView + Coil in 5 adapter files
Copilot Mar 28, 2026
c96953c
Migrate from Fresco ImageRequest to Coil for image loading
Copilot Mar 28, 2026
b7c05cd
Replace Fresco with Coil in MediaDetailFragment, QuizActivity, and Si…
Copilot Mar 28, 2026
6aa944b
Migrate SetWallpaperWorker and PicOfDayAppWidget from Fresco to Coil
Copilot Mar 28, 2026
50d2e69
Replace Glide with Coil 2.x in 4 files
Copilot Mar 28, 2026
f08c6ba
Migrate ZoomableActivity from Fresco ZoomableDraweeView to PhotoView …
Copilot Mar 28, 2026
020fdd8
Remove Fresco initialization and imports from test files
Copilot Mar 28, 2026
aa90847
Migrate from Fresco and Glide to Coil for image loading
Copilot Mar 28, 2026
72c076d
Fix remaining compilation errors: update missed files using Fresco se…
Copilot Mar 28, 2026
c798331
Merge branch 'main' into copilot/migrate-from-fresco-glide-to-coil
RitikaPahwa4444 Apr 9, 2026
3034d2e
Upgrade Coil from 2.7.0 to 3.1.0
Copilot Apr 11, 2026
1360b74
Remove .kotlin error logs from tracking and add to .gitignore
Copilot Apr 11, 2026
1d97b56
Clean up unused imports and duplicate .gitignore entry
Copilot Apr 11, 2026
17227b4
Fix images not loading: configure Coil with app's OkHttpClient for Us…
Copilot Apr 11, 2026
57e96ba
Fix media details page not loading + revert unrelated changes
Copilot Apr 11, 2026
c962907
Merge remote-tracking branch 'origin/main' into copilot/migrate-from-…
Copilot Apr 11, 2026
d73a229
Fix image caching flicker in media details screen
Copilot Apr 11, 2026
e4151db
Fix media details: show thumbnail preview while full image loads
Copilot Apr 11, 2026
25bbc47
Fix stale image: clear old image before loading thumbnail in setupIma…
Copilot Apr 11, 2026
29b07a9
Fix stale/cached image issues following Coil best practices
Copilot Apr 11, 2026
62a687d
Plan: remove imageLoadGeneration, add loading spinner for media detail
Copilot Apr 11, 2026
6b6a2dc
Remove all workarounds — use only standard Coil API
Copilot Apr 11, 2026
0a7ee58
Address code review: fallback to full image on thumbnail error, remov…
Copilot Apr 11, 2026
e095860
Fix wrong image flash: remove static placeholder, use spinner only
Copilot Apr 11, 2026
f2322bc
Clear stale drawable before Coil load to prevent old image flash
Copilot Apr 11, 2026
3c3310b
Fix stale image flash: disable crossfade on initial load in MediaDeta…
Copilot Apr 11, 2026
2a5b48a
Simplify setupImageView: remove nested Coil loads and placeholderMemo…
Copilot Apr 11, 2026
047baa2
Remove duplicate stale Javadoc comment on setupImageView
Copilot Apr 11, 2026
f202e4b
Code cleanup
RitikaPahwa4444 Apr 11, 2026
a9cb8e4
upload: simplify Coil setup and centralize upload thumbnails
Copilot Apr 22, 2026
45e0a2e
upload: remove redundant helper crossfade handling
Copilot Apr 22, 2026
36632f1
upload: parameterize placeholder handling in helper
Copilot Apr 22, 2026
2bf581d
upload: polish helper error handling messages
Copilot Apr 22, 2026
a471211
commons: clarify shared Coil client cache behavior
Copilot Apr 22, 2026
362fe0d
build: remove accidental Kotlin session artifact
Copilot Apr 22, 2026
7b6f9dc
image: downsample upload fallback and remove runBlocking
Copilot Apr 22, 2026
ac23ab1
upload: refine downsample size calculation
Copilot Apr 22, 2026
4e1fc08
upload: fix downsample loop condition
Copilot Apr 22, 2026
fc0ef27
upload: polish fallback load handling
Copilot Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,15 @@ dependencies {
// Utils
implementation(libs.gson)
implementation(libs.okhttp)
implementation(libs.coil)
implementation(libs.coil.network.okhttp)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.retrofit.adapter.rxjava)
implementation(libs.rxandroid)
implementation(libs.rxjava)
implementation(libs.rxbinding)
implementation(libs.rxbinding.appcompat)
implementation(libs.facebook.fresco)
implementation(libs.facebook.fresco.middleware)
implementation(libs.apache.commons.lang3)

// UI
Expand Down Expand Up @@ -299,7 +299,6 @@ dependencies {
testImplementation(libs.androidx.core.testing)
testImplementation(libs.junit.jupiter.api)
testRuntimeOnly(libs.junit.jupiter.engine)
testImplementation(libs.soloader)
testImplementation(libs.kotlinx.coroutines.test)
debugImplementation(libs.androidx.fragment.testing)
testImplementation(libs.commons.io)
Expand Down Expand Up @@ -349,9 +348,6 @@ dependencies {
implementation(libs.kotlinx.coroutines.rx2)
testImplementation(libs.androidx.work.testing)

//Glide
implementation(libs.glide)
annotationProcessor(libs.glide.compiler)
kaptTest(libs.androidx.databinding.compiler)
kaptAndroidTest(libs.androidx.databinding.compiler)

Expand Down
81 changes: 64 additions & 17 deletions app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,17 @@ import android.os.Build
import android.os.Process
import android.util.Log
import androidx.multidex.MultiDexApplication
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import coil3.ImageLoader
import coil3.request.crossfade
import coil3.SingletonImageLoader
import coil3.disk.DiskCache
import coil3.memory.MemoryCache
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
import coil3.intercept.Interceptor as CoilInterceptor
import coil3.request.ErrorResult
import coil3.request.ImageResult
import okhttp3.OkHttpClient
import okio.Path.Companion.toPath
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
Expand All @@ -28,7 +37,6 @@ 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
import fr.free.nrw.commons.settings.Prefs
import fr.free.nrw.commons.upload.FileUtils
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
Expand Down Expand Up @@ -83,7 +91,7 @@ class CommonsApplication : MultiDexApplication() {
lateinit var cookieJar: CommonsCookieJar

@Inject
lateinit var customOkHttpNetworkFetcher: CustomOkHttpNetworkFetcher
lateinit var okHttpClient: OkHttpClient

var languageLookUpTable: AppLanguageLookUpTable? = null
private set
Expand Down Expand Up @@ -116,17 +124,29 @@ class CommonsApplication : MultiDexApplication() {
defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet)
}

// Set DownsampleEnabled to True to downsample the image in case it's heavy
val config = ImagePipelineConfig.newBuilder(this)
.setNetworkFetcher(customOkHttpNetworkFetcher)
.setDownsampleEnabled(true)
// Initialize Coil with the shared app OkHttpClient so image requests keep the
// existing headers, logging, timeout, and HTTP cache configuration.
val imageLoader = ImageLoader.Builder(this)
.crossfade(true)
.components {
add(OkHttpNetworkFetcherFactory(callFactory = { okHttpClient }))
// Skip image loading when limited connection mode is enabled
// (replaces the original CustomOkHttpNetworkFetcher behavior)
add(LimitedConnectionModeInterceptor(defaultPrefs))
}
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(this, 0.25)
.build()
}
.diskCache {
DiskCache.Builder()
.directory(cacheDir.resolve("image_cache").absolutePath.toPath())
.maxSizePercent(0.02)
.build()
}
.build()
try {
Fresco.initialize(this, config)
} catch (e: Exception) {
Timber.e(e)
// TODO: Remove when we're able to initialize Fresco in test builds.
}
SingletonImageLoader.setSafe { imageLoader }

createNotificationChannel(this)

Expand Down Expand Up @@ -227,11 +247,12 @@ class CommonsApplication : MultiDexApplication() {
}

/**
* Clear all images cache held by Fresco
* Clear all images cache held by Coil
*/
private fun clearImageCache() {
val imagePipeline = Fresco.getImagePipeline()
imagePipeline.clearCaches()
val imageLoader = SingletonImageLoader.get(this)
imageLoader.memoryCache?.clear()
imageLoader.diskCache?.clear()
}

/**
Expand Down Expand Up @@ -416,3 +437,29 @@ class CommonsApplication : MultiDexApplication() {
}
}

/**
* Coil interceptor that skips network image loading when limited connection mode is enabled.
* This replaces the original CustomOkHttpNetworkFetcher behavior from the Fresco setup.
*/
class LimitedConnectionModeInterceptor(
private val defaultKvStore: JsonKvStore
) : CoilInterceptor {

override suspend fun intercept(chain: CoilInterceptor.Chain): ImageResult {
val isLimitedConnectionMode = defaultKvStore.getBoolean(
CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false
)
if (isLimitedConnectionMode && chain.request.data is String) {
val url = chain.request.data as String
if (url.startsWith("http://") || url.startsWith("https://")) {
Timber.d("Skipping image load in limited connection mode: %s", url)
return ErrorResult(
image = null,
request = chain.request,
throwable = Exception("Limited connection mode enabled")
)
}
}
return chain.proceed()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.RecyclerView
import com.facebook.drawee.view.SimpleDraweeView
import coil3.load
import coil3.request.placeholder
import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
Expand All @@ -24,7 +26,7 @@ class BookmarkItemsAdapter(
) : RecyclerView.ViewHolder(itemView) {
var depictsLabel: TextView = itemView.findViewById(R.id.depicts_label)
var description: TextView = itemView.findViewById(R.id.description)
var depictsImage: SimpleDraweeView = itemView.findViewById(R.id.depicts_image)
var depictsImage: ImageView = itemView.findViewById(R.id.depicts_image)
var layout: ConstraintLayout = itemView.findViewById(R.id.layout_item)
}

Expand All @@ -48,9 +50,11 @@ class BookmarkItemsAdapter(
holder.description.text = depictedItem.description

if (depictedItem.imageUrl?.isNotBlank() == true) {
holder.depictsImage.setImageURI(depictedItem.imageUrl)
holder.depictsImage.load(depictedItem.imageUrl) {
placeholder(R.drawable.ic_wikidata_logo_24dp)
}
} else {
holder.depictsImage.setActualImageResource(R.drawable.ic_wikidata_logo_24dp)
holder.depictsImage.setImageResource(R.drawable.ic_wikidata_logo_24dp)
}
holder.layout.setOnClickListener {
WikidataItemDetailsActivity.startYourself(context, depictedItem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageView
import android.widget.TextView
import com.facebook.drawee.view.SimpleDraweeView
import coil3.load
import coil3.request.placeholder
import coil3.request.error
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R

Expand Down Expand Up @@ -71,14 +74,17 @@ class GridViewAdapter(
)

val item = data?.get(position)
val imageView = view.findViewById<SimpleDraweeView>(R.id.categoryImageView)
val imageView = view.findViewById<ImageView>(R.id.categoryImageView)
val fileName = view.findViewById<TextView>(R.id.categoryImageTitle)
val uploader = view.findViewById<TextView>(R.id.categoryImageAuthor)

item?.let {
fileName.text = it.mostRelevantCaption
setUploaderView(it, uploader)
imageView.setImageURI(it.thumbUrl)
imageView.load(it.thumbUrl) {
placeholder(R.drawable.image_placeholder)
error(R.drawable.image_placeholder)
}
}

return view
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import android.view.View
import android.webkit.URLUtil
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
import coil3.load
import coil3.request.placeholder
import coil3.request.error
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.utils.MediaAttributionUtil
import fr.free.nrw.commons.MediaDataExtractor
Expand All @@ -33,8 +34,6 @@ class ContributionViewHolder internal constructor(
private var contribution: Contribution? = null
private var isWikipediaButtonDisplayed = false
private val pausingPopUp: AlertDialog
var imageRequest: ImageRequest? = null
private set

init {
binding.contributionImage.setOnClickListener { v: View? -> imageClicked() }
Expand Down Expand Up @@ -62,30 +61,20 @@ an upload might take a dozen seconds. */
binding.contributionTitle.text = contribution.media.mostRelevantCaption
setAuthorText(contribution.media)

//Removes flicker of loading image.
binding.contributionImage.hierarchy.fadeDuration = 0

binding.contributionImage.hierarchy.setPlaceholderImage(R.drawable.image_placeholder)
binding.contributionImage.hierarchy.setFailureImage(R.drawable.image_placeholder)

val imageSource = chooseImageSource(
contribution.media.thumbUrl,
contribution.localUri
)
if (!TextUtils.isEmpty(imageSource)) {
if (URLUtil.isHttpsUrl(imageSource)) {
imageRequest = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
.setProgressiveRenderingEnabled(true)
.build()
} else if (URLUtil.isFileUrl(imageSource)) {
imageRequest = ImageRequest.fromUri(Uri.parse(imageSource))
} else if (imageSource != null) {
val file = File(imageSource)
imageRequest = ImageRequest.fromFile(file)
val data: Any = when {
URLUtil.isHttpsUrl(imageSource) -> imageSource!!
URLUtil.isFileUrl(imageSource) -> Uri.parse(imageSource)
imageSource != null -> File(imageSource)
else -> R.drawable.image_placeholder
}

if (imageRequest != null) {
binding.contributionImage.setImageRequest(imageRequest)
binding.contributionImage.load(data) {
placeholder(R.drawable.image_placeholder)
error(R.drawable.image_placeholder)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,54 +5,47 @@ import android.app.NotificationManager
import android.app.WallpaperManager
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.work.Worker
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.facebook.common.executors.CallerThreadExecutor
import com.facebook.common.references.CloseableReference
import com.facebook.datasource.DataSource
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber
import com.facebook.imagepipeline.image.CloseableImage
import com.facebook.imagepipeline.request.ImageRequestBuilder
import coil3.SingletonImageLoader
import coil3.request.ImageRequest
import coil3.request.SuccessResult
import coil3.request.allowHardware
import coil3.toBitmap
import fr.free.nrw.commons.R
import timber.log.Timber

class SetWallpaperWorker(context: Context, params: WorkerParameters) :
Worker(context, params) {
override fun doWork(): Result {
CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val context = applicationContext
createNotificationChannel(context)
showProgressNotification(context)

val imageUrl = inputData.getString("imageUrl") ?: return Result.failure()

val imageRequest = ImageRequestBuilder
.newBuilderWithSource(Uri.parse(imageUrl))
.build()

val imagePipeline = Fresco.getImagePipeline()
val dataSource = imagePipeline.fetchDecodedImage(imageRequest, context)

dataSource.subscribe(object : BaseBitmapDataSubscriber() {
public override fun onNewResultImpl(bitmap: Bitmap?) {
if (dataSource.isFinished && bitmap != null) {
Timber.d("Bitmap loaded from url %s", imageUrl.toString())
setWallpaper(context, Bitmap.createBitmap(bitmap))
dataSource.close()
}
}

override fun onFailureImpl(dataSource: DataSource<CloseableReference<CloseableImage?>?>) {
Timber.d("Error getting bitmap from image url %s", imageUrl.toString())
return try {
val imageLoader = SingletonImageLoader.get(context)
val request = ImageRequest.Builder(context)
.data(imageUrl)
.allowHardware(false)
.build()
val result = imageLoader.execute(request)
if (result is SuccessResult) {
val bitmap = result.image.toBitmap()
setWallpaper(context, bitmap)
} else {
Timber.d("Error getting bitmap from image url %s", imageUrl)
showNotification(context, "Setting Wallpaper Failed", "Failed to download image.")
dataSource.close()
}
}, CallerThreadExecutor.getInstance())

return Result.success()
Result.success()
} catch (e: Exception) {
Timber.e(e, "Error getting bitmap from image url %s", imageUrl)
showNotification(context, "Setting Wallpaper Failed", "Failed to download image.")
Result.failure()
}
}

private fun setWallpaper(context: Context, bitmap: Bitmap) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import coil3.load
import coil3.request.crossfade
import fr.free.nrw.commons.R
import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.model.Folder
Expand Down Expand Up @@ -77,7 +78,9 @@ class FolderAdapter(
}
} else {
val previewImage = folder.images[0]
Glide.with(holder.image).load(previewImage.uri).into(holder.image)
holder.image.load(previewImage.uri) {
crossfade(true)
}
holder.name.text = folder.name
holder.count.text = count.toString()
holder.itemView.setOnClickListener {
Expand Down
Loading